Skip to content

Commit fa10ca2

Browse files
committed
Harden OAuth refresh and collapse duplicated auth plumbing
Always refresh on 401, cache tokens in memory, run async auth off the event loop, and stop deleting sessions or locks owned by another process.
1 parent a172a16 commit fa10ca2

12 files changed

Lines changed: 633 additions & 466 deletions

File tree

hyperbrowser/client/base.py

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
from dataclasses import replace
22
from typing import Optional
33

4-
from ..config import ClientConfig, _env_positive_int
5-
from ..control_auth import DEFAULT_BASE_URL, resolve_control_plane_config
4+
from ..config import ClientConfig
5+
from ..control_auth import resolve_control_plane_config
66
from ..transport.base import TransportStrategy
7-
import os
87

98

109
class HyperbrowserBase:
@@ -20,31 +19,11 @@ def __init__(
2019
profile: Optional[str] = None,
2120
):
2221
if config is None:
23-
config = ClientConfig(
24-
api_key=(
25-
api_key
26-
if api_key is not None
27-
else os.environ.get("HYPERBROWSER_API_KEY")
28-
),
29-
base_url=(
30-
base_url
31-
if base_url is not None
32-
else os.environ.get("HYPERBROWSER_BASE_URL", DEFAULT_BASE_URL)
33-
),
22+
config = ClientConfig.from_constructor(
23+
api_key=api_key,
24+
base_url=base_url,
3425
runtime_proxy_override=runtime_proxy_override,
35-
profile=(
36-
profile
37-
if profile is not None
38-
else os.environ.get("HYPERBROWSER_PROFILE")
39-
),
40-
frontend_url=os.environ.get("HYPERBROWSER_FRONTEND_URL"),
41-
auth_lock_timeout_ms=_env_positive_int(
42-
"HYPERBROWSER_AUTH_LOCK_TIMEOUT_MS"
43-
),
44-
auth_lock_poll_interval_ms=_env_positive_int(
45-
"HYPERBROWSER_AUTH_LOCK_POLL_INTERVAL_MS"
46-
),
47-
auth_lock_stale_ms=_env_positive_int("HYPERBROWSER_AUTH_LOCK_STALE_MS"),
26+
profile=profile,
4827
)
4928

5029
resolved_base_url, auth = resolve_control_plane_config(config)

hyperbrowser/client/managers/async_manager/session.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import warnings
22
from collections.abc import Mapping
3+
from pathlib import Path
34
from typing import IO, List, Optional, Union, overload
45

56
from hyperbrowser.client._request import coerce_request, dump_request
@@ -42,6 +43,17 @@
4243
CAPTCHA_EVALUATION_REQUEST_TIMEOUT_SECONDS = 185
4344

4445

46+
def _replayable_upload_files(file_path: str):
47+
path = Path(file_path)
48+
return {
49+
"file": (
50+
path.name or "upload.bin",
51+
path.read_bytes(),
52+
"application/octet-stream",
53+
)
54+
}
55+
56+
4557
class SessionEventLogsManager:
4658
def __init__(self, client):
4759
self._client = client
@@ -163,21 +175,14 @@ async def get_downloads_url(self, id: str) -> GetSessionDownloadsUrlResponse:
163175
async def upload_file(
164176
self, id: str, file_input: Union[str, IO]
165177
) -> UploadFileResponse:
166-
response = None
167178
if isinstance(file_input, str):
168-
with open(file_input, "rb") as file_obj:
169-
files = {"file": file_obj}
170-
response = await self._client.transport.post(
171-
self._client._build_url(f"/session/{id}/uploads"),
172-
files=files,
173-
)
179+
files = _replayable_upload_files(file_input)
174180
else:
175181
files = {"file": file_input}
176-
response = await self._client.transport.post(
177-
self._client._build_url(f"/session/{id}/uploads"),
178-
files=files,
179-
)
180-
182+
response = await self._client.transport.post(
183+
self._client._build_url(f"/session/{id}/uploads"),
184+
files=files,
185+
)
181186
return UploadFileResponse(**response.data)
182187

183188
async def extend_session(self, id: str, duration_minutes: int) -> BasicResponse:

hyperbrowser/client/managers/sync_manager/session.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import warnings
22
from collections.abc import Mapping
3+
from pathlib import Path
34
from typing import IO, List, Optional, Union, overload
45

56
from hyperbrowser.client._request import coerce_request, dump_request
@@ -42,6 +43,17 @@
4243
CAPTCHA_EVALUATION_REQUEST_TIMEOUT_SECONDS = 185
4344

4445

46+
def _replayable_upload_files(file_path: str):
47+
path = Path(file_path)
48+
return {
49+
"file": (
50+
path.name or "upload.bin",
51+
path.read_bytes(),
52+
"application/octet-stream",
53+
)
54+
}
55+
56+
4557
class SessionEventLogsManager:
4658
def __init__(self, client):
4759
self._client = client
@@ -159,21 +171,14 @@ def get_downloads_url(self, id: str) -> GetSessionDownloadsUrlResponse:
159171
return GetSessionDownloadsUrlResponse(**response.data)
160172

161173
def upload_file(self, id: str, file_input: Union[str, IO]) -> UploadFileResponse:
162-
response = None
163174
if isinstance(file_input, str):
164-
with open(file_input, "rb") as file_obj:
165-
files = {"file": file_obj}
166-
response = self._client.transport.post(
167-
self._client._build_url(f"/session/{id}/uploads"),
168-
files=files,
169-
)
175+
files = _replayable_upload_files(file_input)
170176
else:
171177
files = {"file": file_input}
172-
response = self._client.transport.post(
173-
self._client._build_url(f"/session/{id}/uploads"),
174-
files=files,
175-
)
176-
178+
response = self._client.transport.post(
179+
self._client._build_url(f"/session/{id}/uploads"),
180+
files=files,
181+
)
177182
return UploadFileResponse(**response.data)
178183

179184
def extend_session(self, id: str, duration_minutes: int) -> BasicResponse:

hyperbrowser/config.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@
22
from typing import Optional
33
import os
44

5+
DEFAULT_BASE_URL = "https://api.hyperbrowser.ai"
6+
DEFAULT_FRONTEND_BASE_URL = "https://app.hyperbrowser.ai"
7+
8+
9+
def _env_text(name: str) -> Optional[str]:
10+
value = os.environ.get(name)
11+
if value is None:
12+
return None
13+
stripped = value.strip()
14+
return stripped or None
15+
516

617
def _env_positive_int(name: str) -> Optional[int]:
718
raw = os.environ.get(name)
@@ -19,7 +30,7 @@ class ClientConfig:
1930
"""Configuration for the Hyperbrowser client"""
2031

2132
api_key: Optional[str] = None
22-
base_url: str = "https://api.hyperbrowser.ai"
33+
base_url: str = DEFAULT_BASE_URL
2334
runtime_proxy_override: Optional[str] = None
2435
profile: Optional[str] = None
2536
frontend_url: Optional[str] = None
@@ -28,21 +39,38 @@ class ClientConfig:
2839
auth_lock_stale_ms: Optional[int] = None
2940

3041
@classmethod
31-
def from_env(cls) -> "ClientConfig":
32-
api_key = os.environ.get("HYPERBROWSER_API_KEY")
33-
if api_key is None:
34-
raise ValueError("HYPERBROWSER_API_KEY environment variable is required")
35-
42+
def from_constructor(
43+
cls,
44+
*,
45+
api_key: Optional[str] = None,
46+
base_url: Optional[str] = None,
47+
runtime_proxy_override: Optional[str] = None,
48+
profile: Optional[str] = None,
49+
) -> "ClientConfig":
3650
return cls(
37-
api_key=api_key,
38-
base_url=os.environ.get(
39-
"HYPERBROWSER_BASE_URL", "https://api.hyperbrowser.ai"
51+
api_key=api_key
52+
if api_key is not None
53+
else _env_text("HYPERBROWSER_API_KEY"),
54+
base_url=(
55+
base_url
56+
if base_url is not None
57+
else (_env_text("HYPERBROWSER_BASE_URL") or DEFAULT_BASE_URL)
58+
),
59+
runtime_proxy_override=runtime_proxy_override,
60+
profile=(
61+
profile if profile is not None else _env_text("HYPERBROWSER_PROFILE")
4062
),
41-
profile=os.environ.get("HYPERBROWSER_PROFILE"),
42-
frontend_url=os.environ.get("HYPERBROWSER_FRONTEND_URL"),
63+
frontend_url=_env_text("HYPERBROWSER_FRONTEND_URL"),
4364
auth_lock_timeout_ms=_env_positive_int("HYPERBROWSER_AUTH_LOCK_TIMEOUT_MS"),
4465
auth_lock_poll_interval_ms=_env_positive_int(
4566
"HYPERBROWSER_AUTH_LOCK_POLL_INTERVAL_MS"
4667
),
4768
auth_lock_stale_ms=_env_positive_int("HYPERBROWSER_AUTH_LOCK_STALE_MS"),
4869
)
70+
71+
@classmethod
72+
def from_env(cls) -> "ClientConfig":
73+
api_key = _env_text("HYPERBROWSER_API_KEY")
74+
if api_key is None:
75+
raise ValueError("HYPERBROWSER_API_KEY environment variable is required")
76+
return cls.from_constructor(api_key=api_key)

0 commit comments

Comments
 (0)