pi-oracle 0.7.4 → 0.7.6

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +53 -18
  3. package/docs/ORACLE_DESIGN.md +16 -8
  4. package/docs/platform-smoke.md +156 -0
  5. package/extensions/oracle/index.ts +10 -4
  6. package/extensions/oracle/lib/config.ts +53 -27
  7. package/extensions/oracle/lib/jobs.ts +9 -5
  8. package/extensions/oracle/lib/poller.ts +1 -0
  9. package/extensions/oracle/lib/runtime.ts +107 -32
  10. package/extensions/oracle/lib/tools.ts +138 -12
  11. package/extensions/oracle/shared/browser-profile-helpers.d.mts +59 -0
  12. package/extensions/oracle/shared/browser-profile-helpers.mjs +395 -0
  13. package/extensions/oracle/shared/process-helpers.mjs +12 -1
  14. package/extensions/oracle/shared/state-coordination-helpers.mjs +8 -2
  15. package/extensions/oracle/worker/auth-bootstrap.mjs +39 -10
  16. package/extensions/oracle/worker/chatgpt-ui-helpers.d.mts +2 -0
  17. package/extensions/oracle/worker/chatgpt-ui-helpers.mjs +157 -1
  18. package/extensions/oracle/worker/chromium-cookie-source.mjs +2 -1
  19. package/extensions/oracle/worker/run-job.mjs +107 -25
  20. package/package.json +30 -9
  21. package/platform-smoke.config.mjs +66 -0
  22. package/scripts/oracle-real-smoke.mjs +500 -0
  23. package/scripts/platform-smoke/Dockerfile.ubuntu +8 -0
  24. package/scripts/platform-smoke/artifacts.mjs +87 -0
  25. package/scripts/platform-smoke/assertions.mjs +34 -0
  26. package/scripts/platform-smoke/crabbox-runner.mjs +135 -0
  27. package/scripts/platform-smoke/doctor.mjs +239 -0
  28. package/scripts/platform-smoke/invariants.mjs +124 -0
  29. package/scripts/platform-smoke/platform-build-windows.ps1 +168 -0
  30. package/scripts/platform-smoke/targets.mjs +434 -0
  31. package/scripts/platform-smoke.mjs +152 -0
@@ -4,9 +4,13 @@
4
4
  // Usage: Imported by the oracle extension entrypoint and sanity tests to register tools against the pi API.
5
5
  // Invariants/Assumptions: The pi runtime validates TypeBox schemas before execute, while execute owns semantic normalization.
6
6
  import { randomUUID } from "node:crypto";
