feat: support upstream token refresh

This commit is contained in:
liumangmang
2026-06-30 14:34:40 +08:00
parent 2d1dcb8f9f
commit c5bb63cf82
15 changed files with 895 additions and 140 deletions
+43 -6
View File
@@ -9,10 +9,12 @@ from urllib.parse import urlparse
logger = logging.getLogger(__name__)
# Keys likely to contain auth tokens in storage
TOKEN_KEYS = frozenset({
"token", "access_token", "accessToken", "jwt", "auth_token", "authToken",
"refresh_token", "refreshToken", "id_token", "session_token",
ACCESS_TOKEN_KEYS = frozenset({
"token", "access_token", "accesstoken", "jwt", "auth_token", "authtoken",
"id_token", "idtoken", "session_token", "sessiontoken",
})
REFRESH_TOKEN_KEYS = frozenset({"refresh_token", "refreshtoken"})
TOKEN_KEYS = ACCESS_TOKEN_KEYS | REFRESH_TOKEN_KEYS
SECRET_KEYS = frozenset({
"secret", "api_key", "apiKey", "apikey",
})
@@ -116,6 +118,27 @@ def _curate_candidates(
extra=bundle_extra,
)
# 0.5. Sub2API browser sessions keep access token and refresh token separately.
storage_sources = [("localStorage", local_storage), ("sessionStorage", session_storage)]
access_entry = _find_storage_token_entry(storage_sources, ACCESS_TOKEN_KEYS)
refresh_entry = _find_storage_token_entry(storage_sources, REFRESH_TOKEN_KEYS)
if access_entry and refresh_entry:
access_source, access_value = access_entry
refresh_source, refresh_value = refresh_entry
_add(
candidates,
"bearer_token",
f"{access_source}+{refresh_source}",
access_value,
_preview(access_value),
"Sub2API Bearer + Refresh Token",
96,
extra={
"refresh_token": refresh_value,
"refresh_token_preview": _preview(refresh_value),
},
)
# 1. CDP-captured network headers (high confidence)
seen = set()
for h in auth_headers:
@@ -141,9 +164,10 @@ def _curate_candidates(
if not isinstance(val, str) or not val:
continue
key_lower = key.lower()
is_refresh_key = any(k in key_lower for k in REFRESH_TOKEN_KEYS)
# Explicit auth-named keys
if any(k in key_lower for k in TOKEN_KEYS):
if any(k in key_lower for k in TOKEN_KEYS) and not is_refresh_key:
preview = _preview(val)
score = 85 if "token" in key_lower and val.count(".") >= 2 else 75
_add(candidates, "bearer_token", f"{store_name}.{key}", val, preview,
@@ -153,14 +177,14 @@ def _curate_candidates(
f"{store_name}.{key}", 70)
# Looks like a JWT (xx.yy.zz format)
if val.count(".") >= 2 and 20 < len(val) < 5000:
if not is_refresh_key and val.count(".") >= 2 and 20 < len(val) < 5000:
if val not in seen:
seen.add(val)
_add(candidates, "bearer_token", f"{store_name}.{key}", val, _preview(val),
f"{store_name}.{key} (JWT)", 80)
# sk-xxx API key pattern
if val.startswith("sk-") and len(val) > 10:
if not is_refresh_key and val.startswith("sk-") and len(val) > 10:
_add(candidates, "bearer_token", f"{store_name}.{key}", val, _preview(val),
f"{store_name}.{key} (API Key)", 90)
@@ -198,6 +222,19 @@ def _find_user_header(headers: list[dict[str, str]]) -> str:
return ""
def _find_storage_token_entry(
stores: list[tuple[str, dict[str, str]]],
key_names: frozenset[str],
) -> tuple[str, str] | None:
for store_name, store in stores:
for key, value in store.items():
if key.lower() not in key_names:
continue
if isinstance(value, str) and value.strip():
return f"{store_name}.{key}", value.strip()
return None
def _find_storage_value(*stores: dict[str, str], key: str) -> str:
for store in stores:
value = store.get(key)