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 */
16import * as Schema from "effect/Schema";
17import { TrimmedNonEmptyString } from "./baseSchemas.ts";
18
19export const BROWSER_PROFILE_NAME_MAX_LENGTH = 48;
20export 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 */
27export const BrowserProfileId = TrimmedNonEmptyString.check(
28 Schema.isMaxLength(64),
29 Schema.isPattern(/^[^\p{Cc}]+$/u),
30);
31export type BrowserProfileId = typeof BrowserProfileId.Type;
32
33export 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 */
41export const BrowserProfileKind = Schema.Literals(["persistent", "incognito"]);
42export type BrowserProfileKind = typeof BrowserProfileKind.Type;
43
44export const BrowserProfile = Schema.Struct({
45 id: BrowserProfileId,
46 name: BrowserProfileName,
47 kind: BrowserProfileKind,
48});
49export type BrowserProfile = typeof BrowserProfile.Type;
50
51export const DEFAULT_BROWSER_PROFILE_ID: BrowserProfileId = "default";
52export 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 */
58export 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
63export 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 */
81export 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
94export 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}