7
- import { lstat, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
7
+ import { spawn } from "node:child_process";
8
+ import { once } from "node:events";
9
+ import { createReadStream } from "node:fs";
10
+ import { lstat, mkdtemp, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
8
11
  import { tmpdir } from "node:os";
9
- import { basename, join, posix } from "node:path";
12
+ import { basename, dirname, join, posix } from "node:path";
13
+ import { sweetCookieSafeStoragePasswordScrubbedEnv } from "../shared/browser-profile-helpers.mjs";
10
14
  import { runOracleAuthBootstrap } from "./auth.js";
11
15
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
12
16
  import { Type } from "typebox";
@@ -125,6 +129,8 @@ const DEFAULT_ARCHIVE_EXCLUDED_DIR_NAMES_ANYWHERE = new Set([
125
129
  ".pi",
126
130
  ".oracle-context",
127
131
  ".cursor",
132
+ ".artifacts",
133
+ ".crabbox",
128
134
  "node_modules",
129
135
  "target",
130
136
  ".venv",
@@ -386,6 +392,99 @@ function formatArchiveOversizeError(args: {
386
392
  .join("\n");
387
393
  }
388
394
 
395
+ function writeOctal(value: number, width: number): Buffer {
396
+ const text = Math.max(0, Math.floor(value)).toString(8).slice(-(width - 1)).padStart(width - 1, "0") + "\0";
397
+ return Buffer.from(text, "ascii");
398
+ }
399
+
400
+ function writeTarName(header: Buffer, name: string): void {
401
+ const normalized = name.replaceAll("\\", "/");
402
+ const nameBytes = Buffer.byteLength(normalized);
403
+ if (nameBytes <= 100) {
404
+ header.write(normalized, 0, 100, "utf8");
405
+ return;
406
+ }
407
+ const parts = normalized.split("/");
408
+ const fileName = parts.pop() || "";
409
+ const prefix = parts.join("/");
410
+ if (Buffer.byteLength(fileName) > 100 || Buffer.byteLength(prefix) > 155) {
411
+ throw new Error(`archive path is too long for portable tar header: ${normalized}`);
412
+ }
413
+ header.write(fileName, 0, 100, "utf8");
414
+ header.write(prefix, 345, 155, "utf8");
415
+ }
416
+
417
+ function buildTarHeader(name: string, options: { mode: number; size: number; mtimeMs: number; type: "file" | "directory" | "symlink"; linkName?: string }): Buffer {
418
+ const header = Buffer.alloc(512);
419
+ writeTarName(header, options.type === "directory" && !name.endsWith("/") ? `${name}/` : name);
420
+ writeOctal(options.mode & 0o7777, 8).copy(header, 100);
421
+ writeOctal(0, 8).copy(header, 108);
422
+ writeOctal(0, 8).copy(header, 116);
423
+ writeOctal(options.size, 12).copy(header, 124);
424
+ writeOctal(Math.floor(options.mtimeMs / 1000), 12).copy(header, 136);
425
+ Buffer.from(" ", "ascii").copy(header, 148);
426
+ header[156] = options.type === "directory" ? 53 : options.type === "symlink" ? 50 : 48;
427
+ if (options.linkName) header.write(options.linkName.replaceAll("\\", "/"), 157, 100, "utf8");
428
+ header.write("ustar", 257, 6, "ascii");
429
+ header.write("00", 263, 2, "ascii");
430
+ let checksum = 0;
431
+ for (const byte of header) checksum += byte;
432
+ const checksumText = checksum.toString(8).padStart(6, "0");
433
+ header.write(`${checksumText}\0 `, 148, 8, "ascii");
434
+ return header;
435
+ }
436
+
437
+ async function writeChunk(stream: NodeJS.WritableStream, chunk: Buffer): Promise<void> {
438
+ if (!stream.write(chunk)) await once(stream, "drain");
439
+ }
440
+
441
+ async function writeWindowsTarArchiveToZstd(cwd: string, entries: string[], archivePath: string, timeout: AbortSignal): Promise<void> {
442
+ const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv(process.env);
443
+ const zstd = spawn("zstd", ["-19", "-T0", "-f", "-o", archivePath], {
444
+ cwd,
445
+ env: scrubbedEnv,
446
+ stdio: ["pipe", "ignore", "pipe"],
447
+ signal: timeout,
448
+ });
449
+ let stderr = "";
450
+ zstd.stderr?.on("data", (chunk) => {
451
+ stderr += String(chunk);
452
+ });
453
+ try {
454
+ for (const entry of entries) {
455
+ const normalizedEntry = entry.replaceAll("\\", "/");
456
+ const absolutePath = join(cwd, normalizedEntry);
457
+ const info = await lstat(absolutePath);
458
+ if (info.isSymbolicLink()) {
459
+ await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "symlink", linkName: await readlink(absolutePath) }));
460
+ continue;
461
+ }
462
+ if (info.isDirectory()) {
463
+ await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: 0, mtimeMs: info.mtimeMs, type: "directory" }));
464
+ continue;
465
+ }
466
+ if (!info.isFile()) continue;
467
+ await writeChunk(zstd.stdin, buildTarHeader(normalizedEntry, { mode: info.mode, size: info.size, mtimeMs: info.mtimeMs, type: "file" }));
468
+ for await (const chunk of createReadStream(absolutePath)) {
469
+ await writeChunk(zstd.stdin, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
470
+ }
471
+ const padding = info.size % 512 === 0 ? 0 : 512 - (info.size % 512);
472
+ if (padding > 0) await writeChunk(zstd.stdin, Buffer.alloc(padding));
473
+ }
474
+ await writeChunk(zstd.stdin, Buffer.alloc(1024));
475
+ zstd.stdin.end();
476
+ } catch (error) {
477
+ zstd.stdin.destroy();
478
+ zstd.kill();
479
+ throw error;
480
+ }
481
+ const code = await new Promise<number | null>((resolve, reject) => {
482
+ zstd.once("error", reject);
483
+ zstd.once("close", resolve);
484
+ });
485
+ if (code !== 0) throw new Error(`zstd archive compression failed with status ${code}: ${stderr.trim()}`);
486
+ }
487
+
389
488
  async function writeArchiveFile(
390
489
  cwd: string,
391
490
  entries: string[],
@@ -396,13 +495,32 @@ async function writeArchiveFile(
396
495
  await writeFile(listPath, Buffer.from(`${entries.join("\0")}\0`), { mode: 0o600 });
397
496
  await rm(archivePath, { force: true }).catch(() => undefined);
398
497
 
399
- const { spawn } = await import("node:child_process");
498
+ if (process.platform === "win32") {
499
+ const timeoutController = new AbortController();
500
+ const timeout = setTimeout(() => timeoutController.abort(), options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS);
501
+ try {
502
+ await writeWindowsTarArchiveToZstd(cwd, entries, archivePath, timeoutController.signal);
503
+ return (await stat(archivePath)).size;
504
+ } catch (error) {
505
+ if (timeoutController.signal.aborted) {
506
+ throw new Error(`Oracle archive subprocess timed out after ${options?.commandTimeoutMs ?? ARCHIVE_COMMAND_TIMEOUT_MS}ms`);
507
+ }
508
+ throw error;
509
+ } finally {
510
+ clearTimeout(timeout);
511
+ }
512
+ }
513
+
400
514
  await new Promise<void>((resolvePromise, rejectPromise) => {
401
- const tar = spawn("tar", ["--null", "-cf", "-", "-T", listPath], {
402
- cwd,
515
+ const scrubbedEnv = sweetCookieSafeStoragePasswordScrubbedEnv();
516
+ const tarArgs = ["--null", "-cf", "-", "-C", cwd, "-T", basename(listPath)];
517
+ const tar = spawn(process.env.PI_ORACLE_TEST_TAR_BIN ?? "tar", tarArgs, {
518
+ cwd: dirname(listPath),
519
+ env: scrubbedEnv,
403
520
  stdio: ["ignore", "pipe", "pipe"],
404
521
  });
405
- const zstd = spawn("zstd", ["-19", "-T0", "-f", "-o", archivePath], {
522
+ const zstd = spawn(process.env.PI_ORACLE_TEST_ZSTD_BIN ?? "zstd", ["-19", "-T0", "-f", "-o", archivePath], {
523
+ env: scrubbedEnv,
406
524
  stdio: ["pipe", "ignore", "pipe"],
407
525
  });
408
526
 
@@ -832,6 +950,14 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
832
950
  };
833
951
  }
834
952
 
953
+ if (/^Oracle (auth seed profile|runtime profile|runtime profiles) path is unsafe: /.test(message)) {
954
+ return {
955
+ code: "oracle_profile_path_unsafe",
956
+ message,
957
+ suggestedNextStep: "Move browser.authSeedProfileDir/browser.runtimeProfilesDir outside real browser profile directories and retry.",
958
+ };
959
+ }
960
+
835
961
  if (message.startsWith("Oracle runtime profiles directory is not writable: ")) {
836
962
  return {
837
963
  code: "runtime_profiles_dir_unwritable",
@@ -964,10 +1090,10 @@ function buildOracleToolErrorDetails(toolName: OracleToolErrorSource, error: unk
964
1090
  function buildOracleToolErrorResult(
965
1091
  toolName: OracleToolName,
966
1092
  error: unknown,
967
- params: Record<string, unknown>,
1093
+ params: unknown,
968
1094
  options?: { job?: NonNullable<ReturnType<typeof readJob>>; jobDetails?: OracleToolJobDetailsOptions },
969
1095
  ) {
970
- const errorDetails = buildOracleToolErrorDetails(toolName, error, params);
1096
+ const errorDetails = buildOracleToolErrorDetails(toolName, error, asRecord(params) ?? {});
971
1097
  return {
972
1098
  content: [{
973
1099
  type: "text" as const,
@@ -1430,7 +1556,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1430
1556
  await appendCleanupWarnings(job.id, cleanupReport.warnings).catch(() => undefined);
1431
1557
  }
1432
1558
  if (ctx.hasUI) refreshOracleStatus(ctx);
1433
- return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>, {
1559
+ return buildOracleToolErrorResult("oracle_submit", error, params, {
1434
1560
  job: latest ?? job,
1435
1561
  jobDetails: {
1436
1562
  queue: latest ? buildOracleQueueSnapshot(latest, latest.status === "queued" ? getQueuePosition(latest.id) : undefined) : undefined,
@@ -1443,7 +1569,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1443
1569
  await rm(tempArchivePath, { force: true }).catch(() => undefined);
1444
1570
  }
1445
1571
  } catch (error) {
1446
- return buildOracleToolErrorResult("oracle_submit", error, params as unknown as Record<string, unknown>);
1572
+ return buildOracleToolErrorResult("oracle_submit", error, params);
1447
1573
  }
1448
1574
  },
1449
1575
  });
@@ -1503,7 +1629,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1503
1629
  },
1504
1630
  };
1505
1631
  } catch (error) {
1506
- return buildOracleToolErrorResult("oracle_read", error, params as unknown as Record<string, unknown>);
1632
+ return buildOracleToolErrorResult("oracle_read", error, params);
1507
1633
  }
1508
1634
  },
1509
1635
  });
@@ -1538,7 +1664,7 @@ export function registerOracleTools(pi: ExtensionAPI, workerPath: string, authWo
1538
1664
  details: { job: redactJobDetails(cancelled, { queue: buildOracleQueueSnapshot(cancelled, cancelled.status === "queued" ? getQueuePosition(cancelled.id) : undefined) }) },
1539
1665
  };
1540
1666
  } catch (error) {
1541
- return buildOracleToolErrorResult("oracle_cancel", error, params as unknown as Record<string, unknown>);
1667
+ return buildOracleToolErrorResult("oracle_cancel", error, params);
1542
1668
  }
1543
1669
  },
1544
1670
  });
@@ -0,0 +1,59 @@
1
+ export type OraclePlatform = NodeJS.Platform;
2
+
3
+ export interface BrowserPathOptions {
4
+ env?: NodeJS.ProcessEnv;
5
+ homeDir?: string;
6
+ }
7
+
8
+ export interface ExecutableSearchOptions {
9
+ pathValue?: string;
10
+ pathDelimiter?: string;
11
+ }
12
+
13
+ export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES: readonly [
14
+ "SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
15
+ "SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
16
+ "SWEET_COOKIE_EDGE_SAFE_STORAGE_PASSWORD",
17
+ ];
18
+
19
+ export function expandHomePath(value: string, homeDir?: string): string;
20
+ export function normalizedAbsolutePath(value: string, options?: BrowserPathOptions): string;
21
+ export function linuxConfigHome(options?: BrowserPathOptions): string;
22
+ export function linuxChromiumCookieImportUserDataDirs(options?: BrowserPathOptions): string[];
23
+ export function linuxBrowserSafetyUserDataDirs(options?: BrowserPathOptions): string[];
24
+ export function browserUserDataDirsForPlatform(
25
+ platform?: OraclePlatform,
26
+ options?: BrowserPathOptions & { includeUnsupported?: boolean },
27
+ ): string[];
28
+ export function defaultCloneStrategyForPlatform(platform?: OraclePlatform): "apfs-clone" | "copy";
29
+ export function chromiumKeychainSupportedOnPlatform(platform?: OraclePlatform): boolean;
30
+ export function chromeUserAgentPlatformToken(platform?: OraclePlatform): string | undefined;
31
+ export function pathInsideOrEqual(childPath: string, parentPath: string): boolean;
32
+ export function resolvePathThroughExistingAncestorsSync(pathValue: string): string | undefined;
33
+ export function protectedCookieSourcePaths(cookieSources?: { chromeProfile?: string; chromeCookiePath?: string }): string[];
34
+ export function knownBrowserUserDataPathMatch(
35
+ pathValue: string,
36
+ options?: BrowserPathOptions & {
37
+ platform?: OraclePlatform;
38
+ includeUnsupported?: boolean;
39
+ extraProtectedPaths?: string[];
40
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
41
+ },
42
+ ): string | undefined;
43
+ export function assertNotKnownBrowserUserDataPath(
44
+ pathValue: string,
45
+ label: string,
46
+ options?: BrowserPathOptions & {
47
+ platform?: OraclePlatform;
48
+ includeUnsupported?: boolean;
49
+ extraProtectedPaths?: string[];
50
+ cookieSources?: { chromeProfile?: string; chromeCookiePath?: string };
51
+ },
52
+ ): void;
53
+ export function isExecutableFileSync(pathValue: string): boolean;
54
+ export function findExecutableOnPathSync(names: readonly string[], options?: ExecutableSearchOptions): string | undefined;
55
+ export function detectDefaultLinuxChromeExecutablePath(options?: ExecutableSearchOptions): string | undefined;
56
+ export function detectDefaultLinuxCookieProfileSource(options?: BrowserPathOptions): string | undefined;
57
+ export function detectDefaultBrowserProfileSource(platform?: OraclePlatform, options?: BrowserPathOptions): string;
58
+ export function scrubSweetCookieSafeStoragePasswordEnv(env?: NodeJS.ProcessEnv): void;
59
+ export function sweetCookieSafeStoragePasswordScrubbedEnv(env?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
@@ -0,0 +1,395 @@
1
+ // Purpose: Centralize platform-specific browser profile paths, executable discovery, and profile-safety checks for oracle auth/runtime code.
2
+ // Responsibilities: Resolve Chromium-family user-data roots, choose platform defaults, find executable files safely, and block oracle profile paths that point into real browser data.
3
+ // Scope: Local filesystem/path policy only; cookie import, browser automation, and config loading stay in higher-level modules.
4
+ // Usage: Imported by config.ts, runtime.ts, auth-bootstrap.mjs, run-job.mjs, and sanity tests.
5
+ // Invariants/Assumptions: Real browser profile roots must never be used as oracle seed/runtime profile destinations, even through symlinked ancestors.
6
+
7
+ import { accessSync, constants as fsConstants, existsSync, realpathSync, readFileSync, statSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { basename, delimiter, dirname, isAbsolute, join, normalize, resolve } from "node:path";
10
+
11
+ /** @typedef {import("./browser-profile-helpers.d.mts").OraclePlatform} OraclePlatform */
12
+ /** @typedef {import("./browser-profile-helpers.d.mts").BrowserPathOptions} BrowserPathOptions */
13
+ /** @typedef {import("./browser-profile-helpers.d.mts").ExecutableSearchOptions} ExecutableSearchOptions */
14
+
15
+ export const SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES = Object.freeze([
16
+ "SWEET_COOKIE_CHROME_SAFE_STORAGE_PASSWORD",
17
+ "SWEET_COOKIE_BRAVE_SAFE_STORAGE_PASSWORD",
18
+ "SWEET_COOKIE_EDGE_SAFE_STORAGE_PASSWORD",
19
+ ]);
20
+
21
+ const LINUX_COOKIE_IMPORT_USER_DATA_RELATIVE_DIRS = Object.freeze([
22
+ ["google-chrome"],
23
+ ["chromium"],
24
+ ["chromium-browser"],
25
+ ["BraveSoftware", "Brave-Browser"],
26
+ ]);
27
+
28
+ const LINUX_SAFETY_EXTRA_USER_DATA_RELATIVE_DIRS = Object.freeze([
29
+ ["google-chrome-beta"],
30
+ ["google-chrome-unstable"],
31
+ ["microsoft-edge"],
32
+ ["microsoft-edge-beta"],
33
+ ["microsoft-edge-dev"],
34
+ ["vivaldi"],
35
+ ["opera"],
36
+ ]);
37
+
38
+ const MAC_CHROMIUM_USER_DATA_RELATIVE_DIRS = Object.freeze([
39
+ ["Library", "Application Support", "Google", "Chrome"],
40
+ ["Library", "Application Support", "Chromium"],
41
+ ["Library", "Application Support", "BraveSoftware", "Brave-Browser"],
42
+ ["Library", "Application Support", "Microsoft Edge"],
43
+ ["Library", "Application Support", "Arc", "User Data"],
44
+ ["Library", "Application Support", "Vivaldi"],
45
+ ["Library", "Application Support", "com.operasoftware.Opera"],
46
+ ["Library", "Application Support", "Google", "Chrome for Testing"],
47
+ ]);
48
+
49
+ const WINDOWS_CHROMIUM_USER_DATA_RELATIVE_DIRS = Object.freeze([
50
+ ["AppData", "Local", "Google", "Chrome", "User Data"],
51
+ ["AppData", "Local", "Chromium", "User Data"],
52
+ ["AppData", "Local", "BraveSoftware", "Brave-Browser", "User Data"],
53
+ ["AppData", "Local", "Microsoft", "Edge", "User Data"],
54
+ ["AppData", "Local", "Vivaldi", "User Data"],
55
+ ["AppData", "Roaming", "Opera Software", "Opera Stable"],
56
+ ]);
57
+
58
+ const LINUX_CHROME_EXECUTABLE_NAMES = Object.freeze(["google-chrome", "google-chrome-stable", "chromium", "chromium-browser", "brave-browser", "brave"]);
59
+
60
+ /**
61
+ * @param {string} value
62
+ * @param {string} [homeDir]
63
+ * @returns {string}
64
+ */
65
+ export function expandHomePath(value, homeDir = homedir()) {
66
+ if (value === "~") return homeDir;
67
+ if (value.startsWith("~/")) return join(homeDir, value.slice(2));
68
+ return value;
69
+ }
70
+
71
+ /**
72
+ * @param {string} value
73
+ * @param {BrowserPathOptions} [options]
74
+ * @returns {string}
75
+ */
76
+ export function normalizedAbsolutePath(value, options = {}) {
77
+ const expanded = expandHomePath(value, options.homeDir ?? homedir());
78
+ return normalize(isAbsolute(expanded) ? expanded : resolve(expanded));
79
+ }
80
+
81
+ /**
82
+ * @param {BrowserPathOptions} [options]
83
+ * @returns {string}
84
+ */
85
+ export function linuxConfigHome(options = {}) {
86
+ const env = options.env ?? process.env;
87
+ const configured = env.XDG_CONFIG_HOME?.trim();
88
+ return configured ? normalizedAbsolutePath(configured, options) : join(options.homeDir ?? homedir(), ".config");
89
+ }
90
+
91
+ /**
92
+ * @param {BrowserPathOptions} [options]
93
+ * @returns {string[]}
94
+ */
95
+ export function linuxChromiumCookieImportUserDataDirs(options = {}) {
96
+ const configHome = linuxConfigHome(options);
97
+ return LINUX_COOKIE_IMPORT_USER_DATA_RELATIVE_DIRS.map((segments) => join(configHome, ...segments));
98
+ }
99
+
100
+ /**
101
+ * @param {BrowserPathOptions} [options]
102
+ * @returns {string[]}
103
+ */
104
+ export function linuxBrowserSafetyUserDataDirs(options = {}) {
105
+ const configHome = linuxConfigHome(options);
106
+ return [
107
+ ...LINUX_COOKIE_IMPORT_USER_DATA_RELATIVE_DIRS,
108
+ ...LINUX_SAFETY_EXTRA_USER_DATA_RELATIVE_DIRS,
109
+ ].map((segments) => join(configHome, ...segments));
110
+ }
111
+
112
+ /**
113
+ * @param {OraclePlatform} [platform]
114
+ * @param {BrowserPathOptions & { includeUnsupported?: boolean }} [options]
115
+ * @returns {string[]}
116
+ */
117
+ export function browserUserDataDirsForPlatform(platform = process.platform, options = {}) {
118
+ const homeDir = options.homeDir ?? homedir();
119
+ if (platform === "darwin") return MAC_CHROMIUM_USER_DATA_RELATIVE_DIRS.map((segments) => join(homeDir, ...segments));
120
+ if (platform === "linux") return options.includeUnsupported === false ? linuxChromiumCookieImportUserDataDirs({ ...options, homeDir }) : linuxBrowserSafetyUserDataDirs({ ...options, homeDir });
121
+ if (platform === "win32") return WINDOWS_CHROMIUM_USER_DATA_RELATIVE_DIRS.map((segments) => join(homeDir, ...segments));
122
+ return [];
123
+ }
124
+
125
+ /**
126
+ * @param {OraclePlatform} [platform]
127
+ * @returns {"apfs-clone" | "copy"}
128
+ */
129
+ export function defaultCloneStrategyForPlatform(platform = process.platform) {
130
+ return platform === "darwin" ? "apfs-clone" : "copy";
131
+ }
132
+
133
+ /**
134
+ * @param {OraclePlatform} [platform]
135
+ * @returns {boolean}
136
+ */
137
+ export function chromiumKeychainSupportedOnPlatform(platform = process.platform) {
138
+ return platform === "darwin";
139
+ }
140
+
141
+ /**
142
+ * @param {OraclePlatform} [platform]
143
+ * @returns {string | undefined}
144
+ */
145
+ export function chromeUserAgentPlatformToken(platform = process.platform) {
146
+ if (platform === "darwin") return "Macintosh; Intel Mac OS X 10_15_7";
147
+ if (platform === "linux") return "X11; Linux x86_64";
148
+ return undefined;
149
+ }
150
+
151
+ /**
152
+ * @param {string} childPath
153
+ * @param {string} parentPath
154
+ * @returns {boolean}
155
+ */
156
+ export function pathInsideOrEqual(childPath, parentPath) {
157
+ const child = normalize(childPath);
158
+ const parent = normalize(parentPath);
159
+ if (child === parent) return true;
160
+ if (!parent) return false;
161
+ const parentWithSeparator = /[/\\]$/.test(parent) ? parent : `${parent}/`;
162
+ const alternateParentWithSeparator = parentWithSeparator.includes("/")
163
+ ? parentWithSeparator.replaceAll("/", "\\")
164
+ : parentWithSeparator.replaceAll("\\", "/");
165
+ return child.startsWith(parentWithSeparator) || child.startsWith(alternateParentWithSeparator);
166
+ }
167
+
168
+ /**
169
+ * Resolve a path as far as its existing ancestors allow. If the final path does
170
+ * not exist yet, any non-existing suffix is appended to the nearest existing
171
+ * ancestor's realpath so symlinked ancestors are still accounted for.
172
+ *
173
+ * @param {string} pathValue
174
+ * @returns {string | undefined}
175
+ */
176
+ export function resolvePathThroughExistingAncestorsSync(pathValue) {
177
+ const absolute = normalizedAbsolutePath(pathValue);
178
+ const suffix = [];
179
+ let current = absolute;
180
+ while (true) {
181
+ if (existsSync(current)) {
182
+ try {
183
+ return normalize(join(realpathSync(current), ...suffix.reverse()));
184
+ } catch {
185
+ return undefined;
186
+ }
187
+ }
188
+ const parent = dirname(current);
189
+ if (parent === current) return undefined;
190
+ suffix.push(basename(current));
191
+ current = parent;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * @param {string} pathValue
197
+ * @returns {boolean}
198
+ */
199
+ function looksLikeFilesystemPath(pathValue) {
200
+ return pathValue.startsWith("/") || pathValue.startsWith("~/") || pathValue === "~" || pathValue.startsWith(".");
201
+ }
202
+
203
+ /**
204
+ * @param {string} pathValue
205
+ * @returns {boolean}
206
+ */
207
+ function isCookiesDbPath(pathValue) {
208
+ return basename(pathValue) === "Cookies";
209
+ }
210
+
211
+ /**
212
+ * @param {string} cookiePath
213
+ * @returns {string[]}
214
+ */
215
+ function protectedPathsForCookieDb(cookiePath) {
216
+ const normalized = normalizedAbsolutePath(cookiePath);
217
+ const cookieParent = dirname(normalized);
218
+ const profileDir = basename(cookieParent) === "Network" ? dirname(cookieParent) : cookieParent;
219
+ return [profileDir, dirname(profileDir)];
220
+ }
221
+
222
+ /**
223
+ * @param {{ chromeProfile?: string; chromeCookiePath?: string } | undefined} cookieSources
224
+ * @returns {string[]}
225
+ */
226
+ export function protectedCookieSourcePaths(cookieSources) {
227
+ if (!cookieSources) return [];
228
+ const roots = [];
229
+ const cookiePath = typeof cookieSources.chromeCookiePath === "string" && cookieSources.chromeCookiePath.trim()
230
+ ? cookieSources.chromeCookiePath.trim()
231
+ : undefined;
232
+ if (cookiePath) roots.push(...protectedPathsForCookieDb(cookiePath));
233
+
234
+ const profile = typeof cookieSources.chromeProfile === "string" && cookieSources.chromeProfile.trim()
235
+ ? cookieSources.chromeProfile.trim()
236
+ : undefined;
237
+ if (profile && looksLikeFilesystemPath(profile)) {
238
+ const normalizedProfile = normalizedAbsolutePath(profile);
239
+ if (isCookiesDbPath(normalizedProfile)) roots.push(...protectedPathsForCookieDb(normalizedProfile));
240
+ else roots.push(normalizedProfile, dirname(normalizedProfile));
241
+ }
242
+
243
+ return [...new Set(roots.map((root) => normalize(root)))];
244
+ }
245
+
246
+ /**
247
+ * @param {string} pathValue
248
+ * @param {BrowserPathOptions & { platform?: OraclePlatform; includeUnsupported?: boolean; extraProtectedPaths?: string[]; cookieSources?: { chromeProfile?: string; chromeCookiePath?: string } }} [options]
249
+ * @returns {string | undefined}
250
+ */
251
+ export function knownBrowserUserDataPathMatch(pathValue, options = {}) {
252
+ const platform = options.platform ?? process.platform;
253
+ const normalizedPath = normalizedAbsolutePath(pathValue, options);
254
+ const resolvedPath = resolvePathThroughExistingAncestorsSync(normalizedPath);
255
+ const roots = [
256
+ ...browserUserDataDirsForPlatform(platform, { ...options, includeUnsupported: options.includeUnsupported ?? true }),
257
+ ...protectedCookieSourcePaths(options.cookieSources),
258
+ ...((options.extraProtectedPaths ?? [])),
259
+ ];
260
+ for (const root of roots) {
261
+ const normalizedRoot = normalizedAbsolutePath(root, options);
262
+ if (pathInsideOrEqual(normalizedPath, normalizedRoot)) return normalizedRoot;
263
+ const resolvedRoot = resolvePathThroughExistingAncestorsSync(normalizedRoot) ?? normalizedRoot;
264
+ if (resolvedPath && pathInsideOrEqual(resolvedPath, resolvedRoot)) return resolvedRoot;
265
+ }
266
+ return undefined;
267
+ }
268
+
269
+ /**
270
+ * @param {string} pathValue
271
+ * @param {string} label
272
+ * @param {BrowserPathOptions & { platform?: OraclePlatform; includeUnsupported?: boolean; extraProtectedPaths?: string[]; cookieSources?: { chromeProfile?: string; chromeCookiePath?: string } }} [options]
273
+ * @returns {void}
274
+ */
275
+ export function assertNotKnownBrowserUserDataPath(pathValue, label, options = {}) {
276
+ const match = knownBrowserUserDataPathMatch(pathValue, options);
277
+ if (match) {
278
+ throw new Error(`${label} must not point into a real browser user-data directory (${match}): ${pathValue}`);
279
+ }
280
+ }
281
+
282
+ /**
283
+ * @param {string} pathValue
284
+ * @returns {boolean}
285
+ */
286
+ export function isExecutableFileSync(pathValue) {
287
+ try {
288
+ const stats = statSync(pathValue);
289
+ if (!stats.isFile()) return false;
290
+ accessSync(pathValue, fsConstants.X_OK);
291
+ return true;
292
+ } catch {
293
+ return false;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * @param {readonly string[]} names
299
+ * @param {ExecutableSearchOptions} [options]
300
+ * @returns {string | undefined}
301
+ */
302
+ export function findExecutableOnPathSync(names, options = {}) {
303
+ const pathValue = options.pathValue ?? process.env.PATH ?? "";
304
+ const pathDelimiter = options.pathDelimiter ?? delimiter;
305
+ for (const name of names) {
306
+ for (const dir of pathValue.split(pathDelimiter).filter(Boolean)) {
307
+ const candidate = join(dir, name);
308
+ if (isExecutableFileSync(candidate)) return candidate;
309
+ }
310
+ }
311
+ return undefined;
312
+ }
313
+
314
+ /**
315
+ * @param {ExecutableSearchOptions} [options]
316
+ * @returns {string | undefined}
317
+ */
318
+ export function detectDefaultLinuxChromeExecutablePath(options = {}) {
319
+ return findExecutableOnPathSync(LINUX_CHROME_EXECUTABLE_NAMES, options);
320
+ }
321
+
322
+ /**
323
+ * @param {string} userDataDir
324
+ * @returns {string | undefined}
325
+ */
326
+ function readLastUsedProfileName(userDataDir) {
327
+ const localStatePath = join(userDataDir, "Local State");
328
+ if (!existsSync(localStatePath)) return undefined;
329
+ try {
330
+ const localState = JSON.parse(readFileSync(localStatePath, "utf8"));
331
+ const lastUsed = localState?.profile?.last_used;
332
+ if (typeof lastUsed !== "string") return undefined;
333
+ const trimmed = lastUsed.trim();
334
+ if (!trimmed || trimmed === "." || trimmed === ".." || trimmed.includes("/") || trimmed.includes("\\")) return undefined;
335
+ return trimmed;
336
+ } catch {
337
+ return undefined;
338
+ }
339
+ }
340
+
341
+ /**
342
+ * Return an absolute Linux Chrome/Chromium-family profile directory for the
343
+ * default cookie importer. Sweet Cookie's Linux `chrome` backend only resolves
344
+ * non-path profile names under google-chrome, so non-Google roots must be
345
+ * passed as absolute paths.
346
+ *
347
+ * @param {BrowserPathOptions} [options]
348
+ * @returns {string | undefined}
349
+ */
350
+ export function detectDefaultLinuxCookieProfileSource(options = {}) {
351
+ for (const userDataDir of linuxChromiumCookieImportUserDataDirs(options)) {
352
+ const lastUsed = readLastUsedProfileName(userDataDir);
353
+ if (lastUsed) {
354
+ const profilePath = join(userDataDir, lastUsed);
355
+ if (pathInsideOrEqual(profilePath, userDataDir) && existsSync(profilePath)) return profilePath;
356
+ }
357
+ const defaultProfile = join(userDataDir, "Default");
358
+ if (existsSync(defaultProfile)) return defaultProfile;
359
+ }
360
+ return undefined;
361
+ }
362
+
363
+ /**
364
+ * @param {OraclePlatform} [platform]
365
+ * @param {BrowserPathOptions} [options]
366
+ * @returns {string}
367
+ */
368
+ export function detectDefaultBrowserProfileSource(platform = process.platform, options = {}) {
369
+ if (platform === "linux") return detectDefaultLinuxCookieProfileSource(options) ?? "Default";
370
+ if (platform === "darwin") {
371
+ const userDataDir = browserUserDataDirsForPlatform("darwin", options)[0];
372
+ return readLastUsedProfileName(userDataDir) ?? "Default";
373
+ }
374
+ return "Default";
375
+ }
376
+
377
+ /**
378
+ * @param {NodeJS.ProcessEnv} [env]
379
+ * @returns {void}
380
+ */
381
+ export function scrubSweetCookieSafeStoragePasswordEnv(env = process.env) {
382
+ for (const name of SWEET_COOKIE_SAFE_STORAGE_PASSWORD_ENV_NAMES) {
383
+ delete env[name];
384
+ }
385
+ }
386
+
387
+ /**
388
+ * @param {NodeJS.ProcessEnv} [env]
389
+ * @returns {NodeJS.ProcessEnv}
390
+ */
391
+ export function sweetCookieSafeStoragePasswordScrubbedEnv(env = process.env) {
392
+ const childEnv = { ...env };
393
+ scrubSweetCookieSafeStoragePasswordEnv(childEnv);
394
+ return childEnv;
395
+ }