1111
1212from playwright .sync_api import BrowserContext , Page , Playwright , sync_playwright
1313
14- from sentience .models import ProxyConfig
14+ from sentience .models import ProxyConfig , StorageState
1515
1616# Import stealth for bot evasion (optional - graceful fallback if not available)
1717try :
@@ -31,6 +31,8 @@ def __init__(
3131 api_url : str | None = None ,
3232 headless : bool | None = None ,
3333 proxy : str | None = None ,
34+ user_data_dir : str | None = None ,
35+ storage_state : str | Path | StorageState | dict | None = None ,
3436 ):
3537 """
3638 Initialize Sentience browser
@@ -46,6 +48,15 @@ def __init__(
4648 proxy: Optional proxy server URL (e.g., 'http://user:pass@proxy.example.com:8080')
4749 Supports HTTP, HTTPS, and SOCKS5 proxies
4850 Falls back to SENTIENCE_PROXY environment variable if not provided
51+ user_data_dir: Optional path to user data directory for persistent sessions.
52+ If None, uses temporary directory (session not persisted).
53+ If provided, cookies and localStorage persist across browser restarts.
54+ storage_state: Optional storage state to inject (cookies + localStorage).
55+ Can be:
56+ - Path to JSON file (str or Path)
57+ - StorageState object
58+ - Dictionary with 'cookies' and/or 'origins' keys
59+ If provided, browser starts with pre-injected authentication.
4960 """
5061 self .api_key = api_key
5162 # Only set api_url if api_key is provided, otherwise None (free tier)
@@ -65,6 +76,10 @@ def __init__(
6576 # Support proxy from argument or environment variable
6677 self .proxy = proxy or os .environ .get ("SENTIENCE_PROXY" )
6778
79+ # Auth injection support
80+ self .user_data_dir = user_data_dir
81+ self .storage_state = storage_state
82+
6883 self .playwright : Playwright | None = None
6984 self .context : BrowserContext | None = None
7085 self .page : Page | None = None
@@ -170,9 +185,16 @@ def start(self) -> None:
170185 # Parse proxy configuration if provided
171186 proxy_config = self ._parse_proxy (self .proxy ) if self .proxy else None
172187
188+ # Handle User Data Directory (Persistence)
189+ if self .user_data_dir :
190+ user_data_dir = str (self .user_data_dir )
191+ Path (user_data_dir ).mkdir (parents = True , exist_ok = True )
192+ else :
193+ user_data_dir = "" # Ephemeral temp dir (existing behavior)
194+
173195 # Build launch_persistent_context parameters
174196 launch_params = {
175- "user_data_dir" : "" , # Ephemeral temp dir
197+ "user_data_dir" : user_data_dir ,
176198 "headless" : False , # IMPORTANT: See note above
177199 "args" : args ,
178200 "viewport" : {"width" : 1280 , "height" : 800 },
@@ -194,6 +216,10 @@ def start(self) -> None:
194216
195217 self .page = self .context .pages [0 ] if self .context .pages else self .context .new_page ()
196218
219+ # Inject storage state if provided (must be after context creation)
220+ if self .storage_state :
221+ self ._inject_storage_state (self .storage_state )
222+
197223 # Apply stealth if available
198224 if STEALTH_AVAILABLE :
199225 stealth_sync (self .page )
@@ -233,6 +259,92 @@ def goto(self, url: str) -> None:
233259 f"5. Diagnostic info: { diag } "
234260 )
235261
262+ def _inject_storage_state (
263+ self , storage_state : str | Path | StorageState | dict
264+ ) -> None : # noqa: C901
265+ """
266+ Inject storage state (cookies + localStorage) into browser context.
267+
268+ Args:
269+ storage_state: Path to JSON file, StorageState object, or dict containing storage state
270+ """
271+ import json
272+
273+ # Load storage state
274+ if isinstance (storage_state , (str , Path )):
275+ # Load from file
276+ with open (storage_state , encoding = "utf-8" ) as f :
277+ state_dict = json .load (f )
278+ state = StorageState .from_dict (state_dict )
279+ elif isinstance (storage_state , StorageState ):
280+ # Already a StorageState object
281+ state = storage_state
282+ elif isinstance (storage_state , dict ):
283+ # Dictionary format
284+ state = StorageState .from_dict (storage_state )
285+ else :
286+ raise ValueError (
287+ f"Invalid storage_state type: { type (storage_state )} . "
288+ "Expected str, Path, StorageState, or dict."
289+ )
290+
291+ # Inject cookies (works globally)
292+ if state .cookies :
293+ # Convert to Playwright cookie format
294+ playwright_cookies = []
295+ for cookie in state .cookies :
296+ cookie_dict = cookie .model_dump ()
297+ # Playwright expects lowercase keys for some fields
298+ playwright_cookie = {
299+ "name" : cookie_dict ["name" ],
300+ "value" : cookie_dict ["value" ],
301+ "domain" : cookie_dict ["domain" ],
302+ "path" : cookie_dict ["path" ],
303+ }
304+ if cookie_dict .get ("expires" ):
305+ playwright_cookie ["expires" ] = cookie_dict ["expires" ]
306+ if cookie_dict .get ("httpOnly" ):
307+ playwright_cookie ["httpOnly" ] = cookie_dict ["httpOnly" ]
308+ if cookie_dict .get ("secure" ):
309+ playwright_cookie ["secure" ] = cookie_dict ["secure" ]
310+ if cookie_dict .get ("sameSite" ):
311+ playwright_cookie ["sameSite" ] = cookie_dict ["sameSite" ]
312+ playwright_cookies .append (playwright_cookie )
313+
314+ self .context .add_cookies (playwright_cookies )
315+ print (f"✅ [Sentience] Injected { len (state .cookies )} cookie(s)" )
316+
317+ # Inject LocalStorage (requires navigation to each domain)
318+ if state .origins :
319+ for origin_data in state .origins :
320+ origin = origin_data .origin
321+ if not origin :
322+ continue
323+
324+ # Navigate to origin to set localStorage
325+ try :
326+ self .page .goto (origin , wait_until = "domcontentloaded" , timeout = 10000 )
327+
328+ # Inject localStorage
329+ if origin_data .localStorage :
330+ # Convert to dict format for JavaScript
331+ localStorage_dict = {
332+ item .name : item .value for item in origin_data .localStorage
333+ }
334+ self .page .evaluate (
335+ """(localStorage_data) => {
336+ for (const [key, value] of Object.entries(localStorage_data)) {
337+ localStorage.setItem(key, value);
338+ }
339+ }""" ,
340+ localStorage_dict ,
341+ )
342+ print (
343+ f"✅ [Sentience] Injected { len (origin_data .localStorage )} localStorage item(s) for { origin } "
344+ )
345+ except Exception as e :
346+ print (f"⚠️ [Sentience] Failed to inject localStorage for { origin } : { e } " )
347+
236348 def _wait_for_extension (self , timeout_sec : float = 5.0 ) -> bool :
237349 """Poll for window.sentience to be available"""
238350 start_time = time .time ()
0 commit comments