| 1 | /** |
| 2 | * Browser profiles - named identities for the in-app preview browser. |
| 3 | * |
| 4 | * Each profile maps to its own Electron session partition, so cookies and |
| 5 | * storage are isolated between them: a tab opened under "Work" cannot see |
| 6 | * "Personal"'s logins. Profiles are client-local, like the other browser |
| 7 | * defaults, because the Chromium guest they configure is desktop-local. |
| 8 | * |
| 9 | * Two profiles are built in and cannot be edited or removed: |
| 10 | * - `default` keeps the partition scope the browser used before profiles |
| 11 | * existed, so upgrading does not sign anyone out. |
| 12 | * - `incognito` maps to a non-persistent partition for throwaway sessions. |
| 13 | * |
| 14 | * @module BrowserProfile |
| 15 | */ |
| 16 | import * as Schema from "effect/Schema"; |
| 17 | import { TrimmedNonEmptyString } from "./baseSchemas.ts"; |
| 18 | |
| 19 | export const BROWSER_PROFILE_NAME_MAX_LENGTH = 48; |
| 20 | export const BROWSER_PROFILE_MAX_COUNT = 24; |
| 21 | |
| 22 | /** |
| 23 | * Control characters are rejected because ids are folded into delimiter-joined |
| 24 | * cache keys on the client; one carrying the delimiter would resolve to a |
| 25 | * different profile's partition. |
| 26 | */ |
| 27 | export const BrowserProfileId = TrimmedNonEmptyString.check( |
| 28 | Schema.isMaxLength(64), |
| 29 | Schema.isPattern(/^[^\p{Cc}]+$/u), |
| 30 | ); |
| 31 | export type BrowserProfileId = typeof BrowserProfileId.Type; |
| 32 | |
| 33 | export const BrowserProfileName = TrimmedNonEmptyString.check( |
| 34 | Schema.isMaxLength(BROWSER_PROFILE_NAME_MAX_LENGTH), |
| 35 | ); |
| 36 | |
| 37 | /** |
| 38 | * `persistent` profiles keep cookies on disk across restarts; `incognito` |
| 39 | * uses an in-memory partition that Chromium discards with the process. |
| 40 | */ |
| 41 | export const BrowserProfileKind = Schema.Literals(["persistent", "incognito"]); |
| 42 | export type BrowserProfileKind = typeof BrowserProfileKind.Type; |
| 43 | |
| 44 | export const BrowserProfile = Schema.Struct({ |
| 45 | id: BrowserProfileId, |
| 46 | name: BrowserProfileName, |
| 47 | kind: BrowserProfileKind, |
| 48 | }); |
| 49 | export type BrowserProfile = typeof BrowserProfile.Type; |
| 50 | |
| 51 | export const DEFAULT_BROWSER_PROFILE_ID: BrowserProfileId = "default"; |
| 52 | export const INCOGNITO_BROWSER_PROFILE_ID: BrowserProfileId = "incognito"; |
| 53 | |
| 54 | /** |
| 55 | * Built-ins are synthesized rather than stored, so they cannot be renamed out |
| 56 | * of existence or deleted by editing the settings file by hand. |
| 57 | */ |
| 58 | export const BUILT_IN_BROWSER_PROFILES: ReadonlyArray<BrowserProfile> = [ |
| 59 | { id: DEFAULT_BROWSER_PROFILE_ID, name: "Default", kind: "persistent" }, |
| 60 | { id: INCOGNITO_BROWSER_PROFILE_ID, name: "Incognito", kind: "incognito" }, |
| 61 | ]; |
| 62 | |
| 63 | export function isBuiltInBrowserProfileId(id: string): boolean { |
| 64 | return BUILT_IN_BROWSER_PROFILES.some((profile) => profile.id === id); |
| 65 | } |
| 66 | |
| 67 | /** |
| 68 | * The full picker list: built-ins first, then the user's own profiles. |
| 69 | * |
| 70 | * Three things are normalized away, because each would present a profile the |
| 71 | * partition layer does not actually deliver: |
| 72 | * |
| 73 | * - Entries colliding with a built-in id, so a hand-edited settings file |
| 74 | * cannot shadow "Default" or "Incognito". |
| 75 | * - Repeated ids, which map to one partition and would otherwise appear as |
| 76 | * two isolated identities sharing every cookie. First entry wins. |
| 77 | * - `kind: "incognito"` on anything but the built-in, since persistence is |
| 78 | * keyed off that one id; such a profile is labelled ephemeral while its |
| 79 | * cookies survive restarts. |
| 80 | */ |
| 81 | export function resolveBrowserProfiles( |
| 82 | userProfiles: ReadonlyArray<BrowserProfile>, |
| 83 | ): ReadonlyArray<BrowserProfile> { |
| 84 | const seen = new Set(BUILT_IN_BROWSER_PROFILES.map((profile) => profile.id)); |
| 85 | const resolved = [...BUILT_IN_BROWSER_PROFILES]; |
| 86 | for (const profile of userProfiles) { |
| 87 | if (seen.has(profile.id)) continue; |
| 88 | seen.add(profile.id); |
| 89 | resolved.push(profile.kind === "persistent" ? profile : { ...profile, kind: "persistent" }); |
| 90 | } |
| 91 | return resolved; |
| 92 | } |
| 93 | |
| 94 | export function findBrowserProfile( |
| 95 | profiles: ReadonlyArray<BrowserProfile>, |
| 96 | id: string | undefined, |
| 97 | ): BrowserProfile | undefined { |
| 98 | return id === undefined ? undefined : profiles.find((profile) => profile.id === id); |
| 99 | } |