cloak22 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/output.ts ADDED
@@ -0,0 +1,21 @@
1
+ import chalk from "chalk";
2
+
3
+ export function formatHeading(text: string): string {
4
+ return chalk.bold.cyan(text);
5
+ }
6
+
7
+ export function formatSuccess(text: string): string {
8
+ return chalk.green(text);
9
+ }
10
+
11
+ export function formatWarning(text: string): string {
12
+ return chalk.yellow(text);
13
+ }
14
+
15
+ export function formatError(text: string): string {
16
+ return chalk.red(text);
17
+ }
18
+
19
+ export function formatInfo(text: string): string {
20
+ return chalk.cyan(text);
21
+ }
@@ -0,0 +1,320 @@
1
+ type StatementLike = {
2
+ run(...parameters: unknown[]): unknown;
3
+ get(...parameters: unknown[]): Record<string, unknown> | undefined;
4
+ all(...parameters: unknown[]): Array<Record<string, unknown>>;
5
+ };
6
+
7
+ type DatabaseLike = {
8
+ exec(sql: string): void;
9
+ prepare(sql: string): StatementLike;
10
+ close(): void;
11
+ };
12
+
13
+ type DatabaseConstructor = new (path: string) => DatabaseLike;
14
+
15
+ export type StoredRunCommand = {
16
+ headless: boolean;
17
+ profile?: string;
18
+ cookieUrls: string[];
19
+ cookieFile?: string;
20
+ };
21
+
22
+ export type DaemonState = StoredRunCommand & {
23
+ pid: number;
24
+ startedAt: string;
25
+ logPath: string;
26
+ };
27
+
28
+ function loadDatabaseConstructor(): DatabaseConstructor {
29
+ const sqlite = require("node:sqlite") as {
30
+ DatabaseSync: DatabaseConstructor;
31
+ };
32
+
33
+ return sqlite.DatabaseSync;
34
+ }
35
+
36
+ function openDatabase(dbPath: string): DatabaseLike {
37
+ const DatabaseSync = loadDatabaseConstructor();
38
+ return new DatabaseSync(dbPath);
39
+ }
40
+
41
+ function normalizeCookieUrls(urls: string[]): string[] {
42
+ return [...new Set(urls)].sort((left, right) => left.localeCompare(right));
43
+ }
44
+
45
+ function initializeSchema(database: DatabaseLike) {
46
+ database.exec(`
47
+ CREATE TABLE IF NOT EXISTS settings (
48
+ key TEXT PRIMARY KEY,
49
+ value TEXT NOT NULL
50
+ );
51
+
52
+ CREATE TABLE IF NOT EXISTS profile_cookie_urls (
53
+ profile TEXT NOT NULL,
54
+ url TEXT NOT NULL,
55
+ PRIMARY KEY (profile, url)
56
+ );
57
+
58
+ CREATE TABLE IF NOT EXISTS daemon_state (
59
+ slot INTEGER PRIMARY KEY CHECK (slot = 1),
60
+ pid INTEGER NOT NULL,
61
+ profile TEXT,
62
+ cookie_urls TEXT NOT NULL,
63
+ cookie_file TEXT,
64
+ headless INTEGER NOT NULL,
65
+ started_at TEXT NOT NULL,
66
+ log_path TEXT NOT NULL
67
+ );
68
+ `);
69
+
70
+ const daemonStateColumns = database
71
+ .prepare("PRAGMA table_info(daemon_state)")
72
+ .all()
73
+ .map((row) => row.name)
74
+ .filter((value): value is string => typeof value === "string");
75
+
76
+ if (!daemonStateColumns.includes("cookie_file")) {
77
+ database.exec("ALTER TABLE daemon_state ADD COLUMN cookie_file TEXT;");
78
+ }
79
+ }
80
+
81
+ function readJson<T>(value: string | undefined): T | undefined {
82
+ if (!value) {
83
+ return undefined;
84
+ }
85
+
86
+ try {
87
+ return JSON.parse(value) as T;
88
+ } catch {
89
+ return undefined;
90
+ }
91
+ }
92
+
93
+ export class CloakStateDb {
94
+ constructor(private readonly dbPath: string) {}
95
+
96
+ private withDatabase<T>(operation: (database: DatabaseLike) => T): T {
97
+ const database = openDatabase(this.dbPath);
98
+ initializeSchema(database);
99
+
100
+ try {
101
+ return operation(database);
102
+ } finally {
103
+ database.close();
104
+ }
105
+ }
106
+
107
+ getDefaultProfile(): string | undefined {
108
+ return this.withDatabase((database) => {
109
+ const row = database
110
+ .prepare("SELECT value FROM settings WHERE key = ?")
111
+ .get("default_profile");
112
+
113
+ return typeof row?.value === "string" ? row.value : undefined;
114
+ });
115
+ }
116
+
117
+ setDefaultProfile(profile: string) {
118
+ this.withDatabase((database) => {
119
+ database
120
+ .prepare(
121
+ [
122
+ "INSERT INTO settings (key, value)",
123
+ "VALUES (?, ?)",
124
+ "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
125
+ ].join(" ")
126
+ )
127
+ .run("default_profile", profile);
128
+ });
129
+ }
130
+
131
+ getRememberedCookieUrls(profile: string): string[] {
132
+ return this.withDatabase((database) => {
133
+ const rows = database
134
+ .prepare(
135
+ [
136
+ "SELECT url",
137
+ "FROM profile_cookie_urls",
138
+ "WHERE profile = ?",
139
+ "ORDER BY url COLLATE NOCASE",
140
+ ].join(" ")
141
+ )
142
+ .all(profile);
143
+
144
+ return rows
145
+ .map((row) => row.url)
146
+ .filter((value): value is string => typeof value === "string");
147
+ });
148
+ }
149
+
150
+ rememberCookieUrls(profile: string, urls: string[]): string[] {
151
+ const normalized = normalizeCookieUrls(urls);
152
+
153
+ this.withDatabase((database) => {
154
+ const statement = database.prepare(
155
+ [
156
+ "INSERT INTO profile_cookie_urls (profile, url)",
157
+ "VALUES (?, ?)",
158
+ "ON CONFLICT(profile, url) DO NOTHING",
159
+ ].join(" ")
160
+ );
161
+
162
+ for (const url of normalized) {
163
+ statement.run(profile, url);
164
+ }
165
+ });
166
+
167
+ return this.getRememberedCookieUrls(profile);
168
+ }
169
+
170
+ replaceRememberedCookieUrls(profile: string, urls: string[]): string[] {
171
+ const normalized = normalizeCookieUrls(urls);
172
+
173
+ this.withDatabase((database) => {
174
+ database
175
+ .prepare("DELETE FROM profile_cookie_urls WHERE profile = ?")
176
+ .run(profile);
177
+
178
+ const insert = database.prepare(
179
+ "INSERT INTO profile_cookie_urls (profile, url) VALUES (?, ?)"
180
+ );
181
+
182
+ for (const url of normalized) {
183
+ insert.run(profile, url);
184
+ }
185
+ });
186
+
187
+ return normalized;
188
+ }
189
+
190
+ getDaemonState(): DaemonState | undefined {
191
+ return this.withDatabase((database) => {
192
+ const row = database
193
+ .prepare(
194
+ [
195
+ "SELECT pid, profile, cookie_urls, cookie_file, headless, started_at, log_path",
196
+ "FROM daemon_state",
197
+ "WHERE slot = 1",
198
+ ].join(" ")
199
+ )
200
+ .get();
201
+
202
+ if (!row) {
203
+ return undefined;
204
+ }
205
+
206
+ const cookieUrls = readJson<string[]>(
207
+ typeof row.cookie_urls === "string" ? row.cookie_urls : undefined
208
+ );
209
+
210
+ if (typeof row.pid !== "number" || !Array.isArray(cookieUrls)) {
211
+ return undefined;
212
+ }
213
+
214
+ return {
215
+ pid: row.pid,
216
+ profile: typeof row.profile === "string" ? row.profile : undefined,
217
+ cookieUrls,
218
+ cookieFile:
219
+ typeof row.cookie_file === "string" ? row.cookie_file : undefined,
220
+ headless: row.headless === 1,
221
+ startedAt:
222
+ typeof row.started_at === "string"
223
+ ? row.started_at
224
+ : new Date(0).toISOString(),
225
+ logPath: typeof row.log_path === "string" ? row.log_path : "",
226
+ };
227
+ });
228
+ }
229
+
230
+ setDaemonState(state: DaemonState) {
231
+ this.withDatabase((database) => {
232
+ database
233
+ .prepare(
234
+ [
235
+ "INSERT INTO daemon_state",
236
+ "(slot, pid, profile, cookie_urls, cookie_file, headless, started_at, log_path)",
237
+ "VALUES (1, ?, ?, ?, ?, ?, ?, ?)",
238
+ "ON CONFLICT(slot) DO UPDATE SET",
239
+ "pid = excluded.pid,",
240
+ "profile = excluded.profile,",
241
+ "cookie_urls = excluded.cookie_urls,",
242
+ "cookie_file = excluded.cookie_file,",
243
+ "headless = excluded.headless,",
244
+ "started_at = excluded.started_at,",
245
+ "log_path = excluded.log_path",
246
+ ].join(" ")
247
+ )
248
+ .run(
249
+ state.pid,
250
+ state.profile ?? null,
251
+ JSON.stringify(normalizeCookieUrls(state.cookieUrls)),
252
+ state.cookieFile ?? null,
253
+ state.headless ? 1 : 0,
254
+ state.startedAt,
255
+ state.logPath
256
+ );
257
+ });
258
+ }
259
+
260
+ clearDaemonState(pid?: number) {
261
+ this.withDatabase((database) => {
262
+ if (typeof pid === "number") {
263
+ database
264
+ .prepare("DELETE FROM daemon_state WHERE slot = 1 AND pid = ?")
265
+ .run(pid);
266
+ return;
267
+ }
268
+
269
+ database.prepare("DELETE FROM daemon_state WHERE slot = 1").run();
270
+ });
271
+ }
272
+
273
+ getLastDaemonCommand(): StoredRunCommand | undefined {
274
+ return this.withDatabase((database) => {
275
+ const row = database
276
+ .prepare("SELECT value FROM settings WHERE key = ?")
277
+ .get("last_daemon_command");
278
+ const command = readJson<StoredRunCommand>(
279
+ typeof row?.value === "string" ? row.value : undefined
280
+ );
281
+
282
+ if (
283
+ !command ||
284
+ !Array.isArray(command.cookieUrls) ||
285
+ (command.cookieFile !== undefined &&
286
+ typeof command.cookieFile !== "string")
287
+ ) {
288
+ return undefined;
289
+ }
290
+
291
+ return {
292
+ headless: Boolean(command.headless),
293
+ profile: command.profile,
294
+ cookieUrls: normalizeCookieUrls(command.cookieUrls),
295
+ cookieFile: command.cookieFile,
296
+ };
297
+ });
298
+ }
299
+
300
+ setLastDaemonCommand(command: StoredRunCommand) {
301
+ const normalized = {
302
+ headless: Boolean(command.headless),
303
+ profile: command.profile,
304
+ cookieUrls: normalizeCookieUrls(command.cookieUrls),
305
+ cookieFile: command.cookieFile,
306
+ };
307
+
308
+ this.withDatabase((database) => {
309
+ database
310
+ .prepare(
311
+ [
312
+ "INSERT INTO settings (key, value)",
313
+ "VALUES (?, ?)",
314
+ "ON CONFLICT(key) DO UPDATE SET value = excluded.value",
315
+ ].join(" ")
316
+ )
317
+ .run("last_daemon_command", JSON.stringify(normalized));
318
+ });
319
+ }
320
+ }