From e5b82d78a1882bba5b19ee2b25bc8d16516a1b49 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Wed, 21 Oct 2026 21:17:24 +0530 Subject: [PATCH 1/2] feat(desktop): import cookies from Zen Browser --- .../src/preview/BrowserImport/Sources.test.ts | 200 ++++++++++++++++++ .../src/preview/BrowserImport/Sources.ts | 25 ++- docs/user/browser-import.md | 2 +- packages/contracts/src/browserImport.ts | 1 + 4 files changed, 222 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/preview/BrowserImport/Sources.test.ts b/apps/desktop/src/preview/BrowserImport/Sources.test.ts index cc92563c5847c..7d5c18f4e37e5 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.test.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.test.ts @@ -326,6 +326,206 @@ 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(HostProcess.Environment, { HOME: "/Users/zen-user" }), + Effect.provideService(HostProcess.Platform, "darwin"), + ); + const windowsContext = yield* sourcePathContext.pipe( + Effect.provideService(HostProcess.Environment, { + HOME: "C:\\Users\\zen-user", + APPDATA: "C:\\Users\\zen-user\\AppData\\Roaming", + }), + Effect.provideService(HostProcess.Platform, "win32"), + ); + const linuxContext = yield* sourcePathContext.pipe( + Effect.provideService(HostProcess.Environment, { HOME: "/home/zen-user" }), + Effect.provideService(HostProcess.Platform, "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"), + ); + assert.deepEqual(zen.alternateLinuxRoots?.(linuxContext), [ + linuxContext.path.join("/home/zen-user", ".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(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "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("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(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "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)); + assert.equal( + yield* resolveCookieDatabase(zen, context, legacyProfile), + context.path.join(legacyProfile, "cookies.sqlite"), + ); + + yield* fileSystem.writeFileString( + context.path.join(currentRoot, "profiles.ini"), + `[Profile0]\nName=Shared\nIsRelative=0\nPath=${legacyProfile}\n`, + ); + assert.deepEqual(yield* listSourceProfiles(zen, context), [ + { directory: legacyProfile, name: "Shared", cookieCount: 1 }, + ]); + + yield* fileSystem.remove(currentRoot, { recursive: true }); + assert.isTrue(yield* isSourceInstalled(zen, context)); + assert.deepEqual(yield* listSourceProfiles(zen, context), [ + { directory: legacyProfile, name: "Legacy", cookieCount: 1 }, + ]); + }), + ), + ); + + 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(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "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)( "finds Snap profiles with or without profiles.ini and checks their locks", diff --git a/apps/desktop/src/preview/BrowserImport/Sources.ts b/apps/desktop/src/preview/BrowserImport/Sources.ts index 3e1cae508a69e..db34c571f51cc 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.ts @@ -26,6 +26,7 @@ export interface BrowserImportSourceDefinition { readonly engine: BrowserImportEngine; readonly platforms: ReadonlyArray; readonly userDataDirectory: (context: BrowserImportPathContext) => string | undefined; + readonly alternateLinuxRoots?: (context: BrowserImportPathContext) => ReadonlyArray; readonly keychainService?: string; readonly keychainAccount?: string; readonly linuxSecretApplication?: string; @@ -171,6 +172,23 @@ export const BROWSER_IMPORT_SOURCES: ReadonlyArray [ + context.path.join(context.home, "snap", "firefox", "common", ".mozilla", "firefox"), + ], + }, + { + id: "zen", + name: "Zen", + engine: "firefox", + platforms: ["darwin", "win32", "linux"], + userDataDirectory: (context) => { + 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"); + }, + alternateLinuxRoots: (context) => [context.path.join(context.home, ".zen")], }, ]; @@ -491,7 +509,7 @@ const listSourceProfilesInDirectory = Effect.fnUntraced(function* ( }); /** - * Include Firefox's Snap home alongside its native home. Snap profiles use + * 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. */ @@ -505,10 +523,7 @@ export const listSourceProfiles = Effect.fn("BrowserImportSources.listSourceProf 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( diff --git a/docs/user/browser-import.md b/docs/user/browser-import.md index 36581c2c7ed27..ab1465e00bfda 100644 --- a/docs/user/browser-import.md +++ b/docs/user/browser-import.md @@ -15,6 +15,6 @@ System Settings permission list, and turn access on. **Continue** becomes availa is detected. macOS may require you to quit and reopen T3 Code before the grant applies; reopen the import wizard afterward. You can revoke Full Disk Access once the import is done. -On Windows, import supports Firefox and Helium profiles that use standard profile encryption. +On Windows, import supports Firefox, Zen, and Helium profiles that use standard profile encryption. Other Chromium-based browsers use app-bound encryption and cannot be imported. Partitioned cookies are skipped on all platforms. diff --git a/packages/contracts/src/browserImport.ts b/packages/contracts/src/browserImport.ts index f355e110c69d8..71feceae708f7 100644 --- a/packages/contracts/src/browserImport.ts +++ b/packages/contracts/src/browserImport.ts @@ -11,6 +11,7 @@ const BROWSER_IMPORT_SOURCE_IDS = [ "arc", "helium", "firefox", + "zen", "safari", ] as const; From 126fbe86a0e1e514a4ae5e753bf950acc8f1ed86 Mon Sep 17 00:00:00 2001 From: UtkarshUsername Date: Sun, 25 Oct 2026 18:18:52 +0530 Subject: [PATCH 2/2] fix(desktop): discover Zen Flatpak profiles --- .../src/preview/BrowserImport/Sources.test.ts | 31 +++++++++++++++++++ .../src/preview/BrowserImport/Sources.ts | 5 ++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/preview/BrowserImport/Sources.test.ts b/apps/desktop/src/preview/BrowserImport/Sources.test.ts index 5cdd95bad23f8..898d94221b8d4 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.test.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.test.ts @@ -357,6 +357,7 @@ describe("Zen", () => { ); assert.deepEqual(zen.alternateLinuxRoots?.(linuxContext), [ linuxContext.path.join("/home/zen-user", ".zen"), + linuxContext.path.join("/home/zen-user", ".var", "app", "app.zen_browser.zen", "zen"), ]); }), ), @@ -472,6 +473,36 @@ describe("Zen", () => { ), ); + it.effect("discovers a Flatpak-only Zen installation and resolves its cookies", () => + run( + Effect.gen(function* () { + const fileSystem = yield* FileSystem.FileSystem; + const home = yield* fileSystem.makeTempDirectoryScoped({ prefix: "t3code-zen-flatpak-" }); + const context = yield* sourcePathContext.pipe( + Effect.provideService(HostProcess.Environment, { HOME: home }), + Effect.provideService(HostProcess.Platform, "linux"), + ); + const root = context.path.join(home, ".var", "app", "app.zen_browser.zen", "zen"); + const profile = context.path.join(root, "flatpak.default"); + yield* fileSystem.makeDirectory(profile, { recursive: true }); + yield* writeFirefoxCookieDatabase(context.path.join(profile, "cookies.sqlite"), 2, 1); + yield* fileSystem.writeFileString( + context.path.join(root, "profiles.ini"), + "[Profile0]\nName=Flatpak\nIsRelative=1\nPath=flatpak.default\n", + ); + + assert.deepEqual(yield* listSourceProfiles(zen, context), [ + { directory: profile, name: "Flatpak", cookieCount: 2 }, + ]); + assert.isTrue(yield* isSourceInstalled(zen, context)); + assert.equal( + yield* resolveCookieDatabase(zen, context, profile), + context.path.join(profile, "cookies.sqlite"), + ); + }), + ), + ); + 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 c46c032d15874..14c4bca77b7e6 100644 --- a/apps/desktop/src/preview/BrowserImport/Sources.ts +++ b/apps/desktop/src/preview/BrowserImport/Sources.ts @@ -186,7 +186,10 @@ export const BROWSER_IMPORT_SOURCES: ReadonlyArray [context.path.join(context.home, ".zen")], + alternateLinuxRoots: (context) => [ + context.path.join(context.home, ".zen"), + context.path.join(context.home, ".var", "app", "app.zen_browser.zen", "zen"), + ], }, ];