From e131d9df5ef54311d0fa2fb1665cf1e2171416c1 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Tue, 8 Sep 2026 20:28:18 +0530 Subject: [PATCH 1/2] feat(desktop): import cookies from Zen Zen uses Firefox-compatible profiles and plaintext cookies.sqlite stores, so model it as a Firefox-engine source with native macOS, Windows, and Linux roots. Keep Firefox-specific Linux Snap handling scoped to Firefox so Zen results stay clean. --- .../src/preview/BrowserImport/Sources.test.ts | 139 ++++++++++++++++++ .../src/preview/BrowserImport/Sources.ts | 15 +- packages/contracts/src/browserImport.ts | 1 + 3 files changed, 154 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/preview/BrowserImport/Sources.test.ts b/apps/desktop/src/preview/BrowserImport/Sources.test.ts index a867f78497b4..c77a789bfc15 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.test.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.test.ts @@ -53,6 +53,7 @@ describe("Linux Chromium secret applications", () => { opera: "opera", helium: "chromium", firefox: undefined, + zen: undefined, }, ); }); @@ -672,6 +673,144 @@ describe("cookieDatabaseCandidatePaths", () => { }); const firefox = BROWSER_IMPORT_SOURCES.find((source) => source.id === "firefox")!; +const zen = BROWSER_IMPORT_SOURCES.find((source) => source.id === "zen")!; + +describe("Zen", () => { + it.effect("resolves Zen's native user-data roots on every supported platform", () => + run( + Effect.gen(function* () { + const darwinContext = yield* sourcePathContext.pipe( + Effect.provideService(HostProcessEnvironment, { HOME: "/Users/zen-user" }), + Effect.provideService(HostProcessPlatform, "darwin"), + ); + const windowsContext = yield* sourcePathContext.pipe( + Effect.provideService(HostProcessEnvironment, { + HOME: "C:\\Users\\zen-user", + APPDATA: "C:\\Users\\zen-user\\AppData\\Roaming", + }), + Effect.provideService(HostProcessPlatform, "win32"), + ); + const linuxContext = yield* sourcePathContext.pipe( + Effect.provideService(HostProcessEnvironment, { HOME: "/home/zen-user" }), + Effect.provideService(HostProcessPlatform, "linux"), + ); + + assert.equal(zen.name, "Zen"); + assert.equal(zen.engine, "firefox"); + assert.deepEqual([...zen.platforms], ["darwin", "win32", "linux"]); + assert.equal( + zen.userDataDirectory(darwinContext), + darwinContext.path.join("/Users/zen-user", "Library", "Application Support", "zen"), + ); + assert.equal( + zen.userDataDirectory(windowsContext), + windowsContext.path.join("C:\\Users\\zen-user\\AppData\\Roaming", "zen"), + ); + assert.equal( + zen.userDataDirectory(linuxContext), + linuxContext.path.join("/home/zen-user", ".config", "zen"), + ); + }), + ), + ); + + it.effect("discovers Zen profiles through its Firefox-compatible metadata", () => + run( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-zen-" }); + const context = yield* sourcePathContext.pipe( + Effect.provideService(HostProcessEnvironment, { HOME: home }), + Effect.provideService(HostProcessPlatform, "darwin"), + ); + const root = zen.userDataDirectory(context)!; + yield* fileSystem.makeDirectory(root, { recursive: true }); + const profile = context.path.join(root, "Profiles", "zen.default"); + yield* fileSystem.makeDirectory(profile, { recursive: true }); + yield* writeFirefoxCookieDatabase(context.path.join(profile, "cookies.sqlite"), 2, 1); + yield* fileSystem.makeDirectory(context.path.join(root, "Profiles", "empty.default"), { + recursive: true, + }); + yield* fileSystem.writeFileString( + context.path.join(root, "profiles.ini"), + [ + "[Profile0]", + "Name=Zen", + "IsRelative=1", + "Path=Profiles/zen.default", + "ZenAvatarPath=chrome://browser/content/zen-avatars/avatar-55.svg", + "Default=1", + "", + "[Profile1]", + "Name=Empty", + "IsRelative=1", + "Path=Profiles/empty.default", + "", + ].join("\n"), + ); + + assert.deepEqual(yield* listSourceProfiles(zen, context), [ + { + directory: context.path.join("Profiles", "zen.default"), + name: "Zen", + cookieCount: 2, + }, + ]); + assert.isTrue(yield* isSourceInstalled(zen, context)); + assert.isFalse(yield* isSourceRunning(zen, context)); + assert.equal( + yield* resolveCookieDatabase(zen, context, context.path.join("Profiles", "zen.default")), + context.path.join(profile, "cookies.sqlite"), + ); + }), + ), + ); + + it.effect("does not mistake Firefox Snap installs for Zen profiles", () => + run( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-zen-snap-" }); + const context = yield* sourcePathContext.pipe( + Effect.provideService(HostProcessEnvironment, { HOME: home }), + Effect.provideService(HostProcessPlatform, "linux"), + ); + const zenRoot = context.path.join(home, ".config", "zen"); + const snapRoot = context.path.join( + home, + "snap", + "firefox", + "common", + ".mozilla", + "firefox", + ); + const zenProfile = context.path.join(zenRoot, "zen.default"); + const snapProfile = context.path.join(snapRoot, "snap.default"); + for (const root of [zenRoot, snapRoot]) { + const profile = root === zenRoot ? zenProfile : snapProfile; + yield* fileSystem.makeDirectory(profile, { recursive: true }); + yield* writeFirefoxCookieDatabase(context.path.join(profile, "cookies.sqlite"), 1, 0); + yield* fileSystem.writeFileString( + context.path.join(root, "profiles.ini"), + [ + "[Profile0]", + "Name=Personal", + "IsRelative=1", + `Path=${profile === zenProfile ? "zen.default" : "snap.default"}`, + ].join("\n"), + ); + } + + const profiles = yield* listSourceProfiles(zen, context); + assert.deepEqual( + profiles.map((profile) => profile.directory), + ["zen.default"], + ); + assert.isTrue(yield* isSourceInstalled(zen, context)); + }), + ), + ); +}); describe("Firefox Snap profiles", () => { it.effect.skipIf(!symlinksSupported)( diff --git a/apps/desktop/src/preview/BrowserImport/Sources.ts b/apps/desktop/src/preview/BrowserImport/Sources.ts index 6075f0ad56a3..d84e5e9d3a52 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.ts @@ -213,6 +213,19 @@ export const BROWSER_IMPORT_SOURCES: ReadonlyArray { + if (context.platform === "darwin") return macApplicationSupport(context, "zen"); + if (context.platform === "win32") { + return context.appData ? context.path.join(context.appData, "zen") : undefined; + } + return context.path.join(context.home, ".config", "zen"); + }, + }, ]; /** @@ -579,7 +592,7 @@ export const listSourceProfiles = Effect.fn("BrowserImportSources.listSourceProf definition: BrowserImportSourceDefinition, context: BrowserImportPathContext, ): Effect.fn.Return, never, FileSystem.FileSystem> { - if (definition.engine !== "firefox" || context.platform !== "linux") { + if (definition.id !== "firefox" || context.platform !== "linux") { return yield* listSourceProfilesInDirectory(definition, context); } diff --git a/packages/contracts/src/browserImport.ts b/packages/contracts/src/browserImport.ts index e560bbadaed0..3e62cac460a8 100644 --- a/packages/contracts/src/browserImport.ts +++ b/packages/contracts/src/browserImport.ts @@ -25,6 +25,7 @@ const BROWSER_IMPORT_SOURCE_IDS = [ "arc", "helium", "firefox", + "zen", "safari", ] as const; From c61db9e4ceecc5d32681b3b9d5fdb7f8771f3f47 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Tue, 8 Sep 2026 22:31:45 +0530 Subject: [PATCH 2/2] fix(desktop): read Zen profiles from its legacy Linux directory --- .../src/preview/BrowserImport/Sources.test.ts | 43 +++++++++++++++++++ .../src/preview/BrowserImport/Sources.ts | 19 ++++---- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/preview/BrowserImport/Sources.test.ts b/apps/desktop/src/preview/BrowserImport/Sources.test.ts index c77a789bfc15..a49ad7475099 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.test.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.test.ts @@ -710,6 +710,9 @@ describe("Zen", () => { zen.userDataDirectory(linuxContext), linuxContext.path.join("/home/zen-user", ".config", "zen"), ); + assert.deepEqual(zen.alternateLinuxRoots?.(linuxContext), [ + linuxContext.path.join("/home/zen-user", ".zen"), + ]); }), ), ); @@ -766,6 +769,46 @@ describe("Zen", () => { ), ); + it.effect("discovers Zen profiles in both current and legacy Linux roots", () => + run( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-zen-linux-" }); + const context = yield* sourcePathContext.pipe( + Effect.provideService(HostProcessEnvironment, { HOME: home }), + Effect.provideService(HostProcessPlatform, "linux"), + ); + const currentRoot = context.path.join(home, ".config", "zen"); + const legacyRoot = context.path.join(home, ".zen"); + const currentProfile = context.path.join(currentRoot, "current.default"); + const legacyProfile = context.path.join(legacyRoot, "legacy.default"); + + for (const [root, profile, name] of [ + [currentRoot, currentProfile, "Current"], + [legacyRoot, legacyProfile, "Legacy"], + ] as const) { + yield* fileSystem.makeDirectory(profile, { recursive: true }); + yield* writeFirefoxCookieDatabase(context.path.join(profile, "cookies.sqlite"), 1, 0); + yield* fileSystem.writeFileString( + context.path.join(root, "profiles.ini"), + [ + "[Profile0]", + `Name=${name}`, + "IsRelative=1", + `Path=${context.path.basename(profile)}`, + ].join("\n"), + ); + } + + assert.deepEqual(yield* listSourceProfiles(zen, context), [ + { directory: "current.default", name: "Current", cookieCount: 1 }, + { directory: legacyProfile, name: "Legacy", cookieCount: 1 }, + ]); + assert.isTrue(yield* isSourceInstalled(zen, context)); + }), + ), + ); + it.effect("does not mistake Firefox Snap installs for Zen profiles", () => run( Effect.gen(function* () { diff --git a/apps/desktop/src/preview/BrowserImport/Sources.ts b/apps/desktop/src/preview/BrowserImport/Sources.ts index d84e5e9d3a52..04d777caf7c3 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.ts @@ -56,6 +56,8 @@ export interface BrowserImportSourceDefinition { /** Platforms the definition has paths for. */ readonly platforms: ReadonlyArray; readonly userDataDirectory: (context: BrowserImportPathContext) => string | undefined; + /** Parallel Linux roots for package variants or legacy layouts. */ + readonly alternateLinuxRoots?: (context: BrowserImportPathContext) => ReadonlyArray; /** Chromium on macOS only: where the OSCrypt key lives in the keychain. */ readonly keychainService?: string; readonly keychainAccount?: string; @@ -212,6 +214,9 @@ export const BROWSER_IMPORT_SOURCES: ReadonlyArray [ + context.path.join(context.home, "snap", "firefox", "common", ".mozilla", "firefox"), + ], }, { id: "zen", @@ -225,6 +230,7 @@ export const BROWSER_IMPORT_SOURCES: ReadonlyArray [context.path.join(context.home, ".zen")], }, ]; @@ -584,24 +590,21 @@ const listSourceProfilesInDirectory = Effect.fnUntraced(function* ( }); /** - * Include Firefox's Snap home alongside its native home. Snap profiles use - * absolute directories so cookie reads and lock checks keep pointing at the - * installation they came from, even when both installs use the same name. + * Include alternate Linux homes alongside the native home. Alternate profiles + * use absolute directories so cookie reads and lock checks keep pointing at + * the installation they came from, even when both installs use the same name. */ export const listSourceProfiles = Effect.fn("BrowserImportSources.listSourceProfiles")(function* ( definition: BrowserImportSourceDefinition, context: BrowserImportPathContext, ): Effect.fn.Return, never, FileSystem.FileSystem> { - if (definition.id !== "firefox" || context.platform !== "linux") { + if (definition.engine !== "firefox" || context.platform !== "linux") { return yield* listSourceProfilesInDirectory(definition, context); } const root = definition.userDataDirectory(context); if (root === undefined) return []; - const roots = [ - root, - context.path.join(context.home, "snap", "firefox", "common", ".mozilla", "firefox"), - ]; + const roots = [root, ...(definition.alternateLinuxRoots?.(context) ?? [])]; const profiles = new Map(); for (const directory of roots) { const found = yield* listSourceProfilesInDirectory(