cursorflow 1.2.0__py3-none-any.whl

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.
@@ -0,0 +1,433 @@
1
+ """
2
+ Authentication Handler
3
+
4
+ Universal authentication support with session persistence.
5
+ Handles form-based login, cookie auth, header auth without framework complexity.
6
+ """
7
+
8
+ import asyncio
9
+ import json
10
+ import time
11
+ from typing import Dict, List, Optional, Any
12
+ from pathlib import Path
13
+ import logging
14
+
15
+
16
+ class AuthHandler:
17
+ """
18
+ Universal authentication handler - works with any web technology
19
+
20
+ Supports multiple auth methods with session persistence for faster testing.
21
+ NO FRAMEWORK ASSUMPTIONS - pure universal patterns.
22
+ """
23
+
24
+ def __init__(self, auth_config: Dict):
25
+ """
26
+ Initialize authentication handler
27
+
28
+ Args:
29
+ auth_config: {
30
+ "method": "form|cookies|headers",
31
+ "username_selector": "#username",
32
+ "password_selector": "#password",
33
+ "submit_selector": "#login-button",
34
+ "username": "test_user",
35
+ "password": "test_pass",
36
+ "session_storage": "sessions/"
37
+ }
38
+ """
39
+ self.config = auth_config
40
+ self.method = auth_config.get("method", "form")
41
+ self.logger = logging.getLogger(__name__)
42
+
43
+ # Create session storage in user's project under .cursorflow
44
+ session_dir = auth_config.get("session_storage", ".cursorflow/sessions/")
45
+ if not Path(session_dir).is_absolute():
46
+ session_dir = Path.cwd() / session_dir
47
+ session_dir = Path(session_dir)
48
+ session_dir.mkdir(parents=True, exist_ok=True)
49
+ self.session_dir = session_dir
50
+
51
+ async def authenticate(
52
+ self,
53
+ page,
54
+ session_options: Optional[Dict] = None
55
+ ) -> bool:
56
+ """
57
+ Authenticate user with session management
58
+
59
+ Args:
60
+ page: Playwright page object
61
+ session_options: {
62
+ "reuse_session": True,
63
+ "save_session": True,
64
+ "fresh_session": False,
65
+ "session_name": "test_session"
66
+ }
67
+
68
+ Returns:
69
+ True if authentication successful, False otherwise
70
+ """
71
+ session_options = session_options or {}
72
+ session_name = session_options.get("session_name", "default")
73
+
74
+ try:
75
+ # Try to reuse existing session if requested
76
+ if (session_options.get("reuse_session", True) and
77
+ not session_options.get("fresh_session", False)):
78
+
79
+ if await self._restore_session(page, session_name):
80
+ self.logger.info("✅ Reused existing authentication session")
81
+ return True
82
+
83
+ # Perform fresh authentication
84
+ self.logger.info("🔐 Performing fresh authentication...")
85
+ success = await self._perform_authentication(page)
86
+
87
+ # Save session if requested and successful
88
+ if success and session_options.get("save_session", True):
89
+ await self._save_session(page, session_name)
90
+
91
+ return success
92
+
93
+ except Exception as e:
94
+ self.logger.error(f"Authentication failed: {e}")
95
+ return False
96
+
97
+ async def _restore_session(self, page, session_name: str) -> bool:
98
+ """Try to restore a saved session"""
99
+ try:
100
+ session_file = self.session_dir / f"{session_name}_session.json"
101
+
102
+ if not session_file.exists():
103
+ return False
104
+
105
+ with open(session_file, 'r') as f:
106
+ session_data = json.load(f)
107
+
108
+ # Restore cookies
109
+ if "cookies" in session_data:
110
+ await page.context.add_cookies(session_data["cookies"])
111
+
112
+ # Restore local storage
113
+ if "localStorage" in session_data:
114
+ await page.evaluate(f"""
115
+ Object.entries({json.dumps(session_data["localStorage"])}).forEach(([key, value]) => {{
116
+ localStorage.setItem(key, value);
117
+ }});
118
+ """)
119
+
120
+ # Restore session storage
121
+ if "sessionStorage" in session_data:
122
+ await page.evaluate(f"""
123
+ Object.entries({json.dumps(session_data["sessionStorage"])}).forEach(([key, value]) => {{
124
+ sessionStorage.setItem(key, value);
125
+ }});
126
+ """)
127
+
128
+ # Test if session is still valid by checking for auth indicators
129
+ is_valid = await self._validate_session(page)
130
+
131
+ if is_valid:
132
+ self.logger.info(f"Successfully restored session: {session_name}")
133
+ return True
134
+ else:
135
+ self.logger.info(f"Restored session {session_name} is no longer valid")
136
+ # Clean up invalid session
137
+ session_file.unlink(missing_ok=True)
138
+ return False
139
+
140
+ except Exception as e:
141
+ self.logger.warning(f"Session restoration failed: {e}")
142
+ return False
143
+
144
+ async def _save_session(self, page, session_name: str):
145
+ """Save current session for reuse"""
146
+ try:
147
+ session_file = self.session_dir / f"{session_name}_session.json"
148
+
149
+ # Get browser storage state
150
+ storage_state = await page.context.storage_state()
151
+
152
+ # Get local storage
153
+ local_storage = await page.evaluate("""
154
+ () => {
155
+ const storage = {};
156
+ for (let i = 0; i < localStorage.length; i++) {
157
+ const key = localStorage.key(i);
158
+ storage[key] = localStorage.getItem(key);
159
+ }
160
+ return storage;
161
+ }
162
+ """)
163
+
164
+ # Get session storage
165
+ session_storage = await page.evaluate("""
166
+ () => {
167
+ const storage = {};
168
+ for (let i = 0; i < sessionStorage.length; i++) {
169
+ const key = sessionStorage.key(i);
170
+ storage[key] = sessionStorage.getItem(key);
171
+ }
172
+ return storage;
173
+ }
174
+ """)
175
+
176
+ session_data = {
177
+ "timestamp": time.time(),
178
+ "method": self.method,
179
+ "cookies": storage_state.get("cookies", []),
180
+ "localStorage": local_storage,
181
+ "sessionStorage": session_storage,
182
+ "url": page.url
183
+ }
184
+
185
+ with open(session_file, 'w') as f:
186
+ json.dump(session_data, f, indent=2)
187
+
188
+ self.logger.info(f"💾 Session saved: {session_name}")
189
+
190
+ except Exception as e:
191
+ self.logger.warning(f"Session save failed: {e}")
192
+
193
+ async def _perform_authentication(self, page) -> bool:
194
+ """Perform authentication based on configured method"""
195
+
196
+ if self.method == "form":
197
+ return await self._form_authentication(page)
198
+ elif self.method == "cookies":
199
+ return await self._cookie_authentication(page)
200
+ elif self.method == "headers":
201
+ return await self._header_authentication(page)
202
+ else:
203
+ self.logger.error(f"Unsupported authentication method: {self.method}")
204
+ return False
205
+
206
+ async def _form_authentication(self, page) -> bool:
207
+ """Perform form-based authentication"""
208
+ try:
209
+ username = self.config.get("username")
210
+ password = self.config.get("password")
211
+ username_selector = self.config.get("username_selector", "#username")
212
+ password_selector = self.config.get("password_selector", "#password")
213
+ submit_selector = self.config.get("submit_selector", "#login-button")
214
+
215
+ if not username or not password:
216
+ self.logger.error("Username and password required for form authentication")
217
+ return False
218
+
219
+ # Wait for login form
220
+ await page.wait_for_selector(username_selector, timeout=10000)
221
+
222
+ # Fill username
223
+ await page.fill(username_selector, username)
224
+ self.logger.debug(f"Filled username: {username_selector}")
225
+
226
+ # Fill password
227
+ await page.fill(password_selector, password)
228
+ self.logger.debug(f"Filled password: {password_selector}")
229
+
230
+ # Submit form
231
+ await page.click(submit_selector)
232
+ self.logger.debug(f"Clicked submit: {submit_selector}")
233
+
234
+ # Wait for navigation or success indicator
235
+ try:
236
+ await page.wait_for_load_state("networkidle", timeout=10000)
237
+ except:
238
+ # If no navigation, wait a bit for any AJAX auth
239
+ await page.wait_for_timeout(3000)
240
+
241
+ # Validate authentication success
242
+ is_authenticated = await self._validate_authentication(page)
243
+
244
+ if is_authenticated:
245
+ self.logger.info("✅ Form authentication successful")
246
+ return True
247
+ else:
248
+ self.logger.warning("❌ Form authentication failed")
249
+ return False
250
+
251
+ except Exception as e:
252
+ self.logger.error(f"Form authentication error: {e}")
253
+ return False
254
+
255
+ async def _cookie_authentication(self, page) -> bool:
256
+ """Perform cookie-based authentication"""
257
+ try:
258
+ cookies = self.config.get("cookies", [])
259
+
260
+ if not cookies:
261
+ self.logger.error("No cookies provided for cookie authentication")
262
+ return False
263
+
264
+ # Add cookies to context
265
+ await page.context.add_cookies(cookies)
266
+ self.logger.info(f"Added {len(cookies)} authentication cookies")
267
+
268
+ # Refresh page to apply cookies
269
+ await page.reload()
270
+ await page.wait_for_load_state("networkidle")
271
+
272
+ # Validate authentication
273
+ is_authenticated = await self._validate_authentication(page)
274
+
275
+ if is_authenticated:
276
+ self.logger.info("✅ Cookie authentication successful")
277
+ return True
278
+ else:
279
+ self.logger.warning("❌ Cookie authentication failed")
280
+ return False
281
+
282
+ except Exception as e:
283
+ self.logger.error(f"Cookie authentication error: {e}")
284
+ return False
285
+
286
+ async def _header_authentication(self, page) -> bool:
287
+ """Perform header-based authentication"""
288
+ try:
289
+ headers = self.config.get("headers", {})
290
+
291
+ if not headers:
292
+ self.logger.error("No headers provided for header authentication")
293
+ return False
294
+
295
+ # Set extra HTTP headers
296
+ await page.set_extra_http_headers(headers)
297
+ self.logger.info(f"Set {len(headers)} authentication headers")
298
+
299
+ # Refresh page to apply headers
300
+ await page.reload()
301
+ await page.wait_for_load_state("networkidle")
302
+
303
+ # Validate authentication
304
+ is_authenticated = await self._validate_authentication(page)
305
+
306
+ if is_authenticated:
307
+ self.logger.info("✅ Header authentication successful")
308
+ return True
309
+ else:
310
+ self.logger.warning("❌ Header authentication failed")
311
+ return False
312
+
313
+ except Exception as e:
314
+ self.logger.error(f"Header authentication error: {e}")
315
+ return False
316
+
317
+ async def _validate_authentication(self, page) -> bool:
318
+ """Validate that authentication was successful"""
319
+ try:
320
+ # Universal authentication validation strategies
321
+
322
+ # Strategy 1: Check for common auth failure indicators
323
+ auth_failure_selectors = [
324
+ ".error", ".alert-danger", ".login-error",
325
+ "#error", "#login-error", "[data-testid='error']"
326
+ ]
327
+
328
+ for selector in auth_failure_selectors:
329
+ try:
330
+ error_element = await page.wait_for_selector(selector, timeout=1000)
331
+ if error_element:
332
+ error_text = await error_element.text_content()
333
+ if error_text and any(word in error_text.lower() for word in ["error", "invalid", "failed", "incorrect"]):
334
+ self.logger.warning(f"Authentication error detected: {error_text}")
335
+ return False
336
+ except:
337
+ continue
338
+
339
+ # Strategy 2: Check for common success indicators
340
+ success_indicators = self.config.get("success_indicators", [
341
+ "dashboard", "profile", "logout", "welcome", "user", "account"
342
+ ])
343
+
344
+ page_content = await page.content()
345
+ page_content_lower = page_content.lower()
346
+
347
+ success_count = sum(1 for indicator in success_indicators if indicator in page_content_lower)
348
+
349
+ # Strategy 3: Check URL changes
350
+ current_url = page.url
351
+ login_urls = ["/login", "/signin", "/auth"]
352
+
353
+ url_indicates_success = not any(login_url in current_url.lower() for login_url in login_urls)
354
+
355
+ # Strategy 4: Check for auth-specific elements
356
+ auth_selectors = self.config.get("auth_check_selectors", [])
357
+ auth_elements_found = 0
358
+
359
+ for selector in auth_selectors:
360
+ try:
361
+ element = await page.wait_for_selector(selector, timeout=1000)
362
+ if element:
363
+ auth_elements_found += 1
364
+ except:
365
+ continue
366
+
367
+ # Determine authentication success
368
+ is_authenticated = (
369
+ success_count >= 2 or # Multiple success indicators
370
+ url_indicates_success or # URL changed from login page
371
+ auth_elements_found > 0 # Found auth-specific elements
372
+ )
373
+
374
+ self.logger.debug(f"Auth validation - success_count: {success_count}, url_ok: {url_indicates_success}, auth_elements: {auth_elements_found}")
375
+
376
+ return is_authenticated
377
+
378
+ except Exception as e:
379
+ self.logger.error(f"Authentication validation error: {e}")
380
+ return False
381
+
382
+ async def _validate_session(self, page) -> bool:
383
+ """Validate that a restored session is still valid"""
384
+ # Use the same validation logic as fresh authentication
385
+ return await self._validate_authentication(page)
386
+
387
+ def get_session_info(self, session_name: str = "default") -> Optional[Dict]:
388
+ """Get information about a saved session"""
389
+ try:
390
+ session_file = self.session_dir / f"{session_name}_session.json"
391
+
392
+ if not session_file.exists():
393
+ return None
394
+
395
+ with open(session_file, 'r') as f:
396
+ session_data = json.load(f)
397
+
398
+ return {
399
+ "session_name": session_name,
400
+ "timestamp": session_data.get("timestamp"),
401
+ "method": session_data.get("method"),
402
+ "url": session_data.get("url"),
403
+ "cookies_count": len(session_data.get("cookies", [])),
404
+ "has_local_storage": bool(session_data.get("localStorage")),
405
+ "has_session_storage": bool(session_data.get("sessionStorage"))
406
+ }
407
+
408
+ except Exception as e:
409
+ self.logger.error(f"Session info retrieval failed: {e}")
410
+ return None
411
+
412
+ def clear_session(self, session_name: str = "default"):
413
+ """Clear a saved session"""
414
+ try:
415
+ session_file = self.session_dir / f"{session_name}_session.json"
416
+ session_file.unlink(missing_ok=True)
417
+ self.logger.info(f"Cleared session: {session_name}")
418
+
419
+ except Exception as e:
420
+ self.logger.error(f"Session clearing failed: {e}")
421
+
422
+ def list_sessions(self) -> List[str]:
423
+ """List all saved sessions"""
424
+ try:
425
+ sessions = []
426
+ for session_file in self.session_dir.glob("*_session.json"):
427
+ session_name = session_file.stem.replace("_session", "")
428
+ sessions.append(session_name)
429
+ return sessions
430
+
431
+ except Exception as e:
432
+ self.logger.error(f"Session listing failed: {e}")
433
+ return []