texas-grocery-mcp 0.1.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.
- texas_grocery_mcp/__init__.py +3 -0
- texas_grocery_mcp/auth/__init__.py +5 -0
- texas_grocery_mcp/auth/browser_refresh.py +1629 -0
- texas_grocery_mcp/auth/credentials.py +337 -0
- texas_grocery_mcp/auth/session.py +767 -0
- texas_grocery_mcp/clients/__init__.py +5 -0
- texas_grocery_mcp/clients/graphql.py +2400 -0
- texas_grocery_mcp/models/__init__.py +54 -0
- texas_grocery_mcp/models/cart.py +60 -0
- texas_grocery_mcp/models/coupon.py +44 -0
- texas_grocery_mcp/models/errors.py +43 -0
- texas_grocery_mcp/models/health.py +41 -0
- texas_grocery_mcp/models/product.py +274 -0
- texas_grocery_mcp/models/store.py +77 -0
- texas_grocery_mcp/observability/__init__.py +6 -0
- texas_grocery_mcp/observability/health.py +141 -0
- texas_grocery_mcp/observability/logging.py +73 -0
- texas_grocery_mcp/reliability/__init__.py +23 -0
- texas_grocery_mcp/reliability/cache.py +116 -0
- texas_grocery_mcp/reliability/circuit_breaker.py +138 -0
- texas_grocery_mcp/reliability/retry.py +96 -0
- texas_grocery_mcp/reliability/throttle.py +113 -0
- texas_grocery_mcp/server.py +211 -0
- texas_grocery_mcp/services/__init__.py +5 -0
- texas_grocery_mcp/services/geocoding.py +227 -0
- texas_grocery_mcp/state.py +166 -0
- texas_grocery_mcp/tools/__init__.py +5 -0
- texas_grocery_mcp/tools/cart.py +821 -0
- texas_grocery_mcp/tools/coupon.py +381 -0
- texas_grocery_mcp/tools/product.py +437 -0
- texas_grocery_mcp/tools/session.py +486 -0
- texas_grocery_mcp/tools/store.py +353 -0
- texas_grocery_mcp/utils/__init__.py +5 -0
- texas_grocery_mcp/utils/config.py +146 -0
- texas_grocery_mcp/utils/secure_file.py +123 -0
- texas_grocery_mcp-0.1.0.dist-info/METADATA +296 -0
- texas_grocery_mcp-0.1.0.dist-info/RECORD +40 -0
- texas_grocery_mcp-0.1.0.dist-info/WHEEL +4 -0
- texas_grocery_mcp-0.1.0.dist-info/entry_points.txt +2 -0
- texas_grocery_mcp-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
"""Session management tools for MCP."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from texas_grocery_mcp.auth.browser_refresh import (
|
|
9
|
+
BrowserRefreshError,
|
|
10
|
+
LoginRequiredError,
|
|
11
|
+
PlaywrightNotInstalledError,
|
|
12
|
+
auto_login_with_credentials,
|
|
13
|
+
is_playwright_available,
|
|
14
|
+
refresh_session_with_browser,
|
|
15
|
+
)
|
|
16
|
+
from texas_grocery_mcp.auth.credentials import CredentialError, CredentialStore
|
|
17
|
+
from texas_grocery_mcp.auth.session import (
|
|
18
|
+
check_session_freshness,
|
|
19
|
+
get_session_info,
|
|
20
|
+
get_session_status,
|
|
21
|
+
is_authenticated,
|
|
22
|
+
)
|
|
23
|
+
from texas_grocery_mcp.utils.config import get_settings
|
|
24
|
+
|
|
25
|
+
logger = structlog.get_logger()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def session_status() -> dict[str, Any]:
|
|
29
|
+
"""Get current session status including token lifecycle and credential storage.
|
|
30
|
+
|
|
31
|
+
Returns comprehensive session information:
|
|
32
|
+
- authenticated: Whether session is valid
|
|
33
|
+
- needs_refresh: Whether refresh is required now (token expired)
|
|
34
|
+
- refresh_recommended: Whether proactive refresh is advised (< 4 hours remaining)
|
|
35
|
+
- time_remaining_hours: Hours until token expires
|
|
36
|
+
- expires_at: ISO timestamp of expiration
|
|
37
|
+
- message: Human-readable status
|
|
38
|
+
- credentials_stored: Whether HEB credentials are saved for auto-login
|
|
39
|
+
|
|
40
|
+
Use this to check session health before operations or to decide
|
|
41
|
+
when to proactively refresh.
|
|
42
|
+
"""
|
|
43
|
+
status = get_session_status()
|
|
44
|
+
|
|
45
|
+
# Also include basic session info
|
|
46
|
+
basic_info = get_session_info()
|
|
47
|
+
|
|
48
|
+
# Check credential storage status
|
|
49
|
+
settings = get_settings()
|
|
50
|
+
auth_dir = Path(settings.auth_state_path).expanduser().parent
|
|
51
|
+
cred_store = CredentialStore(auth_dir)
|
|
52
|
+
cred_info = cred_store.get_storage_info()
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
# Lifecycle status (new fields)
|
|
56
|
+
"authenticated": status["authenticated"],
|
|
57
|
+
"needs_refresh": status["needs_refresh"],
|
|
58
|
+
"refresh_recommended": status["refresh_recommended"],
|
|
59
|
+
"time_remaining_hours": status["time_remaining_hours"],
|
|
60
|
+
"expires_at": status["expires_at"],
|
|
61
|
+
"reese84_present": status["reese84_present"],
|
|
62
|
+
"message": status["message"],
|
|
63
|
+
# Basic info
|
|
64
|
+
"auth_path": basic_info.get("auth_path"),
|
|
65
|
+
"store_id": basic_info.get("store_id"),
|
|
66
|
+
"user_id": basic_info.get("user_id"),
|
|
67
|
+
"cookies_count": basic_info.get("cookies_count", 0),
|
|
68
|
+
# Credential storage info
|
|
69
|
+
"credentials_stored": cred_info["credentials_stored"],
|
|
70
|
+
"credential_storage_method": cred_info["storage_method"],
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
async def session_refresh(
|
|
75
|
+
headless: bool = True,
|
|
76
|
+
timeout: int = 30000,
|
|
77
|
+
login_timeout: int = 300000,
|
|
78
|
+
use_saved_credentials: bool = True,
|
|
79
|
+
) -> dict[str, Any]:
|
|
80
|
+
"""Refresh HEB session cookies and tokens.
|
|
81
|
+
|
|
82
|
+
Uses embedded browser when available (fast: ~10-15 seconds).
|
|
83
|
+
If credentials are saved and login is required, attempts automatic login.
|
|
84
|
+
Falls back to returning Playwright MCP commands if browser
|
|
85
|
+
dependencies aren't installed.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
headless: Run browser without visible window (default True).
|
|
89
|
+
Set to False if you need to complete a manual login
|
|
90
|
+
(e.g., when your session has fully expired).
|
|
91
|
+
timeout: Maximum time to wait for page load in milliseconds.
|
|
92
|
+
Default 30000 (30 seconds).
|
|
93
|
+
login_timeout: Maximum time to wait for manual login in milliseconds.
|
|
94
|
+
Default 300000 (5 minutes). Only used when headless=False.
|
|
95
|
+
use_saved_credentials: If True and credentials are stored, attempt
|
|
96
|
+
automatic login when session is expired.
|
|
97
|
+
Default True.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
dict with one of these statuses:
|
|
101
|
+
- {"status": "success", ...} - Login/refresh completed successfully
|
|
102
|
+
- {"status": "human_action_required", "action": "login" | "captcha" | "2fa" | "waf", ...}
|
|
103
|
+
Human intervention required (login form, CAPTCHA, 2FA, or a WAF/security interstitial).
|
|
104
|
+
The browser remains open; complete the action, then call session_refresh() again.
|
|
105
|
+
- {"status": "failed", ...} - Login/refresh failed with error details
|
|
106
|
+
|
|
107
|
+
Use this tool when:
|
|
108
|
+
- session_status shows needs_refresh: true
|
|
109
|
+
- session_status shows refresh_recommended: true
|
|
110
|
+
- product_search returns security_challenge_detected: true
|
|
111
|
+
- You want to proactively refresh before token expires
|
|
112
|
+
|
|
113
|
+
CAPTCHA/2FA handling:
|
|
114
|
+
- When CAPTCHA or 2FA is detected, returns immediately with screenshot_path
|
|
115
|
+
- The screenshot shows exactly what the user sees in the browser
|
|
116
|
+
- Use the Read tool to view the screenshot and describe it to the user
|
|
117
|
+
- The browser stays open - user solves CAPTCHA/enters code in that window
|
|
118
|
+
- After solving, call session_refresh() again to continue the login flow
|
|
119
|
+
- Repeat until status is "success" or "failed"
|
|
120
|
+
"""
|
|
121
|
+
settings = get_settings()
|
|
122
|
+
auth_path = Path(settings.auth_state_path).expanduser()
|
|
123
|
+
auth_dir = auth_path.parent
|
|
124
|
+
|
|
125
|
+
# Check for saved credentials
|
|
126
|
+
cred_store = CredentialStore(auth_dir)
|
|
127
|
+
has_credentials = cred_store.has_credentials() if use_saved_credentials else False
|
|
128
|
+
|
|
129
|
+
# Try embedded Playwright first (fast path)
|
|
130
|
+
if is_playwright_available():
|
|
131
|
+
try:
|
|
132
|
+
result = await refresh_session_with_browser(
|
|
133
|
+
auth_path=auth_path,
|
|
134
|
+
headless=headless,
|
|
135
|
+
timeout=timeout,
|
|
136
|
+
login_timeout=login_timeout,
|
|
137
|
+
)
|
|
138
|
+
return result
|
|
139
|
+
|
|
140
|
+
except PlaywrightNotInstalledError:
|
|
141
|
+
# Fall through to command-based approach
|
|
142
|
+
pass
|
|
143
|
+
|
|
144
|
+
except LoginRequiredError as e:
|
|
145
|
+
# Session expired - try auto-login if we have credentials
|
|
146
|
+
if has_credentials:
|
|
147
|
+
logger.info("Session expired, attempting auto-login with saved credentials")
|
|
148
|
+
credentials = cred_store.get()
|
|
149
|
+
if credentials:
|
|
150
|
+
email, password = credentials
|
|
151
|
+
# Use visible browser for auto-login (needed for CAPTCHA handoff)
|
|
152
|
+
result = await auto_login_with_credentials(
|
|
153
|
+
auth_path=auth_path,
|
|
154
|
+
email=email,
|
|
155
|
+
password=password,
|
|
156
|
+
headless=False, # Always visible for human handoff
|
|
157
|
+
timeout=timeout,
|
|
158
|
+
login_timeout=login_timeout,
|
|
159
|
+
)
|
|
160
|
+
return result
|
|
161
|
+
|
|
162
|
+
# No credentials or auto-login not attempted
|
|
163
|
+
suggestion = (
|
|
164
|
+
"Your session has fully expired. Try:\n"
|
|
165
|
+
" session_refresh(headless=False)\n"
|
|
166
|
+
"to login in a visible browser window."
|
|
167
|
+
)
|
|
168
|
+
if not has_credentials:
|
|
169
|
+
suggestion += (
|
|
170
|
+
"\n\nTip: Save your credentials with session_save_credentials() "
|
|
171
|
+
"for automatic login next time."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"success": False,
|
|
176
|
+
"status": "failed",
|
|
177
|
+
"error": str(e),
|
|
178
|
+
"error_type": "login_required",
|
|
179
|
+
"credentials_available": has_credentials,
|
|
180
|
+
"suggestion": suggestion,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
except BrowserRefreshError as e:
|
|
184
|
+
return {
|
|
185
|
+
"success": False,
|
|
186
|
+
"status": "failed",
|
|
187
|
+
"error": str(e),
|
|
188
|
+
"error_type": "browser_error",
|
|
189
|
+
"suggestion": "Check your internet connection and try again.",
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
# Fallback: return Playwright MCP commands for external execution
|
|
193
|
+
expanded_auth_path = str(auth_path)
|
|
194
|
+
freshness = check_session_freshness()
|
|
195
|
+
|
|
196
|
+
# Build the JavaScript code to extract and save session
|
|
197
|
+
extract_code = f"""
|
|
198
|
+
// Extract session data and save to auth.json
|
|
199
|
+
const fs = require('fs');
|
|
200
|
+
const path = require('path');
|
|
201
|
+
|
|
202
|
+
// Get cookies
|
|
203
|
+
const cookies = await page.context().cookies();
|
|
204
|
+
|
|
205
|
+
// Get localStorage
|
|
206
|
+
const localStorage = await page.evaluate(() => {{
|
|
207
|
+
const items = [];
|
|
208
|
+
for (let i = 0; i < window.localStorage.length; i++) {{
|
|
209
|
+
const name = window.localStorage.key(i);
|
|
210
|
+
const value = window.localStorage.getItem(name);
|
|
211
|
+
items.push({{ name, value }});
|
|
212
|
+
}}
|
|
213
|
+
return items;
|
|
214
|
+
}});
|
|
215
|
+
|
|
216
|
+
// Build auth state matching Playwright's storageState format
|
|
217
|
+
const authState = {{
|
|
218
|
+
cookies: cookies,
|
|
219
|
+
origins: [{{
|
|
220
|
+
origin: "https://www.heb.com",
|
|
221
|
+
localStorage: localStorage
|
|
222
|
+
}}]
|
|
223
|
+
}};
|
|
224
|
+
|
|
225
|
+
// Ensure directory exists
|
|
226
|
+
const authPath = '{expanded_auth_path}';
|
|
227
|
+
const dir = path.dirname(authPath);
|
|
228
|
+
if (!fs.existsSync(dir)) {{
|
|
229
|
+
fs.mkdirSync(dir, {{ recursive: true }});
|
|
230
|
+
}}
|
|
231
|
+
|
|
232
|
+
// Save auth state
|
|
233
|
+
fs.writeFileSync(authPath, JSON.stringify(authState, null, 2));
|
|
234
|
+
|
|
235
|
+
return {{
|
|
236
|
+
success: true,
|
|
237
|
+
message: 'Session saved to ' + authPath,
|
|
238
|
+
cookies_count: cookies.length,
|
|
239
|
+
localStorage_count: localStorage.length
|
|
240
|
+
}};
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
return {
|
|
244
|
+
"message": (
|
|
245
|
+
"Playwright not installed. Execute these Playwright MCP commands to refresh "
|
|
246
|
+
"session:"
|
|
247
|
+
),
|
|
248
|
+
"install_for_fast_refresh": (
|
|
249
|
+
"pip install texas-grocery-mcp[browser] && playwright install chromium"
|
|
250
|
+
),
|
|
251
|
+
"current_status": {
|
|
252
|
+
"authenticated": freshness.get("authenticated", False),
|
|
253
|
+
"needs_refresh": freshness.get("needs_refresh", True),
|
|
254
|
+
"reason": freshness.get("reason"),
|
|
255
|
+
},
|
|
256
|
+
"commands": [
|
|
257
|
+
{
|
|
258
|
+
"tool": "browser_navigate",
|
|
259
|
+
"parameters": {"url": "https://www.heb.com"},
|
|
260
|
+
"description": "Navigate to HEB homepage to trigger reese84 token generation",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"tool": "browser_wait_for",
|
|
264
|
+
"parameters": {"time": 5},
|
|
265
|
+
"description": "Wait for page load and reese84 token initialization",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
"tool": "browser_run_code",
|
|
269
|
+
"parameters": {"code": extract_code},
|
|
270
|
+
"description": "Extract cookies and localStorage, save to auth.json",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
"after_refresh": "Call session_status to verify the refresh succeeded.",
|
|
274
|
+
"auth_path": expanded_auth_path,
|
|
275
|
+
"troubleshooting": {
|
|
276
|
+
"no_playwright": "Install Playwright MCP: https://github.com/microsoft/playwright-mcp",
|
|
277
|
+
"still_failing": (
|
|
278
|
+
"Try browser_navigate with a longer wait, or check if HEB requires login"
|
|
279
|
+
),
|
|
280
|
+
"login_required": (
|
|
281
|
+
"If HEB prompts for login, use browser_fill_form to enter credentials or "
|
|
282
|
+
"complete login manually"
|
|
283
|
+
),
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def session_save_instructions() -> dict[str, Any]:
|
|
289
|
+
"""Get instructions for saving browser session cookies.
|
|
290
|
+
|
|
291
|
+
Call this to get step-by-step instructions for authenticating
|
|
292
|
+
via Playwright MCP and saving the session for fast API access.
|
|
293
|
+
|
|
294
|
+
For automatic session extraction, use session_refresh instead.
|
|
295
|
+
"""
|
|
296
|
+
settings = get_settings()
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
"instructions": [
|
|
300
|
+
"1. Navigate to HEB login page:",
|
|
301
|
+
" browser_navigate('https://www.heb.com/my-account/login')",
|
|
302
|
+
"",
|
|
303
|
+
"2. Complete the login process in the browser",
|
|
304
|
+
" (Enter credentials and click Sign In)",
|
|
305
|
+
"",
|
|
306
|
+
"3. After successful login, save the browser state:",
|
|
307
|
+
" browser_run_code with this code:",
|
|
308
|
+
f" await page.context().storageState({{ path: '{settings.auth_state_path}' }})",
|
|
309
|
+
"",
|
|
310
|
+
"4. Verify session was saved:",
|
|
311
|
+
" Call session_status to confirm authentication",
|
|
312
|
+
],
|
|
313
|
+
"auth_path": str(settings.auth_state_path),
|
|
314
|
+
"current_status": {
|
|
315
|
+
"authenticated": is_authenticated(),
|
|
316
|
+
},
|
|
317
|
+
"alternative": "Use session_refresh for automatic session extraction without manual login.",
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def session_clear() -> dict[str, Any]:
|
|
322
|
+
"""Clear saved session cookies.
|
|
323
|
+
|
|
324
|
+
Use this to log out or clear invalid session data.
|
|
325
|
+
After clearing, you will need to run session_refresh again.
|
|
326
|
+
|
|
327
|
+
Note: This does NOT clear saved credentials. Use session_clear_credentials()
|
|
328
|
+
to remove stored login credentials.
|
|
329
|
+
"""
|
|
330
|
+
settings = get_settings()
|
|
331
|
+
auth_path = settings.auth_state_path
|
|
332
|
+
|
|
333
|
+
if not auth_path.exists():
|
|
334
|
+
return {
|
|
335
|
+
"success": True,
|
|
336
|
+
"message": "No session file to clear.",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
try:
|
|
340
|
+
auth_path.unlink()
|
|
341
|
+
return {
|
|
342
|
+
"success": True,
|
|
343
|
+
"message": "Session cleared. Run session_refresh to re-authenticate.",
|
|
344
|
+
"cleared_path": str(auth_path),
|
|
345
|
+
}
|
|
346
|
+
except OSError as e:
|
|
347
|
+
return {
|
|
348
|
+
"error": True,
|
|
349
|
+
"code": "CLEAR_FAILED",
|
|
350
|
+
"message": f"Failed to clear session: {e!s}",
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
async def session_save_credentials(email: str, password: str) -> dict[str, Any]:
|
|
355
|
+
"""Save HEB login credentials for automatic login.
|
|
356
|
+
|
|
357
|
+
Credentials are stored securely using:
|
|
358
|
+
- OS keyring (macOS Keychain, Windows Credential Manager, Linux Secret Service)
|
|
359
|
+
- Encrypted file fallback when keyring is unavailable
|
|
360
|
+
|
|
361
|
+
After saving, session_refresh will automatically use these credentials
|
|
362
|
+
when your session expires, eliminating manual browser login.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
email: Your HEB.com account email address
|
|
366
|
+
password: Your HEB.com account password
|
|
367
|
+
|
|
368
|
+
Returns:
|
|
369
|
+
dict with success status and storage method used
|
|
370
|
+
|
|
371
|
+
Security notes:
|
|
372
|
+
- Credentials are encrypted at rest
|
|
373
|
+
- Password is never logged or exposed in output
|
|
374
|
+
- Use session_clear_credentials() to remove stored credentials
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
session_save_credentials("user@example.com", "mypassword")
|
|
378
|
+
# Now session_refresh will auto-login when session expires
|
|
379
|
+
"""
|
|
380
|
+
if not email or not password:
|
|
381
|
+
return {
|
|
382
|
+
"success": False,
|
|
383
|
+
"error": "Email and password are required",
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
# Basic email validation
|
|
387
|
+
if "@" not in email or "." not in email:
|
|
388
|
+
return {
|
|
389
|
+
"success": False,
|
|
390
|
+
"error": "Invalid email format",
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
settings = get_settings()
|
|
394
|
+
auth_dir = Path(settings.auth_state_path).expanduser().parent
|
|
395
|
+
cred_store = CredentialStore(auth_dir)
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
result = cred_store.save(email, password)
|
|
399
|
+
storage_info = cred_store.get_storage_info()
|
|
400
|
+
|
|
401
|
+
# Mask email for response
|
|
402
|
+
masked_email = _mask_email(email)
|
|
403
|
+
|
|
404
|
+
logger.info("Credentials saved successfully", email_masked=masked_email)
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
"success": True,
|
|
408
|
+
"message": f"Credentials saved for {masked_email}",
|
|
409
|
+
"storage_method": result.get("method", "unknown"),
|
|
410
|
+
"storage_backend": storage_info.get("storage_backend", "unknown"),
|
|
411
|
+
"next_steps": (
|
|
412
|
+
"Your credentials are now saved. When your session expires, "
|
|
413
|
+
"session_refresh will automatically log you in. "
|
|
414
|
+
"If CAPTCHA or 2FA is required, you'll be prompted to complete it."
|
|
415
|
+
),
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
except CredentialError as e:
|
|
419
|
+
logger.error("Failed to save credentials", error=str(e))
|
|
420
|
+
return {
|
|
421
|
+
"success": False,
|
|
422
|
+
"error": str(e),
|
|
423
|
+
"suggestion": "Check that your system supports secure credential storage.",
|
|
424
|
+
}
|
|
425
|
+
except Exception as e:
|
|
426
|
+
logger.error("Unexpected error saving credentials", error=str(e))
|
|
427
|
+
return {
|
|
428
|
+
"success": False,
|
|
429
|
+
"error": f"Failed to save credentials: {e}",
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def session_clear_credentials() -> dict[str, Any]:
|
|
434
|
+
"""Remove stored HEB login credentials.
|
|
435
|
+
|
|
436
|
+
After clearing, session_refresh will fall back to manual browser login
|
|
437
|
+
when your session expires.
|
|
438
|
+
|
|
439
|
+
Returns:
|
|
440
|
+
dict with success status
|
|
441
|
+
|
|
442
|
+
Note: This does NOT clear your current session. Use session_clear()
|
|
443
|
+
to remove session cookies.
|
|
444
|
+
"""
|
|
445
|
+
settings = get_settings()
|
|
446
|
+
auth_dir = Path(settings.auth_state_path).expanduser().parent
|
|
447
|
+
cred_store = CredentialStore(auth_dir)
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
had_credentials = cred_store.has_credentials()
|
|
451
|
+
cred_store.clear()
|
|
452
|
+
|
|
453
|
+
if had_credentials:
|
|
454
|
+
logger.info("Credentials cleared successfully")
|
|
455
|
+
return {
|
|
456
|
+
"success": True,
|
|
457
|
+
"message": "Credentials cleared. Auto-login is now disabled.",
|
|
458
|
+
"had_credentials": True,
|
|
459
|
+
}
|
|
460
|
+
else:
|
|
461
|
+
return {
|
|
462
|
+
"success": True,
|
|
463
|
+
"message": "No credentials were stored.",
|
|
464
|
+
"had_credentials": False,
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.error("Failed to clear credentials", error=str(e))
|
|
469
|
+
return {
|
|
470
|
+
"success": False,
|
|
471
|
+
"error": f"Failed to clear credentials: {e}",
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _mask_email(email: str) -> str:
|
|
476
|
+
"""Mask email for safe display (e.g., u***r@example.com)."""
|
|
477
|
+
if not email or "@" not in email:
|
|
478
|
+
return "***"
|
|
479
|
+
|
|
480
|
+
local, domain = email.split("@", 1)
|
|
481
|
+
if len(local) <= 2:
|
|
482
|
+
masked_local = "*" * len(local)
|
|
483
|
+
else:
|
|
484
|
+
masked_local = local[0] + "*" * (len(local) - 2) + local[-1]
|
|
485
|
+
|
|
486
|
+
return f"{masked_local}@{domain}"
|