Open History to see the last commit
1// @effect-diagnostics nodeBuiltinImport:off -- The Clerk bridge must lock the token file down before startup can yield to Electron's ready event.
2import * as NodeFS from "node:fs";
3import * as NodePath from "node:path";
4
5// electron-store chmods every write to 0666; the private directory protects tokens during writes.
6const CLERK_TOKEN_DIRECTORY_MODE = 0o700;
7const CLERK_TOKEN_FILE_MODE = 0o600;
8const CLERK_TOKEN_DIRECTORY_NAME = "clerk";
9const CLERK_TOKEN_FILE_NAME = "clerk-tokens.json";
10
11type Awaitable<T> = T | Promise<T>;
12
13export interface ClerkTokenStorage {
14 readonly getItem: (key: string) => Awaitable<string | null>;
15 readonly setItem: (key: string, value: string) => Awaitable<void>;
16 readonly removeItem: (key: string) => Awaitable<void>;
17}
18
19function isErrno(error: unknown, code: string): boolean {
20 return typeof error === "object" && error !== null && "code" in error && error.code === code;
21}
22
23function lstatIfExists(path: string): NodeFS.Stats | undefined {
24 try {
25 return NodeFS.lstatSync(path);
26 } catch (error) {
27 if (isErrno(error, "ENOENT")) return undefined;
28 throw error;
29 }
30}
31
32function assertRegularFile(path: string, stat: NodeFS.Stats): void {
33 if (stat.isSymbolicLink() || !stat.isFile()) {
34 throw new Error(`Clerk token file "${path}" must be a regular file.`);
35 }
36}
37
38function restrictClerkTokenFile(path: string): void {
39 const stat = lstatIfExists(path);
40 if (stat === undefined) return;
41 assertRegularFile(path, stat);
42 if ((stat.mode & 0o777) === CLERK_TOKEN_FILE_MODE) return;
43 NodeFS.chmodSync(path, CLERK_TOKEN_FILE_MODE);
44}
45
46function migrateLegacyClerkTokenFile(stateDir: string, directory: string): void {
47 const legacyPath = NodePath.join(stateDir, CLERK_TOKEN_FILE_NAME);
48 const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
49 const legacy = lstatIfExists(legacyPath);
50 if (legacy !== undefined) {
51 assertRegularFile(legacyPath, legacy);
52 NodeFS.chmodSync(legacyPath, CLERK_TOKEN_FILE_MODE);
53 }
54
55 const destination = lstatIfExists(tokenPath);
56 if (destination === undefined) {
57 if (legacy === undefined) return;
58 try {
59 NodeFS.copyFileSync(legacyPath, tokenPath, NodeFS.constants.COPYFILE_EXCL);
60 NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
61 NodeFS.unlinkSync(legacyPath);
62 } catch (error) {
63 if (!isErrno(error, "ENOENT")) throw error;
64 }
65 return;
66 }
67
68 assertRegularFile(tokenPath, destination);
69 NodeFS.chmodSync(tokenPath, CLERK_TOKEN_FILE_MODE);
70 if (legacy === undefined) return;
71 try {
72 NodeFS.unlinkSync(legacyPath);
73 } catch (error) {
74 if (!isErrno(error, "ENOENT")) throw error;
75 }
76}
77
78function restrictAfter<T>(run: () => Awaitable<T>, tighten: () => void): Awaitable<T> {
79 let result: Awaitable<T>;
80 try {
81 result = run();
82 } catch (error) {
83 tighten();
84 throw error;
85 }
86 if (result instanceof Promise) return result.finally(tighten);
87 tighten();
88 return result;
89}
90
91/** Opens Clerk token storage in a private directory and keeps the file at `0600`. */
92export function openClerkTokenStorage(
93 stateDir: string,
94 createStorage: (directory: string) => ClerkTokenStorage,
95 userDataPath = ".",
96): ClerkTokenStorage {
97 const absoluteStateDir = NodePath.resolve(stateDir);
98 const directory = NodePath.join(absoluteStateDir, CLERK_TOKEN_DIRECTORY_NAME);
99 NodeFS.mkdirSync(directory, { recursive: true, mode: CLERK_TOKEN_DIRECTORY_MODE });
100 const stat = NodeFS.lstatSync(directory);
101 if (stat.isSymbolicLink() || !stat.isDirectory()) {
102 throw new Error(`Clerk token directory "${directory}" must be a real directory.`);
103 }
104 NodeFS.chmodSync(directory, CLERK_TOKEN_DIRECTORY_MODE);
105 // electron-store rebases relative paths under Electron userData, not the working directory.
106 migrateLegacyClerkTokenFile(NodePath.resolve(userDataPath, stateDir), directory);
107 const tokenPath = NodePath.join(directory, CLERK_TOKEN_FILE_NAME);
108 restrictClerkTokenFile(tokenPath);
109
110 const storage = createStorage(directory);
111 const tighten = () => restrictClerkTokenFile(tokenPath);
112 return {
113 getItem: (key) => restrictAfter(() => storage.getItem(key), tighten),
114 setItem: (key, value) => restrictAfter(() => storage.setItem(key, value), tighten),
115 removeItem: (key) => restrictAfter(() => storage.removeItem(key), tighten),
116 };
117}