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/src/cli.js ADDED
@@ -0,0 +1,368 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { installApp } from "./app-installer.js";
5
+ import { createBroker } from "./broker.js";
6
+ import {
7
+ CODEX_RUNNING_ERROR,
8
+ directImportToCodex,
9
+ isCodexRunning,
10
+ } from "./codex-direct-import.js";
11
+ import { readChromiumProfile } from "./chromium-reader.js";
12
+ import { installConfig, installRuntime, readConfig, updatePreferences } from "./config.js";
13
+ import {
14
+ braveCookiePaths,
15
+ appLoginLaunchAgentPath,
16
+ codexCookiePaths,
17
+ configPath,
18
+ installedAppPath,
19
+ installedExtensionDir,
20
+ launchAgentPath,
21
+ loginSyncLaunchAgentPath,
22
+ SOURCE_BROWSERS,
23
+ TARGET_BROWSERS,
24
+ } from "./paths.js";
25
+ import {
26
+ installAppLogin,
27
+ installLoginSync,
28
+ installSchedule,
29
+ removeAppLogin,
30
+ removeLoginSync,
31
+ removeSchedule,
32
+ } from "./scheduler.js";
33
+ import { performUpdate, startDetachedUpdate } from "./updater.js";
34
+
35
+ const HELP = `browser-cookie-bridge
36
+
37
+ Local cookie and session transfer between Chromium browsers and into ChatGPT Codex.
38
+
39
+ Commands:
40
+ setup [--hour 9] [--minute 0] [--no-schedule]
41
+ install-app [--no-open]
42
+ preferences --source brave --target codex --cookies on --history off --menu-bar on --auto-check-updates on
43
+ sync [--timeout 300]
44
+ doctor
45
+ enable-login-sync
46
+ disable-login-sync
47
+ enable-app-login
48
+ disable-app-login
49
+ remove-schedule
50
+ help
51
+ `;
52
+
53
+ export async function main(argv) {
54
+ const [command = "help", ...args] = argv;
55
+ switch (command) {
56
+ case "setup":
57
+ return setup(args);
58
+ case "sync":
59
+ return sync(args);
60
+ case "install-app":
61
+ return installDesktopApp(args);
62
+ case "preferences":
63
+ return preferences(args);
64
+ case "install-update":
65
+ return installUpdate(args);
66
+ case "perform-update":
67
+ return performUpdateWorker(args);
68
+ case "doctor":
69
+ return doctor();
70
+ case "enable-login-sync":
71
+ return enableLoginSync();
72
+ case "disable-login-sync":
73
+ return disableLoginSync();
74
+ case "enable-app-login":
75
+ return setAppLogin(true);
76
+ case "disable-app-login":
77
+ return setAppLogin(false);
78
+ case "remove-schedule":
79
+ return remove();
80
+ case "help":
81
+ case "--help":
82
+ case "-h":
83
+ console.log(HELP);
84
+ return;
85
+ default:
86
+ throw new Error(`Unknown command: ${command}\n\n${HELP}`);
87
+ }
88
+ }
89
+
90
+ function installUpdate(args) {
91
+ assertMacOS();
92
+ const result = startDetachedUpdate({
93
+ version: stringFlag(args, "--version", ""),
94
+ appPath: stringFlag(args, "--app-path", ""),
95
+ appPID: integerFlag(args, "--app-pid", 0, 2, Number.MAX_SAFE_INTEGER),
96
+ });
97
+ console.log(`Update worker started: ${result.workerPID}`);
98
+ }
99
+
100
+ function performUpdateWorker(args) {
101
+ assertMacOS();
102
+ const result = performUpdate({
103
+ version: stringFlag(args, "--version", ""),
104
+ appPath: stringFlag(args, "--app-path", ""),
105
+ appPID: integerFlag(args, "--app-pid", 0, 2, Number.MAX_SAFE_INTEGER),
106
+ });
107
+ console.log(`Updated and relaunched: ${result.destination}`);
108
+ }
109
+
110
+ function preferences(args) {
111
+ const existing = readConfig();
112
+ const config = updatePreferences({
113
+ cookies: booleanFlag(args, "--cookies", existing.imports?.cookies !== false),
114
+ history: booleanFlag(args, "--history", existing.imports?.history === true),
115
+ sourceBrowser: stringFlag(args, "--source", existing.sourceBrowser || "brave"),
116
+ targetBrowser: stringFlag(args, "--target", existing.targetBrowser || "codex"),
117
+ menuBar: booleanFlag(args, "--menu-bar", existing.ui?.menuBar === true),
118
+ openAtLogin: booleanFlag(args, "--open-at-login", existing.ui?.openAtLogin !== false),
119
+ autoCheckUpdates: booleanFlag(args, "--auto-check-updates", existing.ui?.autoCheckUpdates !== false),
120
+ });
121
+ console.log(
122
+ `Saved: source=${config.sourceBrowser}, target=${config.targetBrowser}, cookies=${config.imports.cookies ? "on" : "off"}, history=${config.imports.history ? "on" : "off"}, menu-bar=${config.ui.menuBar ? "on" : "off"}, open-at-login=${config.ui.openAtLogin ? "on" : "off"}, auto-check-updates=${config.ui.autoCheckUpdates ? "on" : "off"}`,
123
+ );
124
+ }
125
+
126
+ function installDesktopApp(args) {
127
+ assertMacOS();
128
+ const firstInstall = !fs.existsSync(configPath());
129
+ const runtime = installRuntime();
130
+ const existing = fs.existsSync(configPath()) ? readConfig() : null;
131
+ const config = installConfig({
132
+ hour: existing?.schedule?.hour ?? 9,
133
+ minute: existing?.schedule?.minute ?? 0,
134
+ });
135
+ console.log("Building the native macOS app…");
136
+ const destination = installApp({ open: !args.includes("--no-open") });
137
+ if (config.ui.openAtLogin) {
138
+ installAppLogin({ appPath: destination, bootstrapNow: false });
139
+ } else {
140
+ removeAppLogin();
141
+ }
142
+ if (firstInstall) {
143
+ const cliPath = path.join(runtime, "bin", "brave-codex-cookie-sync.js");
144
+ installLoginSync({ cliPath });
145
+ console.log("Daily sync is off by default; sync at login is enabled.");
146
+ }
147
+ console.log(`Installed: ${destination}`);
148
+ console.log(destination.startsWith("/Applications/")
149
+ ? "The app is available in Applications and Spotlight."
150
+ : "The app is available in your user Applications folder and Spotlight.");
151
+ }
152
+
153
+ function setup(args) {
154
+ assertMacOS();
155
+ const hour = integerFlag(args, "--hour", 9, 0, 23);
156
+ const minute = integerFlag(args, "--minute", 0, 0, 59);
157
+ const noSchedule = args.includes("--no-schedule");
158
+ const runtime = installRuntime();
159
+ const config = installConfig({ hour, minute });
160
+ let plist = null;
161
+
162
+ if (!noSchedule) {
163
+ plist = installSchedule({
164
+ hour,
165
+ minute,
166
+ cliPath: path.join(runtime, "bin", "brave-codex-cookie-sync.js"),
167
+ });
168
+ }
169
+
170
+ console.log("Setup complete.");
171
+ console.log(`Pinned runtime: ${runtime}`);
172
+ if (config.targetBrowser === "codex") {
173
+ console.log(`Source browser: ${config.sourceBrowser} (read locally; no extension required)`);
174
+ console.log("Target integration: direct local Codex merge (Codex must be closed)");
175
+ } else {
176
+ console.log(`Source extension (${config.sourceBrowser}): ${installedExtensionDir(undefined, config.sourceBrowser)}`);
177
+ console.log(`Target extension (${config.targetBrowser}): ${installedExtensionDir(undefined, config.targetBrowser)}`);
178
+ console.log("In both browsers: open Extensions → Developer mode → Load unpacked → choose the generated extension folder");
179
+ }
180
+ if (plist) console.log(`Daily schedule: ${pad(hour)}:${pad(minute)} (${plist})`);
181
+ console.log(`Broker port: 127.0.0.1:${config.port}`);
182
+ console.log(config.targetBrowser === "codex"
183
+ ? "Codex is backed up and validated before its local browser data is changed."
184
+ : "Cookie values are transferred in memory and are not written to logs or disk.");
185
+ }
186
+
187
+ async function sync(args) {
188
+ assertMacOS();
189
+ const seconds = integerFlag(args, "--timeout", 300, 5, 3600);
190
+ const config = readConfig();
191
+ const isCodexTarget = (config.targetBrowser || "codex") === "codex";
192
+ if (isCodexTarget) {
193
+ if (isCodexRunning()) throw new Error(CODEX_RUNNING_ERROR);
194
+ const payload = readChromiumProfile({
195
+ browser: config.sourceBrowser || "brave",
196
+ imports: config.imports || { cookies: true, history: false },
197
+ });
198
+ console.log(`Read ${payload.cookies.length} cookies and ${payload.history.length} history URLs from ${config.sourceBrowser || "brave"}.`);
199
+ const result = directImportToCodex({
200
+ cookies: payload.cookies,
201
+ history: payload.history,
202
+ codexRunning: false,
203
+ });
204
+ console.log(directCodexSummary(result));
205
+ return result;
206
+ }
207
+ const broker = createBroker({
208
+ token: config.token,
209
+ port: config.port,
210
+ imports: config.imports || { cookies: true, history: false },
211
+ sourceBrowser: config.sourceBrowser || "brave",
212
+ targetBrowser: config.targetBrowser || "codex",
213
+ timeoutMs: seconds * 1000,
214
+ onEvent(event) {
215
+ if (event.type === "listening") {
216
+ console.log(`Waiting for ${config.sourceBrowser || "brave"} and ${config.targetBrowser || "codex"} extensions on 127.0.0.1:${event.port}…`);
217
+ } else if (event.type === "source") {
218
+ console.log(`Received ${event.cookies} cookies and ${event.history} history URLs in memory.`);
219
+ } else if (event.type === "complete") {
220
+ console.log(
221
+ `Cookies: ${event.imported} imported, ${event.skipped} skipped, ${event.failed} failed. History: ${event.historyImported} imported, ${event.historySkipped} skipped, ${event.historyFailed} failed.`,
222
+ );
223
+ }
224
+ },
225
+ });
226
+ await broker.listen();
227
+ const result = await broker.completion;
228
+ console.log(transferSummary(result));
229
+ return result;
230
+ }
231
+
232
+ export function directCodexSummary(result) {
233
+ const imported = result.imported + result.historyImported;
234
+ const skipped = result.skipped + result.historySkipped;
235
+ const failures = result.failed + result.historyFailed;
236
+ if (failures > 0) {
237
+ return `Direct Codex sync completed with warnings: ${imported} imported, ${skipped} skipped, ${failures} failed. Backup: ${result.backupPath}`;
238
+ }
239
+ return `Direct Codex sync complete: ${imported} imported and ${skipped} skipped. Reopen Codex to use the updated sessions. Backup: ${result.backupPath}`;
240
+ }
241
+
242
+ export function transferSummary(result) {
243
+ const failures = result.failed + result.historyFailed;
244
+ const imported = result.imported + result.historyImported;
245
+ const skipped = result.skipped + result.historySkipped;
246
+ return failures > 0
247
+ ? `Partially synced: ${imported} imported, ${skipped} skipped, ${failures} failed. Reload the destination extension and try again.`
248
+ : `Transfer complete: ${imported} imported and ${skipped} skipped.`;
249
+ }
250
+
251
+ function doctor() {
252
+ const home = os.homedir();
253
+ const brave = existing(braveCookiePaths(home));
254
+ const codex = existing(codexCookiePaths(home));
255
+ console.log(`Platform: ${process.platform} ${process.arch}`);
256
+ console.log(`Node: ${process.version}`);
257
+ console.log(`Configuration: ${status(configPath(home))}`);
258
+ const config = fs.existsSync(configPath(home)) ? readConfig(home) : null;
259
+ const source = config?.sourceBrowser || "brave";
260
+ const target = config?.targetBrowser || "codex";
261
+ console.log(`Selected source: ${source}`);
262
+ console.log(`Selected target: ${target}`);
263
+ console.log(`Source extension: ${status(installedExtensionDir(home, source))}`);
264
+ console.log(
265
+ target === "codex"
266
+ ? "Target integration: direct local Codex merge (Codex must be closed)"
267
+ : `Target extension: ${status(installedExtensionDir(home, target))}`,
268
+ );
269
+ console.log(`Daily schedule: ${status(launchAgentPath(home))}`);
270
+ console.log(`Sync at login: ${status(loginSyncLaunchAgentPath(home))}`);
271
+ console.log(`Open app at login: ${status(appLoginLaunchAgentPath(home))}`);
272
+ console.log(`Desktop app: ${status(installedAppPath(home))}`);
273
+ console.log(`Brave cookie stores detected: ${brave.length}`);
274
+ console.log(`Codex cookie stores detected: ${codex.length}`);
275
+ for (const file of brave) console.log(` Brave: ${file}`);
276
+ for (const file of codex) console.log(` Codex: ${file}`);
277
+ console.log("No cookie names, domains, values, or encryption keys were read.");
278
+ }
279
+
280
+ function enableLoginSync() {
281
+ assertMacOS();
282
+ const runtime = installRuntime();
283
+ if (!fs.existsSync(configPath())) installConfig({ hour: 9, minute: 0 });
284
+ const plist = installLoginSync({
285
+ cliPath: path.join(runtime, "bin", "brave-codex-cookie-sync.js"),
286
+ });
287
+ console.log(`Login sync enabled: ${plist}`);
288
+ const config = readConfig();
289
+ console.log(config.targetBrowser === "codex"
290
+ ? "A sync starts when you sign in and updates Codex only when Codex is closed."
291
+ : "A sync starts when you sign in and waits up to five minutes for both browser extensions.");
292
+ }
293
+
294
+ function disableLoginSync() {
295
+ console.log(removeLoginSync() ? "Login sync disabled." : "Login sync was not enabled.");
296
+ }
297
+
298
+ function setAppLogin(enabled) {
299
+ assertMacOS();
300
+ const existing = readConfig();
301
+ updatePreferences({
302
+ sourceBrowser: existing.sourceBrowser || "brave",
303
+ targetBrowser: existing.targetBrowser || "codex",
304
+ cookies: existing.imports?.cookies !== false,
305
+ history: existing.imports?.history === true,
306
+ menuBar: existing.ui?.menuBar === true,
307
+ openAtLogin: enabled,
308
+ autoCheckUpdates: existing.ui?.autoCheckUpdates !== false,
309
+ });
310
+ if (enabled) {
311
+ const appPath = installedAppPath();
312
+ if (!fs.existsSync(appPath)) throw new Error("Desktop app is not installed. Run install-app first.");
313
+ console.log(`App login enabled: ${installAppLogin({ appPath })}`);
314
+ } else {
315
+ removeAppLogin();
316
+ console.log("App login disabled.");
317
+ }
318
+ }
319
+
320
+ function remove() {
321
+ console.log(removeSchedule() ? "Daily schedule removed." : "No daily schedule was installed.");
322
+ }
323
+
324
+ function assertMacOS() {
325
+ if (process.platform !== "darwin") throw new Error("This release supports macOS only.");
326
+ }
327
+
328
+ function integerFlag(args, name, fallback, minimum, maximum) {
329
+ const index = args.indexOf(name);
330
+ if (index === -1) return fallback;
331
+ const value = Number(args[index + 1]);
332
+ if (!Number.isInteger(value) || value < minimum || value > maximum) {
333
+ throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`);
334
+ }
335
+ return value;
336
+ }
337
+
338
+ function booleanFlag(args, name, fallback) {
339
+ const value = stringFlag(args, name, fallback ? "on" : "off");
340
+ if (!["on", "off"].includes(value)) throw new Error(`${name} must be on or off`);
341
+ return value === "on";
342
+ }
343
+
344
+ function stringFlag(args, name, fallback) {
345
+ const index = args.indexOf(name);
346
+ if (index === -1) return fallback;
347
+ const value = args[index + 1];
348
+ if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
349
+ if (name === "--source" && !SOURCE_BROWSERS.includes(value)) {
350
+ throw new Error(`${name} must be one of: ${SOURCE_BROWSERS.join(", ")}`);
351
+ }
352
+ if (name === "--target" && !TARGET_BROWSERS.includes(value)) {
353
+ throw new Error(`${name} must be one of: ${TARGET_BROWSERS.join(", ")}`);
354
+ }
355
+ return value;
356
+ }
357
+
358
+ function existing(paths) {
359
+ return paths.filter((candidate) => fs.existsSync(candidate));
360
+ }
361
+
362
+ function status(candidate) {
363
+ return fs.existsSync(candidate) ? candidate : "not installed";
364
+ }
365
+
366
+ function pad(value) {
367
+ return String(value).padStart(2, "0");
368
+ }
@@ -0,0 +1,320 @@
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 { DatabaseSync } from "node:sqlite";
6
+ import { appSupportDir, codexCookiePaths } from "./paths.js";
7
+
8
+ const CHROMIUM_EPOCH_OFFSET_SECONDS = 11_644_473_600;
9
+ const CODEX_RUNNING_ERROR = "ChatGPT Codex is open. Quit it completely, then click Sync now again.";
10
+ const MAX_BACKUPS = 14;
11
+
12
+ export function isCodexRunning({ processList } = {}) {
13
+ const output = processList ?? spawnSync("/bin/ps", ["-axo", "command="], {
14
+ encoding: "utf8",
15
+ maxBuffer: 16 * 1024 * 1024,
16
+ }).stdout;
17
+ return String(output).split("\n").some((command) =>
18
+ /\/ChatGPT\.app\/Contents\/MacOS\/ChatGPT(?:\s|$)/.test(command.trim()),
19
+ );
20
+ }
21
+
22
+ export function directImportToCodex({
23
+ cookies,
24
+ history = [],
25
+ home = os.homedir(),
26
+ codexRunning = isCodexRunning(),
27
+ now = new Date(),
28
+ } = {}) {
29
+ if (!Array.isArray(cookies) || !Array.isArray(history)) {
30
+ throw new TypeError("cookies and history must be arrays");
31
+ }
32
+ if (codexRunning) throw new Error(CODEX_RUNNING_ERROR);
33
+
34
+ const cookiePath = codexCookiePaths(home).find((candidate) => fs.existsSync(candidate));
35
+ if (!cookiePath) {
36
+ throw new Error("Codex browser storage was not found. Open the Codex browser once, quit Codex, then try again.");
37
+ }
38
+ const historyPath = path.join(path.dirname(cookiePath), "History");
39
+ const backupRoot = path.join(appSupportDir(home), "backups", "codex");
40
+ const backupPath = path.join(backupRoot, backupDirectoryName(now));
41
+ fs.mkdirSync(backupPath, { recursive: true, mode: 0o700 });
42
+
43
+ const cookieBackup = path.join(backupPath, "Cookies");
44
+ try {
45
+ snapshotDatabase(cookiePath, cookieBackup);
46
+ } catch (error) {
47
+ fs.rmSync(backupPath, { recursive: true, force: true });
48
+ if (/locked|busy/i.test(error.message)) {
49
+ throw new Error("Codex is still releasing its browser database. Wait a few seconds after quitting Codex, then try again.");
50
+ }
51
+ throw error;
52
+ }
53
+ const cookieWorking = temporarySibling(cookiePath);
54
+ fs.copyFileSync(cookieBackup, cookieWorking);
55
+ fs.chmodSync(cookieWorking, 0o600);
56
+
57
+ let historyBackup = null;
58
+ let historyWorking = null;
59
+ if (history.length > 0) {
60
+ if (!fs.existsSync(historyPath)) {
61
+ fs.rmSync(cookieWorking, { force: true });
62
+ throw new Error("Codex history storage was not found. Open the Codex browser once, quit Codex, then try again.");
63
+ }
64
+ historyBackup = path.join(backupPath, "History");
65
+ try {
66
+ snapshotDatabase(historyPath, historyBackup);
67
+ } catch (error) {
68
+ fs.rmSync(cookieWorking, { force: true });
69
+ if (/locked|busy/i.test(error.message)) {
70
+ throw new Error("Codex is still releasing its history database. Wait a few seconds after quitting Codex, then try again.");
71
+ }
72
+ throw error;
73
+ }
74
+ historyWorking = temporarySibling(historyPath);
75
+ fs.copyFileSync(historyBackup, historyWorking);
76
+ fs.chmodSync(historyWorking, 0o600);
77
+ }
78
+
79
+ let cookieResult;
80
+ let historyResult = { imported: 0, skipped: 0, failed: 0 };
81
+ try {
82
+ cookieResult = mergeCookies(cookieWorking, cookies, now);
83
+ if (historyWorking) historyResult = mergeHistory(historyWorking, history, now);
84
+ replaceDatabase(cookieWorking, cookiePath);
85
+ cookieResult.replaced = true;
86
+ if (historyWorking) {
87
+ replaceDatabase(historyWorking, historyPath);
88
+ historyResult.replaced = true;
89
+ }
90
+ } catch (error) {
91
+ restoreDatabase(cookieBackup, cookiePath);
92
+ if (historyBackup) restoreDatabase(historyBackup, historyPath);
93
+ throw new Error(`Codex data was restored from backup after the sync failed: ${error.message}`);
94
+ } finally {
95
+ fs.rmSync(cookieWorking, { force: true });
96
+ if (historyWorking) fs.rmSync(historyWorking, { force: true });
97
+ }
98
+
99
+ pruneBackups(backupRoot, MAX_BACKUPS);
100
+ return {
101
+ imported: cookieResult.imported,
102
+ skipped: cookieResult.skipped,
103
+ failed: cookieResult.failed,
104
+ historyImported: historyResult.imported,
105
+ historySkipped: historyResult.skipped,
106
+ historyFailed: historyResult.failed,
107
+ backupPath,
108
+ targetPath: cookiePath,
109
+ directCodexImport: true,
110
+ };
111
+ }
112
+
113
+ function mergeCookies(databasePath, cookies, now) {
114
+ const database = new DatabaseSync(databasePath);
115
+ let imported = 0;
116
+ let skipped = 0;
117
+ let failed = 0;
118
+ try {
119
+ assertIntegrity(database);
120
+ assertTableColumns(database, "cookies", [
121
+ "creation_utc", "host_key", "top_frame_site_key", "name", "value",
122
+ "encrypted_value", "path", "expires_utc", "is_secure", "is_httponly",
123
+ "last_access_utc", "has_expires", "is_persistent", "priority", "samesite",
124
+ "source_scheme", "source_port", "last_update_utc", "source_type",
125
+ "has_cross_site_ancestor",
126
+ ]);
127
+ const insert = database.prepare(`
128
+ INSERT OR REPLACE INTO cookies(
129
+ creation_utc, host_key, top_frame_site_key, name, value, encrypted_value,
130
+ path, expires_utc, is_secure, is_httponly, last_access_utc, has_expires,
131
+ is_persistent, priority, samesite, source_scheme, source_port,
132
+ last_update_utc, source_type, has_cross_site_ancestor
133
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, 1, ?)
134
+ `);
135
+ const currentTime = chromiumTime(now.getTime() / 1000);
136
+ database.exec("BEGIN IMMEDIATE");
137
+ for (const cookie of cookies) {
138
+ try {
139
+ const normalized = normalizeCookie(cookie);
140
+ if (!normalized) {
141
+ skipped += 1;
142
+ continue;
143
+ }
144
+ insert.run(
145
+ currentTime,
146
+ normalized.domain,
147
+ normalized.topFrameSiteKey,
148
+ normalized.name,
149
+ normalized.value,
150
+ Buffer.alloc(0),
151
+ normalized.path,
152
+ normalized.expiresUtc,
153
+ normalized.secure ? 1 : 0,
154
+ normalized.httpOnly ? 1 : 0,
155
+ currentTime,
156
+ normalized.persistent ? 1 : 0,
157
+ normalized.persistent ? 1 : 0,
158
+ normalized.sameSite,
159
+ normalized.secure ? 2 : 1,
160
+ normalized.secure ? 443 : 80,
161
+ currentTime,
162
+ normalized.hasCrossSiteAncestor ? 1 : 0,
163
+ );
164
+ imported += 1;
165
+ } catch {
166
+ failed += 1;
167
+ }
168
+ }
169
+ database.exec("COMMIT");
170
+ assertIntegrity(database);
171
+ } catch (error) {
172
+ try { database.exec("ROLLBACK"); } catch {}
173
+ throw error;
174
+ } finally {
175
+ database.close();
176
+ }
177
+ return { imported, skipped, failed };
178
+ }
179
+
180
+ function mergeHistory(databasePath, history, now) {
181
+ const database = new DatabaseSync(databasePath);
182
+ let imported = 0;
183
+ let skipped = 0;
184
+ let failed = 0;
185
+ try {
186
+ assertIntegrity(database);
187
+ assertTableColumns(database, "urls", ["id", "url", "visit_count", "last_visit_time"]);
188
+ assertTableColumns(database, "visits", ["url", "visit_time", "transition"]);
189
+ const findURL = database.prepare("SELECT id FROM urls WHERE url = ? LIMIT 1");
190
+ const insertURL = database.prepare(
191
+ "INSERT INTO urls(url, title, visit_count, typed_count, last_visit_time, hidden) VALUES (?, '', 1, 0, ?, 0)",
192
+ );
193
+ const updateURL = database.prepare(
194
+ "UPDATE urls SET visit_count = visit_count + 1, last_visit_time = ? WHERE id = ?",
195
+ );
196
+ const insertVisit = database.prepare(
197
+ "INSERT INTO visits(url, visit_time, from_visit, transition, visit_duration) VALUES (?, ?, 0, 805306368, 0)",
198
+ );
199
+ const currentTime = chromiumTime(now.getTime() / 1000);
200
+ database.exec("BEGIN IMMEDIATE");
201
+ for (const item of history) {
202
+ try {
203
+ if (!item || typeof item.url !== "string" || !/^https?:\/\//.test(item.url)) {
204
+ skipped += 1;
205
+ continue;
206
+ }
207
+ const existing = findURL.get(item.url);
208
+ let urlID;
209
+ if (existing) {
210
+ urlID = existing.id;
211
+ updateURL.run(currentTime, urlID);
212
+ } else {
213
+ urlID = insertURL.run(item.url, currentTime).lastInsertRowid;
214
+ }
215
+ insertVisit.run(urlID, currentTime);
216
+ imported += 1;
217
+ } catch {
218
+ failed += 1;
219
+ }
220
+ }
221
+ database.exec("COMMIT");
222
+ assertIntegrity(database);
223
+ } catch (error) {
224
+ try { database.exec("ROLLBACK"); } catch {}
225
+ throw error;
226
+ } finally {
227
+ database.close();
228
+ }
229
+ return { imported, skipped, failed };
230
+ }
231
+
232
+ function snapshotDatabase(source, destination) {
233
+ fs.mkdirSync(path.dirname(destination), { recursive: true, mode: 0o700 });
234
+ const database = new DatabaseSync(source, { readOnly: true });
235
+ try {
236
+ assertIntegrity(database);
237
+ database.exec(`VACUUM INTO '${escapeSqlitePath(destination)}'`);
238
+ } finally {
239
+ database.close();
240
+ }
241
+ fs.chmodSync(destination, 0o600);
242
+ }
243
+
244
+ function replaceDatabase(source, target) {
245
+ fs.rmSync(`${target}-wal`, { force: true });
246
+ fs.rmSync(`${target}-shm`, { force: true });
247
+ fs.renameSync(source, target);
248
+ fs.chmodSync(target, 0o600);
249
+ }
250
+
251
+ function restoreDatabase(backup, target) {
252
+ if (!fs.existsSync(backup)) return;
253
+ fs.rmSync(`${target}-wal`, { force: true });
254
+ fs.rmSync(`${target}-shm`, { force: true });
255
+ fs.copyFileSync(backup, target);
256
+ fs.chmodSync(target, 0o600);
257
+ }
258
+
259
+ function assertIntegrity(database) {
260
+ const row = database.prepare("PRAGMA quick_check").get();
261
+ if (Object.values(row ?? {})[0] !== "ok") throw new Error("SQLite integrity check failed");
262
+ }
263
+
264
+ function assertTableColumns(database, table, required) {
265
+ const columns = new Set(database.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name));
266
+ const missing = required.filter((name) => !columns.has(name));
267
+ if (missing.length > 0) throw new Error(`Unsupported Codex ${table} schema; missing ${missing.join(", ")}`);
268
+ }
269
+
270
+ function normalizeCookie(cookie) {
271
+ if (!cookie || typeof cookie.name !== "string" || typeof cookie.value !== "string") return null;
272
+ if (typeof cookie.domain !== "string") return null;
273
+ const cleanHost = cookie.domain.replace(/^\./, "").trim().toLowerCase();
274
+ if (!cleanHost || cleanHost.includes("/") || cleanHost.includes(":")) return null;
275
+ const persistent = !cookie.session && Number.isFinite(cookie.expirationDate) && cookie.expirationDate > 0;
276
+ return {
277
+ name: cookie.name,
278
+ value: cookie.value,
279
+ domain: cookie.hostOnly ? cleanHost : `.${cleanHost}`,
280
+ topFrameSiteKey: typeof cookie.partitionKey?.topLevelSite === "string" ? cookie.partitionKey.topLevelSite : "",
281
+ hasCrossSiteAncestor: cookie.partitionKey?.hasCrossSiteAncestor ?? !cookie.partitionKey,
282
+ path: typeof cookie.path === "string" && cookie.path.startsWith("/") ? cookie.path : "/",
283
+ secure: Boolean(cookie.secure),
284
+ httpOnly: Boolean(cookie.httpOnly),
285
+ persistent,
286
+ expiresUtc: persistent ? chromiumTime(cookie.expirationDate) : 0,
287
+ sameSite: ({ unspecified: -1, no_restriction: 0, lax: 1, strict: 2 })[cookie.sameSite] ?? -1,
288
+ };
289
+ }
290
+
291
+ function pruneBackups(root, keep) {
292
+ if (!fs.existsSync(root)) return;
293
+ const directories = fs.readdirSync(root, { withFileTypes: true })
294
+ .filter((entry) => entry.isDirectory())
295
+ .map((entry) => entry.name)
296
+ .sort()
297
+ .reverse();
298
+ for (const name of directories.slice(keep)) {
299
+ fs.rmSync(path.join(root, name), { recursive: true, force: true });
300
+ }
301
+ }
302
+
303
+ function backupDirectoryName(now) {
304
+ const timestamp = now.toISOString().replace(/[:.]/g, "-");
305
+ return `${timestamp}-${process.pid}`;
306
+ }
307
+
308
+ function temporarySibling(target) {
309
+ return `${target}.browser-cookie-bridge-${process.pid}-${Date.now()}.tmp`;
310
+ }
311
+
312
+ function chromiumTime(unixSeconds) {
313
+ return Math.trunc((unixSeconds + CHROMIUM_EPOCH_OFFSET_SECONDS) * 1_000_000);
314
+ }
315
+
316
+ function escapeSqlitePath(target) {
317
+ return target.replaceAll("'", "''");
318
+ }
319
+
320
+ export { CODEX_RUNNING_ERROR };