mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-04 10:26:52 +02:00
v1.90.0.0 feat: make browser cookie imports explicit and safe (#2964)
* fix(browse): prepare reliable cookie import wave for validation * ci: sequence quality and behavior for validation branch * fix(browse): isolate Windows qualification and preserve native diagnostics * test(browse): cover cookie workflow quality and isolate Windows user paths * test(browse): trace native member startup and initialize fresh folders * fix(browse): keep Windows member stdin alive through EOF * fix(browse): latch native timeouts and compare contained Edge startup * test(browse): verify native version metadata and actual Windows argv * test(browse): qualify Dia import on isolated macOS CI * fix(browse): require picker origin for session mutations * fix(browse): bound credential reads through stream completion * test(browse): inspect owned Windows process arguments natively * test(evals): preserve passing coverage during cookie repair reruns * test(browse): isolate Dia qualification in a fresh macOS account * test(browse): pass bounded integer timeouts to native Mac probes * test(browse): distinguish Windows profile initialization from containment * test(browse): await descendant pipe readiness before parent exit * test(browse): initialize and restore isolated macOS Keychain state * test(browse): initialize Windows fixture folders before qualification * test(ci): pin the same Node runtime across Windows checks * test(browse): distinguish native macOS browser preflight stages * test(browse): isolate Windows descendant console lifetime * test(browse): preserve native receipts and identify fixture lock holders * test(browse): prepare dependency resolution before native Mac worker startup * test(ci): include lock and close checks in native diagnostics * test(browse): preserve native owner probe stages and subprocess deadlines * fix(browse): classify Chromium profile-in-use exit precisely * test(browse): retain Mac qualification evidence through cleanup failures * test(browse): bound Mac fixture paths and retire its owned user domain * test(browse): accept vanished fixture entries without weakening cleanup * test(browse): identify probe-created macOS user domains safely * test(browse): observe Mac user domains without targeting them first * test(browse): use passive fresh-user ownership throughout Mac qualification * test(browse): distinguish profile and registered-home Keychain lookups * test(browse): qualify Dia under one registered account home * test(browse): identify Dia startup and owned process-group failures * test(browse): classify bounded Dia startup diagnostics without leaking output * fix(test): preserve native Mac sandboxing and reap owned browser children * fix(browse): preserve Chromium sandboxing for native profile imports * test(browse): inspect signed Mach-O architecture without launching Xcode tools * test(browse): sample pending Dia startup and reap on all cleanup paths * test(browse): compare protected Dia launches in fresh Bun and Node accounts * test(browse): inspect isolated Mac GUI readiness without browser access * v1.90.0.0 fix: bind cookie picker actions to their document * test: validate cookie guards and fit nested launch fixtures * ci: configure the bundled Chromium sandbox helper * fix(browse): classify Playwright authentication timeouts * test: retain bounded Windows lifecycle diagnostics * test(cso): reuse bounded NTFS precision candidates * test(review): handle explicit preservation choices safely * test(browse): remove owned fixture directories with explicit primitives * test(review): distinguish descriptive reuse from edit commitments * test: admit only the approved unscored cookie workflow refusal * test: keep the Office Hours judge mock export-complete * fix: keep dependency-free CI planners independent of the model SDK * test: observe the exact holder after a native fixture unlink failure * fix: start seeded PTY observations at owned readiness * test: acquire identity-bound Windows deletion admission before profile resets * test: preserve qualified Git index bits without authorizing mutations
This commit is contained in:
1 parent
730a1017d1
commit
a84b0b5b6d
111 files changed
+14996
-1057
No files matched your search
@@ -0,0 +1,198 @@
|
||||
import type { Page } from 'playwright';
|
||||
import {
|
||||
CookieImportError, cookieDomainMatches, importCookies, importCookiesViaCdp,
|
||||
listDomains, listProfiles, normalizeCookieDomain, withCookieReadRetry,
|
||||
type ProfileEntry,
|
||||
} from './cookie-import-browser';
|
||||
import { clearCookieTargetStorage, validateCookieAuthOptions, validateCookieStorageSupport, verifyCookieAuthentication, type CookieAuthVerificationOptions } from './cookie-auth-verification';
|
||||
|
||||
export interface CookieImportTarget {
|
||||
page: Page;
|
||||
url: string;
|
||||
}
|
||||
|
||||
export interface CookieImportOptions {
|
||||
browser: string;
|
||||
domains?: string[];
|
||||
profile?: string;
|
||||
all?: boolean;
|
||||
clearStorage?: boolean;
|
||||
verifyAuth?: boolean;
|
||||
}
|
||||
|
||||
const activeImports = new WeakSet<object>();
|
||||
|
||||
export function parseCookieImportArgs(args: string[]): CookieImportOptions {
|
||||
const options: CookieImportOptions = { browser: 'comet' };
|
||||
let browserSet = false;
|
||||
const seen = new Set<string>();
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
if (!arg.startsWith('--')) {
|
||||
if (browserSet) throw new CookieImportError('Specify only one source browser.', 'bad_request');
|
||||
options.browser = arg;
|
||||
browserSet = true;
|
||||
continue;
|
||||
}
|
||||
if (seen.has(arg)) throw new CookieImportError('Duplicate cookie-import option.', 'bad_request');
|
||||
seen.add(arg);
|
||||
if (arg === '--domain' || arg === '--profile') {
|
||||
const value = args[++i];
|
||||
if (!value || value.startsWith('--')) throw new CookieImportError('Cookie-import option requires a value.', 'bad_request');
|
||||
if (arg === '--domain') options.domains = [normalizeCookieDomain(value)];
|
||||
else options.profile = value;
|
||||
} else if (arg === '--all') options.all = true;
|
||||
else if (arg === '--clear-storage') options.clearStorage = true;
|
||||
else if (arg === '--verify-auth') options.verifyAuth = true;
|
||||
else throw new CookieImportError('Unknown cookie-import option.', 'bad_request');
|
||||
}
|
||||
if (options.all && options.domains) throw new CookieImportError('Choose --domain or --all, not both.', 'bad_request');
|
||||
if (options.all && options.clearStorage) throw new CookieImportError('Storage reset requires a single target origin; --all is not supported.', 'bad_request');
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function getCookieProfiles(browser: string, domains: string[] = [], hostname?: string) {
|
||||
const profiles: Array<ProfileEntry & { matches?: boolean; unavailable?: boolean }> = listProfiles(browser);
|
||||
if (domains.length || hostname) {
|
||||
const selected = domains.map(normalizeCookieDomain);
|
||||
let index = 0;
|
||||
const check = async () => {
|
||||
while (index < profiles.length) {
|
||||
const profile = profiles[index++];
|
||||
try {
|
||||
const result = await withCookieReadRetry(() => listDomains(browser, profile.name));
|
||||
profile.matches = result.domains.some(entry => selected.length
|
||||
? selected.includes(normalizeCookieDomain(entry.domain))
|
||||
: cookieDomainMatches(hostname!, entry.domain));
|
||||
} catch (err) {
|
||||
if (err instanceof CookieImportError && err.code === 'sqlite_unavailable') throw err;
|
||||
profile.unavailable = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
await Promise.all([check(), check()]);
|
||||
}
|
||||
const matching = profiles.filter(profile => profile.matches === true);
|
||||
const recommendedProfile = !profiles.some(profile => profile.unavailable) && matching.length === 1
|
||||
? matching[0].name : profiles.length === 1 && profiles[0].matches !== false && !profiles[0].unavailable ? profiles[0].name : undefined;
|
||||
return { profiles, recommendedProfile };
|
||||
}
|
||||
|
||||
export function validateCookieTarget(target: CookieImportTarget): URL {
|
||||
try {
|
||||
const url = new URL(target.url);
|
||||
if (!['http:', 'https:'].includes(url.protocol)) throw new Error();
|
||||
if (target.page.isClosed() || target.page.url() !== target.url) throw new Error();
|
||||
return url;
|
||||
} catch {
|
||||
throw new CookieImportError('The captured HTTP(S) target has changed or is unavailable. Reopen the picker on the intended page.', 'target_changed');
|
||||
}
|
||||
}
|
||||
|
||||
export async function runCookieImport(
|
||||
options: CookieImportOptions,
|
||||
target: CookieImportTarget,
|
||||
trackDomains: (domains: string[]) => void,
|
||||
authOptions: CookieAuthVerificationOptions = {},
|
||||
) {
|
||||
for (const value of [options.all, options.clearStorage, options.verifyAuth]) {
|
||||
if (value !== undefined && typeof value !== 'boolean') throw new CookieImportError('Cookie import options must be booleans.', 'bad_request');
|
||||
}
|
||||
if (typeof options.browser !== 'string' || !options.browser.trim()) throw new CookieImportError('Select a source browser.', 'bad_request');
|
||||
if (options.profile !== undefined && (typeof options.profile !== 'string' || !options.profile)) throw new CookieImportError('Invalid source profile.', 'bad_request');
|
||||
if (options.all && options.domains || options.all && options.clearStorage) throw new CookieImportError('All-domain import cannot be combined with a scoped domain or storage reset.', 'bad_request');
|
||||
if (!options.all && (!Array.isArray(options.domains) || !options.domains.length)) throw new CookieImportError('Select at least one cookie domain.', 'bad_request');
|
||||
const selected = options.domains?.map(normalizeCookieDomain) ?? [];
|
||||
const needsTarget = options.clearStorage || options.verifyAuth;
|
||||
const targetUrl = needsTarget ? validateCookieTarget(target) : undefined;
|
||||
if (options.clearStorage) validateCookieStorageSupport(target.page);
|
||||
if (options.verifyAuth) validateCookieAuthOptions(authOptions);
|
||||
if (targetUrl && selected.length && !selected.some(domain => cookieDomainMatches(targetUrl.hostname, '.' + domain))) {
|
||||
throw new CookieImportError('The selected cookies do not match the captured target origin.', 'target_mismatch');
|
||||
}
|
||||
const context = target.page.context();
|
||||
if (target.page.isClosed()) throw new CookieImportError('The captured target is closed.', 'target_closed');
|
||||
if (activeImports.has(context)) throw new CookieImportError('Another cookie import is still running. Wait before retrying.', 'import_busy', 'retry');
|
||||
activeImports.add(context);
|
||||
try {
|
||||
let profile = options.profile;
|
||||
if (!profile) {
|
||||
const suggestion = await getCookieProfiles(options.browser, selected);
|
||||
profile = suggestion.recommendedProfile;
|
||||
if (!profile) throw new CookieImportError('Choose a source profile explicitly; matching profiles are ambiguous, unavailable, or empty.', 'profile_required');
|
||||
}
|
||||
const domains = options.all
|
||||
? (await withCookieReadRetry(() => listDomains(options.browser, profile!))).domains.map(entry => entry.domain)
|
||||
: selected;
|
||||
let result = await withCookieReadRetry(() => importCookies(options.browser, domains, profile));
|
||||
if (result.count === 0 && result.failureReasons?.unsupported_encryption && process.platform === 'win32') {
|
||||
const failed = result.failed;
|
||||
result = await importCookiesViaCdp(options.browser, domains, profile);
|
||||
result.failed = Math.max(result.failed, failed - result.count);
|
||||
if (result.failed) result.failureReasons = { native_unrecovered: result.failed };
|
||||
}
|
||||
const receipt = {
|
||||
browser: options.browser,
|
||||
profile,
|
||||
imported: 0,
|
||||
failed: result.failed,
|
||||
domainCounts: {} as Record<string, number>,
|
||||
failureReasons: result.failureReasons ?? {},
|
||||
outcome: (result.failed ? 'failed' : 'empty') as 'empty' | 'imported' | 'partial' | 'failed',
|
||||
reset: 'not_requested' as 'not_requested' | 'cleared' | 'failed',
|
||||
verification: { verified: false, reason: 'not_requested' } as { verified: boolean; reason: string; status?: number },
|
||||
message: result.failed ? 'No cookies imported; cookies could not be decrypted.' : 'No matching cookies found.',
|
||||
};
|
||||
if (!result.count) {
|
||||
if (options.verifyAuth) receipt.verification.reason = 'no_cookies_imported';
|
||||
return receipt;
|
||||
}
|
||||
if (targetUrl && !result.cookies.some(cookie => cookieDomainMatches(targetUrl.hostname, cookie.domain))) {
|
||||
throw new CookieImportError('No imported cookies apply to the captured target origin.', 'target_mismatch');
|
||||
}
|
||||
if (target.page.isClosed()) throw new CookieImportError('The captured target is closed.', 'target_closed');
|
||||
if (needsTarget) validateCookieTarget(target);
|
||||
if (options.clearStorage) {
|
||||
try {
|
||||
await clearCookieTargetStorage(target.page, targetUrl!.origin);
|
||||
receipt.reset = 'cleared';
|
||||
} catch {
|
||||
receipt.outcome = 'failed';
|
||||
receipt.reset = 'failed';
|
||||
receipt.message = 'Storage reset did not complete; storage may be partially cleared. No new cookies were applied.';
|
||||
receipt.verification.reason = 'reset_failed';
|
||||
return receipt;
|
||||
}
|
||||
}
|
||||
const appliedDomains = [...new Set(result.cookies.map(cookie => cookie.domain))];
|
||||
try {
|
||||
await context.addCookies(result.cookies);
|
||||
} catch {
|
||||
trackDomains(appliedDomains);
|
||||
receipt.outcome = 'failed';
|
||||
receipt.message = 'Cookie application failed; the browser may contain a partial import. Authentication was not verified.';
|
||||
receipt.verification.reason = 'application_failed';
|
||||
return receipt;
|
||||
}
|
||||
trackDomains(appliedDomains);
|
||||
receipt.imported = result.count;
|
||||
receipt.domainCounts = result.domainCounts;
|
||||
receipt.outcome = result.failed ? 'partial' : 'imported';
|
||||
receipt.message = result.failed ? 'Some cookies could not be decrypted.' : 'Cookie copy complete.';
|
||||
if (options.verifyAuth) {
|
||||
try {
|
||||
validateCookieTarget(target);
|
||||
receipt.verification = await verifyCookieAuthentication(target.page, authOptions, targetUrl!.origin);
|
||||
} catch {
|
||||
receipt.verification = { verified: false, reason: 'target_changed' };
|
||||
}
|
||||
}
|
||||
return receipt;
|
||||
} finally {
|
||||
activeImports.delete(context);
|
||||
}
|
||||
}
|
||||
|
||||
export function formatCookieImportResult(result: Awaited<ReturnType<typeof runCookieImport>>): string {
|
||||
return `Imported ${result.imported} cookies from ${result.browser} (profile: ${result.profile}); ${result.failed} failed to decrypt. ${result.message} Storage reset: ${result.reset}. Authentication: ${result.verification.reason}.`;
|
||||
}
|
||||
Reference in new issue
Block a user