1/**
2 * Shared pieces of cookie extraction: the shape both engines produce, and the
3 * snapshot every reader takes before touching a live database.
4 *
5 * @module CookieDatabase
6 */
7import * as Effect from "effect/Effect";
8import * as FileSystem from "effect/FileSystem";
9import * as Path from "effect/Path";
10import * as SqlClient from "effect/sql/SqlClient";
11
12import * as NodeSqliteClient from "@t3tools/shared/nodeSqliteClient";
13
14/** A cookie in the shape Electron's `session.cookies.set` accepts. */
15export interface ImportedCookie {
16 readonly url: string;
17 readonly name: string;
18 readonly value: string;
19 /**
20 * Set only for domain cookies, which the sources mark with a leading dot.
21 * A host-only cookie leaves this undefined: Electron treats any `domain` it
22 * is given as marking a domain cookie and re-adds the dot, which would widen
23 * the cookie to every subdomain of the host it was scoped to, and rejects
24 * `__Host-` cookies, which require it to be absent.
25 */
26 readonly domain: string | undefined;
27 readonly path: string;
28 readonly secure: boolean;
29 readonly httpOnly: boolean;
30 /** Seconds since the UNIX epoch, or undefined for a session cookie. */
31 readonly expirationDate: number | undefined;
32 readonly sameSite: "unspecified" | "no_restriction" | "lax" | "strict";
33}
34
35/**
36 * Cookies recovered from one database and rows that could not be decrypted.
37 * The skipped count reaches the user instead of disappearing from a partial
38 * import result.
39 */
40export interface CookieReadResult {
41 readonly cookies: ReadonlyArray<ImportedCookie>;
42 readonly undecryptable: number;
43 /** Distinct hosts of the rows that could not be decrypted. */
44 readonly undecryptableHosts: ReadonlyArray<string>;
45}
46
47/**
48 * The URL and domain Electron should register a stored row under.
49 *
50 * Both engines mark a domain cookie with a leading dot on the host. Electron
51 * matches on a URL, so the dot comes off for that; `domain` is passed through
52 * only for domain cookies, because supplying it at all makes Electron treat
53 * the cookie as one and re-add the dot — widening a host-only cookie to every
54 * subdomain of the host it was scoped to, and rejecting `__Host-` cookies,
55 * which require it to be absent.
56 */
57export const cookieScope = (
58 host: string,
59 path: string,
60 secure: boolean,
61): { readonly url: string; readonly domain: string | undefined } => {
62 const isDomainCookie = host.startsWith(".");
63 const unwrappedHost = bareHost(host);
64 const authority =
65 unwrappedHost.includes(":") && !(unwrappedHost.startsWith("[") && unwrappedHost.endsWith("]"))
66 ? `[${unwrappedHost}]`
67 : unwrappedHost;
68 return {
69 url: `${secure ? "https" : "http"}://${authority}${path}`,
70 domain: isDomainCookie ? host : undefined,
71 };
72};
73
74/** A host without the leading dot both engines put on a domain cookie, for display. */
75export const bareHost = (host: string): string => (host.startsWith(".") ? host.slice(1) : host);
76
77/**
78 * Creates a transactionally consistent snapshot of a cookie database in a
79 * temporary directory and returns the snapshot's path.
80 *
81 * Both engines keep the file open with WAL while the browser runs, so reading
82 * in place can observe a torn write. Copying also guarantees we never open the
83 * browser's own file for writing.
84 *
85 * Scoped: the temporary directory goes away when the caller's scope closes.
86 */
87export const snapshotCookieDatabase = Effect.fn("CookieDatabase.snapshotCookieDatabase")(function* (
88 cookiePath: string,
89 tempPrefix = "t3code-cookie-import-",
90) {
91 const fileSystem = yield* FileSystem.FileSystem;
92 const path = yield* Path.Path;
93 const directory = yield* fileSystem.makeTempDirectoryScoped({ prefix: tempPrefix });
94 const target = path.join(directory, path.basename(cookiePath));
95 yield* Effect.gen(function* () {
96 const sql = yield* SqlClient.SqlClient;
97 yield* sql`VACUUM INTO ${target}`;
98 }).pipe(Effect.provide(NodeSqliteClient.layer({ filename: cookiePath, readonly: true })));
99 return target;
100});