Skill Tracer
Skills › gstack › browse › cookie-import-native.ts

cookie-import-native.ts

159 lines · browse/src/cookie-import-native.ts at 3f79d81

1import { spawn } from 'node:child_process';
2import { realpathSync, statSync } from 'node:fs';
3import { createRequire } from 'node:module';
4import { release } from 'node:os';
5import path from 'node:path';
6import { CookieImportError, normalizeCookieDomain, type PlaywrightCookie } from './cookie-import-browser';
7import { emptyBunConfig, findWindowsBunExe } from './bun-child-config';
8import { hashNativeFile, nativeCodeHashes, nativeCodeMatches, readNativeQualifications, type NativeQualifiedBuild } from './cookie-import-native-integrity';
9
10type BrowserName = 'Chrome' | 'Chromium' | 'Brave' | 'Edge';
12export function nativeBrowserPaths(browserName: string, env: NodeJS.ProcessEnv): {
13 name: BrowserName;
14 userDataDir: string;
15 executables: string[];
16} {
17 const get = (key: string) => Object.entries(env).find(([name]) => name.toLowerCase() === key.toLowerCase())?.[1];
18 const local = get('LOCALAPPDATA');
19 if (!local || !path.win32.isAbsolute(local)) {
20 throw new CookieImportError('The Windows browser data location is unavailable. Sign in manually in the gstack browser.', 'native_profile_unsupported');
21 }
22 const pf = get('PROGRAMFILES') || 'C:\\Program Files';
23 const pf86 = get('PROGRAMFILES(X86)') || 'C:\\Program Files (x86)';
24 const mappings = {
25 chrome: { name: 'Chrome', root: ['Google', 'Chrome'], exe: 'chrome.exe', installs: [pf, pf86, local] },
26 chromium: { name: 'Chromium', root: ['Chromium'], exe: 'chrome.exe', installs: [local] },
27 brave: { name: 'Brave', root: ['BraveSoftware', 'Brave-Browser'], exe: 'brave.exe', installs: [pf, pf86, local] },
28 edge: { name: 'Edge', root: ['Microsoft', 'Edge'], exe: 'msedge.exe', installs: [pf86, pf, local] },
29 } as const;
30 const key = browserName.toLowerCase();
31 if (!Object.hasOwn(mappings, key)) {
32 throw new CookieImportError('This browser has no supported Windows native-cookie mapping. Sign in manually in the gstack browser.', 'not_supported');
33 }
34 const browser = mappings[key as keyof typeof mappings];
35 return {
36 name: browser.name,
37 userDataDir: path.win32.join(local, ...browser.root, 'User Data'),
38 executables: [...new Set(browser.installs.map(root => path.win32.join(root, ...browser.root, 'Application', browser.exe)))],
39 };
42export async function importNativeCookies(options: {
43 browserName: string;
44 userDataDir: string;
45 profile: string;
46 domains: string[];
47}): Promise<PlaywrightCookie[]> {
48 let domains: string[];
49 try {
50 if (!Array.isArray(options.domains)) throw new Error();
51 domains = [...new Set(options.domains.map(normalizeCookieDomain))];
52 } catch {
53 throw new CookieImportError('Native cookie import needs explicit valid domain selections.', 'invalid_domain');
54 }
55 if (!domains.length) return [];
56 if (process.platform !== 'win32' || process.versions.bun) {
57 throw new CookieImportError('Native cookie import requires the Windows Node server. Sign in manually in the gstack browser.', 'not_supported');
58 }
59 if (!/^(Default|Profile [0-9]+)$/.test(options.profile)) {
60 throw new CookieImportError('Select an existing browser profile before importing cookies.', 'invalid_profile');
61 }
62 const browser = nativeBrowserPaths(options.browserName, process.env);
63 if (path.win32.resolve(options.userDataDir).toLowerCase() !== path.win32.resolve(browser.userDataDir).toLowerCase()) {
64 throw new CookieImportError('Native cookie import cannot substitute or copy the selected browser profile. Sign in manually in the gstack browser.', 'native_profile_unsupported');
65 }
66 if (browser.name === 'Chrome') {
67 throw new CookieImportError('Chrome 136 and later block remote debugging of the default user-data directory, including every profile inside it, over both pipe and TCP. Default-directory extraction is unavailable; sign in manually in the gstack browser.', 'native_profile_unsupported');
68 }
69 const deadline = Date.now() + 25_000;
70 let qualified: NativeQualifiedBuild[];
71 try {
72 const root = path.resolve(import.meta.dir, '../..');
73 const builds = await readNativeQualifications(root, deadline);
74 qualified = builds.filter(build => build.browserName === browser.name && build.nodeVersion === process.version && build.architecture === process.arch && build.windowsRelease === release());
75 if (qualified.length) {
76 const hashes = await nativeCodeHashes(root, deadline);
77 qualified = qualified.filter(build => nativeCodeMatches(build.sourceHashes, hashes));
78 }
79 } catch (error) {
80 throw new CookieImportError('The native-cookie qualification inputs could not be verified. Sign in manually in the gstack browser.', error instanceof Error && error.message === 'native_timeout' ? 'native_timeout' : 'native_unqualified');
81 }
82 if (!qualified.length) {
83 throw new CookieImportError('Windows native cookie extraction is disabled until this browser and runtime pass native process-ownership and forced-cleanup qualification. Sign in manually in the gstack browser.', 'native_unqualified');
84 }
85 const executablePath = browser.executables.find(candidate => {
86 try { return statSync(candidate).isFile(); } catch { return false; }
87 });
88 if (!executablePath) throw new CookieImportError('The selected browser executable is not installed.', 'not_installed');
89 try {
90 const profilePath = path.join(options.userDataDir, options.profile);
91 if (realpathSync(options.userDataDir).toLowerCase() !== path.win32.resolve(options.userDataDir).toLowerCase()
92 || realpathSync(profilePath).toLowerCase() !== path.win32.resolve(profilePath).toLowerCase()
93 || !statSync(profilePath).isDirectory()) {
94 throw new Error('invalid_profile');
95 }
96 } catch {
97 throw new CookieImportError('The selected source profile is unavailable or redirects to another location.', 'native_profile_unsupported');
98 }
99 const require = createRequire(import.meta.url);
100 let playwrightVersion: string;
101 let playwrightEntry: string;
102 let executableSha256: string;
103 try {
104 playwrightVersion = require('playwright/package.json').version;
105 playwrightEntry = require.resolve('playwright');
106 executableSha256 = await hashNativeFile(executablePath, deadline);
107 } catch (error) {
108 throw new CookieImportError('The native browser build could not be checked safely. Sign in manually in the gstack browser.', error instanceof Error && error.message === 'native_timeout' ? 'native_timeout' : 'native_unqualified');
109 }
110 const bunExecutable = findWindowsBunExe();
111 if (!bunExecutable || !qualified.some(build => build.executableSha256 === executableSha256 && build.playwrightVersion === playwrightVersion)) {
112 throw new CookieImportError('This browser build or supervisor runtime has not passed native qualification. Sign in manually in the gstack browser.', 'native_unqualified');
113 }
114 if (Date.now() >= deadline) throw new CookieImportError('Native cookie import exceeded its operation deadline.', 'native_timeout');
115 const env = Object.fromEntries(Object.entries(process.env).filter(([key, value]) => /^(systemroot|windir|temp|tmp|userprofile|localappdata|appdata|programfiles|programfiles\(x86\)|programdata|path|pathext)$/i.test(key) && typeof value === 'string'));
116 const worker = spawn(bunExecutable, ['--no-env-file', '--no-install', '--no-macros', `--config=${emptyBunConfig()}`, path.join(import.meta.dir, 'cookie-import-native-worker.ts')], {
117 env,
118 stdio: ['pipe', 'pipe', 'ignore'],
119 windowsHide: true,
120 });
121 return new Promise((resolve, reject) => {
122 let output = '';
123 const timer = setTimeout(() => {
124 worker.kill();
125 reject(new CookieImportError('Native cookie cleanup could not be confirmed within its deadline.', 'native_cleanup_failed'));
126 }, Math.max(0, deadline + 5_000 - Date.now()));
127 worker.stdout.setEncoding('utf8');
128 worker.stdout.on('data', chunk => {
129 output += chunk;
130 if (Buffer.byteLength(output) > 8 * 1024 * 1024) worker.kill();
131 });
132 worker.once('error', () => {
133 clearTimeout(timer);
134 reject(new CookieImportError('Native cookie supervision could not start.', 'native_supervision_failed'));
135 });
136 worker.once('close', () => {
137 clearTimeout(timer);
138 try {
139 const result = JSON.parse(output);
140 if (Array.isArray(result.cookies)) resolve(result.cookies);
141 else reject(new CookieImportError('Native cookie import did not complete. Sign in manually in the gstack browser.', ['native_timeout', 'native_failed', 'native_cleanup_failed', 'native_supervision_failed', 'browser_running', 'native_profile_unsupported'].includes(result.error) ? result.error : 'native_failed'));
142 } catch {
143 reject(new CookieImportError('Native cookie supervision did not return a complete result.', 'native_failed'));
144 }
145 });
146 worker.stdin.on('error', () => {});
147 worker.stdin.write(JSON.stringify({
148 nodeExecutable: process.execPath,
149 nodeArchitecture: process.arch,
150 playwrightEntry,
151 executablePath,
152 userDataDir: options.userDataDir,
153 profile: options.profile,
154 domains,
155 deadline,
156 qualifiedBunVersions: qualified.filter(build => build.executableSha256 === executableSha256 && build.playwrightVersion === playwrightVersion).map(build => build.bunVersion),
157 }) + '\n');
158 });