| import os |
| import json |
| from pathlib import Path |
| from playwright.async_api import Browser, BrowserContext |
|
|
| class KeychainBypassCookieInjector: |
| """ |
| Keychain-Bypass Cookie Injector for Playwright Python. |
| Bypasses macOS/Linux OS Keychain password popups and decryption errors |
| by launching a clean non-persistent context and injecting standard JSON storage state. |
| """ |
|
|
| DEFAULT_STATE_PATH = Path("/Users/vincenta/GoogleKwok022/Archon/enduser-ui-fe/.playwright/admin_storage_state.json") |
|
|
| @staticmethod |
| def get_state_path(custom_path: str = None) -> Path: |
| """Resolves the physical path of the storage state JSON file.""" |
| if custom_path: |
| p = Path(custom_path) |
| else: |
| p = KeychainBypassCookieInjector.DEFAULT_STATE_PATH |
| |
| |
| possible_paths = [ |
| p, |
| Path("./enduser-ui-fe/.playwright/admin_storage_state.json"), |
| Path("../enduser-ui-fe/.playwright/admin_storage_state.json"), |
| Path("/app/enduser-ui-fe/.playwright/admin_storage_state.json") |
| ] |
| |
| for path in possible_paths: |
| if path.exists(): |
| return path.resolve() |
| return p.resolve() |
|
|
| @classmethod |
| async def create_keychain_bypass_context( |
| cls, |
| playwright_instance, |
| headless: bool = True, |
| state_path: str = None, |
| **context_kwargs |
| ) -> tuple[Browser, BrowserContext]: |
| """ |
| Launches a standard chromium browser and injects the JSON state file. |
| Completely immune to system OS keychain encryption prompts. |
| """ |
| resolved_state = cls.get_state_path(state_path) |
| |
| print(f"🔑 [CookieInjector] Bypassing Keychain. Launching clean non-persistent context...") |
| |
| |
| browser_args = [ |
| '--no-sandbox', |
| '--disable-setuid-sandbox', |
| '--password-store=basic', |
| '--use-mock-keyring' |
| ] |
| |
| browser = await playwright_instance.chromium.launch( |
| headless=headless, |
| args=browser_args |
| ) |
| |
| |
| if resolved_state.exists(): |
| print(f"🍪 [CookieInjector] Injecting storage state from: {resolved_state}") |
| context = await browser.new_context( |
| storage_state=str(resolved_state), |
| **context_kwargs |
| ) |
| else: |
| print(f"⚠️ [CookieInjector] Storage state file not found at: {resolved_state}. Launching clean context.") |
| context = await browser.new_context(**context_kwargs) |
| |
| return browser, context |
|
|
| @classmethod |
| async def save_context_state(cls, context: BrowserContext, state_path: str = None): |
| """Saves current cookies, localStorage, and sessionStorage back to the JSON file.""" |
| resolved_state = cls.get_state_path(state_path) |
| resolved_state.parent.mkdir(parents=True, exist_ok=True) |
| |
| |
| state = await context.storage_state() |
| |
| with open(resolved_state, "w", encoding="utf-8") as f: |
| json.dump(state, f, indent=2, ensure_ascii=False) |
| |
| print(f"💾 [CookieInjector] Saved updated state to: {resolved_state}") |
|
|