Mirrowel commited on
Commit
abbdf2a
·
1 Parent(s): 302d563

fix(auth): 🔨 tighten oauth refresh intervals, expand refresh buffers, improve logging and add iFlow docs

Browse files

- lower default OAUTH_REFRESH_INTERVAL from 3600s to 600s (updated .env.example, README, and background refresher)
- move background refresher sleep to after proactive refresh loop and suppress noisy info log to avoid unnecessary startup delay/noise
- increase REFRESH_EXPIRY_BUFFER_SECONDS for Gemini (30 minutes) and Qwen (3 hours)
- convert several lib_logger.info calls to debug and add richer error logging for HTTP failures and missing refresh tokens
- add Content-Type/Accept headers for Qwen token requests and surface HTTP error bodies for easier troubleshooting
- add extracted documentation: iFlow OAuth flow and iFlow JS bundle summary (docs/iflow_auth_flow_extracted.md, docs/iflow_js_bundle_summary.md)

.env.example CHANGED
@@ -166,7 +166,7 @@ MAX_CONCURRENT_REQUESTS_PER_KEY_IFLOW=1
166
  # --- OAuth Refresh Interval ---
167
  # How often, in seconds, the background refresher should check and refresh
168
  # expired OAuth tokens.
169
- OAUTH_REFRESH_INTERVAL=3600 # Default is 3600 seconds (1 hour)
170
 
171
  # --- Skip OAuth Initialization ---
172
  # Set to "true" to prevent the proxy from performing the interactive OAuth
 
166
  # --- OAuth Refresh Interval ---
167
  # How often, in seconds, the background refresher should check and refresh
168
  # expired OAuth tokens.
169
+ OAUTH_REFRESH_INTERVAL=600 # Default is 600 seconds (10 minutes)
170
 
171
  # --- Skip OAuth Initialization ---
172
  # Set to "true" to prevent the proxy from performing the interactive OAuth
README.md CHANGED
@@ -480,9 +480,9 @@ The following advanced settings can be added to your `.env` file (or configured
480
 
481
  #### OAuth and Refresh Settings
482
 
483
- - **`OAUTH_REFRESH_INTERVAL`**: Controls how often (in seconds) the background refresher checks for expired OAuth tokens. Default is `3600` (1 hour).
484
  ```env
485
- OAUTH_REFRESH_INTERVAL=1800 # Check every 30 minutes
486
  ```
487
 
488
  - **`SKIP_OAUTH_INIT_CHECK`**: Set to `true` to skip the interactive OAuth setup/validation check on startup. Essential for non-interactive environments like Docker containers or CI/CD pipelines.
 
480
 
481
  #### OAuth and Refresh Settings
482
 
483
+ - **`OAUTH_REFRESH_INTERVAL`**: Controls how often (in seconds) the background refresher checks for expired OAuth tokens. Default is `600` (10 minutes).
484
  ```env
485
+ OAUTH_REFRESH_INTERVAL=600 # Check every 10 minutes
486
  ```
487
 
488
  - **`SKIP_OAUTH_INIT_CHECK`**: Set to `true` to skip the interactive OAuth setup/validation check on startup. Essential for non-interactive environments like Docker containers or CI/CD pipelines.
docs/iflow_auth_flow_extracted.md ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **iFlow OAuth Flow – Extracted From Bundle (`iflow.js`)**
2
+
3
+ Purpose
4
+ - Provide a precise, implementation-ready description of the iFlow authentication flow as embedded in the bundled CLI (`external/iflow-cli-tarball/package/bundle/iflow.js`).
5
+ - Enable faithful replication in other tooling without re-reading the minified bundle.
6
+
7
+ Scope
8
+ - Focuses ONLY on iFlow-specific authorization (NOT generic Google / other OAuth client code also present in the bundle).
9
+ - Omits internal unrelated libraries (telemetry, localization, unrelated OAuth handlers for other issuers).
10
+
11
+ Overview of Sequence
12
+ 1. Determine callback port: Either `OAUTH_CALLBACK_PORT` env var, or dynamically allocate an ephemeral free port.
13
+ 2. Build local redirect URI: `http://localhost:{port}/oauth2callback` (host override via `OAUTH_CALLBACK_HOST`, default `localhost`).
14
+ 3. Generate anti-CSRF `state`: 32 random hex bytes.
15
+ 4. Construct authorization URL:
16
+ - Base: `https://iflow.cn/oauth`
17
+ - Query parameters:
18
+ - `loginMethod=phone`
19
+ - `type=phone`
20
+ - `redirect=<encoded redirect URL>&state=<state>` (note: the bundle concatenates and encodes the redirect first, then appends `&state=` and the state value; effectively `redirect` holds both the actual callback URL and embeds the state parameter)
21
+ - `client_id=10009311001`
22
+ 5. Display URL & attempt to open it in the system browser.
23
+ 6. Local HTTP server listens for requests to `/oauth2callback`.
24
+ 7. On callback:
25
+ - Reject if path not `/oauth2callback`.
26
+ - If `error` param present: redirect to `https://iflow.cn/oauth/error` and fail.
27
+ - If `state` mismatch: respond with plain text error (“State mismatch. Possible CSRF attack”).
28
+ - If `code` is present and `state` matches: exchange code for tokens.
29
+ 8. Token exchange: POST `https://iflow.cn/oauth/token`.
30
+ 9. On success: persist credentials (JSON file via internal helper), then fetch user info to obtain `apiKey`.
31
+ 10. Redirect browser to success page: `https://iflow.cn/oauth/success`.
32
+
33
+ Authorization URL Details
34
+ - Final composed form (illustrative – values substituted):
35
+ ```
36
+ https://iflow.cn/oauth?loginMethod=phone&type=phone&redirect={ENCODED_REDIRECT_AND_STATE}&client_id=10009311001
37
+ ```
38
+ - Important nuance: The bundle embeds the state inside the `redirect` query value (pattern: `redirect=<encodeURIComponent(callbackUrl)> &state=<state>` BEFORE concatenation). Replication MAY instead send state as a separate top-level query parameter if the server tolerates it; however to remain faithful, mimic the bundle pattern: keep `state` appended inside the `redirect` value.
39
+ - Conservative replication: EXACT concatenation used by bundle:
40
+ - Let `callback = http://localhost:{port}/oauth2callback`
41
+ - Let `state = <random-hex>`
42
+ - Let `redirectParamValue = encodeURIComponent(callback) + "&state=" + state`
43
+ - Query param: `redirect=redirectParamValue`
44
+ - Full URL: `https://iflow.cn/oauth?loginMethod=phone&type=phone&redirect=${redirectParamValue}&client_id=10009311001`
45
+
46
+ Callback Handling Logic
47
+ - Expected parameters (in parsed search params of original request URL):
48
+ - `code`: authorization code (required for success)
49
+ - `state`: must equal originally generated random hex string
50
+ - `error`: signals authentication failure
51
+ - Error conditions:
52
+ - Missing `code`: treated as failure ("noCodeFound").
53
+ - Missing or mismatched `state`: potential CSRF → failure.
54
+ - Any `error` param → failure.
55
+ - On success: code progresses to token exchange.
56
+
57
+ Token Exchange (Authorization Code Grant)
58
+ - Endpoint: `https://iflow.cn/oauth/token`
59
+ - Method: `POST`
60
+ - Headers:
61
+ - `Content-Type: application/x-www-form-urlencoded`
62
+ - `Authorization: Basic <base64(client_id:client_secret)>`
63
+ - Body parameters (URL-encoded form):
64
+ - `grant_type=authorization_code`
65
+ - `code=<authorization_code>`
66
+ - `redirect_uri=http://localhost:{port}/oauth2callback` (MUST match original callback URI before encoding inside redirect param)
67
+ - `client_id=10009311001`
68
+ - `client_secret=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW`
69
+ - Response JSON fields consumed:
70
+ - `access_token`
71
+ - `refresh_token`
72
+ - `expires_in` (converted to absolute expiry timestamp)
73
+ - `token_type`
74
+ - `scope`
75
+
76
+ User Info / API Key Retrieval
77
+ - Endpoint: `https://iflow.cn/api/oauth/getUserInfo?accessToken=<access_token>`
78
+ - Method: `GET`
79
+ - Response expected shape (simplified):
80
+ ```json
81
+ {
82
+ "success": true,
83
+ "data": { "apiKey": "...", "email": "..." }
84
+ }
85
+ ```
86
+ - On success, the extracted `apiKey` is appended to stored credentials and cached.
87
+ - Retries: The bundle performs up to 3 retries with backoff for transient (5xx / 408 / 429) failures.
88
+
89
+ Persisted Credential Structure (conceptual)
90
+ - Keys stored: `access_token`, `refresh_token`, `expiry_date`, `token_type`, `scope`, optionally `apiKey`.
91
+ - File path: derived via internal helper (not exposed here); replication may choose its own path (e.g. `~/.iflow/credentials.json`).
92
+ - Permissions set mode `0600` (decimal 384) when writing.
93
+
94
+ Refresh Flow
95
+ - Trigger conditions:
96
+ - Token nearing expiry (< 24 hours) or explicit refresh check in background tasks.
97
+ - Refresh request:
98
+ - POST `https://iflow.cn/oauth/token`
99
+ - Headers: same as authorization-code exchange.
100
+ - Body params:
101
+ - `grant_type=refresh_token`
102
+ - `refresh_token=<stored_refresh_token>`
103
+ - `client_id=10009311001`
104
+ - `client_secret=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW`
105
+ - Response handling mirrors initial token response (update expiry, access/refresh tokens, scope, token type). May re-fetch user info to ensure `apiKey` is current.
106
+
107
+ PKCE Status
108
+ - The bundle contains a generic OAuth library with PKCE helpers (code_verifier / code_challenge generation) for other issuers.
109
+ - The iFlow-specific code path DOES NOT include `code_challenge` or `code_verifier` in its authorization or token requests.
110
+ - Conclusion: PKCE is NOT required for the current iFlow CLI flow; replication should omit PKCE unless the provider later enforces it.
111
+
112
+ Environment Variables Influencing Flow
113
+ - `OAUTH_CALLBACK_PORT`: If set and valid (1–65535), use as callback port instead of ephemeral port.
114
+ - `OAUTH_CALLBACK_HOST`: Hostname for listening; default `localhost`.
115
+ - (Potential others exist for generic flows, but only these are critical for iFlow-specific path.)
116
+
117
+ Security Considerations
118
+ - State validation prevents CSRF / unsolicited redirects.
119
+ - The unusual embedding of state inside the `redirect` parameter means the state also appears server-side only if parsed out; exact replication should preserve this pattern unless confirmed safe to normalize.
120
+ - Basic auth header exposes client_secret; always use HTTPS (the CLI does). Do not log raw Authorization headers.
121
+ - Persisted credentials should be stored with restrictive file permissions.
122
+ - Refresh token invalidation must trigger re-authentication (bundle clears credentials on failure).
123
+
124
+ Error Handling Patterns (Simplified)
125
+ - Authorization errors: Redirect user to error page `https://iflow.cn/oauth/error`.
126
+ - Token request failure: Inspect HTTP status & status text; raise user-facing error.
127
+ - Refresh failure or expiry: Clear credential file and require re-login.
128
+ - User info failures: Retry transient errors (5xx / 408 / 429), otherwise log and continue without apiKey.
129
+
130
+ Replication Checklist
131
+ 1. Generate random hex `state` (32 bytes recommended).
132
+ 2. Select callback port: env override or ephemeral free port.
133
+ 3. Build callback URL `http://localhost:{port}/oauth2callback`.
134
+ 4. Compose authorization URL exactly as bundle does (embedded `&state=` inside `redirect` value).
135
+ 5. Launch local HTTP server; validate `state`, extract `code`.
136
+ 6. Exchange code using Basic auth + form body (include both client_id/client_secret).
137
+ 7. Store credentials; compute `expiry_date = now + expires_in*1000`.
138
+ 8. Fetch user info to get `apiKey`; attach to stored credentials.
139
+ 9. Implement refresh POST (grant_type=refresh_token) with same auth style.
140
+ 10. Implement retry & expiry logic; clear credentials if permanently invalid.
141
+ 11. Never include PKCE unless provider changes requirements.
142
+ 12. Optionally open browser automatically and present fallback URL if open fails.
143
+
144
+ Minimal Pseudocode Outline (Language-Agnostic)
145
+ ```text
146
+ state = randomHex(32)
147
+ port = env.OAUTH_CALLBACK_PORT || findFreePort()
148
+ callback = "http://localhost:" + port + "/oauth2callback"
149
+ redirectParamValue = urlencode(callback) + "&state=" + state
150
+ authUrl = "https://iflow.cn/oauth?loginMethod=phone&type=phone&redirect=" + redirectParamValue + "&client_id=10009311001"
151
+
152
+ openBrowser(authUrl)
153
+ code = await waitForCallback(callback, state)
154
+
155
+ tokenResp = POST https://iflow.cn/oauth/token
156
+ Headers: Content-Type, Authorization: Basic base64(client_id:client_secret)
157
+ Body (form): grant_type=authorization_code, code, redirect_uri=callback, client_id, client_secret
158
+
159
+ creds = {access_token, refresh_token, expiry_date=now+expires_in*1000, token_type, scope}
160
+ userInfo = GET https://iflow.cn/api/oauth/getUserInfo?accessToken=creds.access_token
161
+ if userInfo.success: creds.apiKey = userInfo.data.apiKey
162
+ store(creds)
163
+
164
+ // Refresh flow
165
+ if nearing expiry:
166
+ refreshResp = POST https://iflow.cn/oauth/token (grant_type=refresh_token,...)
167
+ update creds, optionally re-fetch apiKey
168
+ ```
169
+
170
+ Testing Recommendations
171
+ - Simulate callback manually with crafted URL containing correct `code` & `state` to test server path.
172
+ - Validate rejection on mismatched state.
173
+ - Confirm `apiKey` presence after user info call.
174
+ - Force refresh by adjusting expiry_date to near past and invoking refresh routine.
175
+
176
+ Future-Proofing
177
+ - Monitor provider announcements for PKCE enforcement; if required, switch to `code_challenge` / `code_verifier` standard pattern.
178
+ - Consider switching state embedding to separate query parameter if provider standardizes (requires test).
179
+
180
+ Summary
181
+ - The iFlow CLI uses a straightforward OAuth2 Authorization Code Grant with Basic client authentication and NO PKCE.
182
+ - The distinctive element is embedding `state` within the `redirect` parameter value.
183
+ - Replication requires careful reconstruction of the authorization URL, robust callback validation, token + refresh exchanges, and an additional user info call to retrieve the operational `apiKey`.
184
+
185
+ -- End of extracted flow documentation
docs/iflow_js_bundle_summary.md ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **iFlow JS Bundle Summary**
2
+
3
+ Path: `external/iflow-cli-tarball/package/bundle/iflow.js`
4
+
5
+ Purpose
6
+ - A single-file bundled/minified Node/Browser CLI distribution for the iFlow CLI.
7
+ - Contains the CLI runtime, UI strings, crypto helpers, networking logic and an OAuth-based authentication flow used by iFlow's tooling.
8
+
9
+ Bundle metadata
10
+ - File size (approx): ~10.2 MB (minified/bundled, many libraries included).
11
+ - Other files in same folder: `iflow-cli-vscode-ide-companion-*.vsix`, `sandbox-*.sb` files (IDE companions / sandbox artifacts).
12
+
13
+ High-level contents and useful targets
14
+ - Crypto helpers
15
+ - Implements both browser and Node crypto adapters (named `BrowserCrypto` and `NodeCrypto`).
16
+ - Exposed helpers include: `sha256DigestHex`, `sha256DigestBase64`, random bytes generator, HMAC signing helpers, and base64 helpers.
17
+ - These utilities are used to compute PKCE code_challenge values (S256) and other digests.
18
+
19
+ - PKCE evidence
20
+ - The bundle includes code that computes SHA-256 digests and derives hex/base64 strings. This is strong evidence PKCE is used by the CLI.
21
+ - Keywords to look for: `sha256DigestHex`, `code_challenge`, `code_verifier`, `code_challenge_method`, `PKCE`, `S256`.
22
+
23
+ - OAuth + Browser flow
24
+ - The CLI appears to use a browser-based authorization flow:
25
+ - It constructs an authorization URL (authUrl) and attempts to open it in the user's browser.
26
+ - It starts or expects a local callback path (the pattern `/oauth2callback` appears in the bundle).
27
+ - After the browser redirects to the local callback, the bundle's callback handler extracts `code` and `state` and resolves them (pattern seen as resolving an object like `{code: c, state: u}`).
28
+ - User-facing strings found in the bundle include messages and instructions such as:
29
+ - "Attempting to open authentication page in your browser."
30
+ - "Navigate to the URL below to sign in" (or similar localized messages).
31
+
32
+ - Token exchange and API-key retrieval
33
+ - The bundle contains logic that exchanges an authorization code for tokens and then retrieves user info to extract provider-specific `apiKey` (iFlow uses a separate API key for API calls).
34
+ - Token exchange patterns include POSTing to token endpoints and handling success/error JSON responses.
35
+ - Note: In some CLI flows, both PKCE and client-secret based exchanges may be present; inspect the exact token-request payload to confirm which parameters are sent.
36
+
37
+ - Additional artifacts bundled
38
+ - Localization strings, telemetry/logging code, UI wrappers, and other third-party libraries are bundled together, making raw inspection noisy.
39
+
40
+ Practical guidance for other agents
41
+ - Why it's tricky
42
+ - `iflow.js` is minified/compiled and contains many third-party modules concatenated together. This makes it hard to read directly—searching for specific keywords is the fastest approach.
43
+
44
+ - Key search terms (recommended)
45
+ - `sha256`, `sha256DigestHex`, `code_challenge`, `code_verifier`, `code_challenge_method`, `authUrl`, `oauth2callback`, `Attempting to open`, `authUrl`, `authorize`, `token`, `getUserInfo`, `apiKey`, `redirect`, `state`.
46
+
47
+ - Useful PowerShell commands (run at repo root) to inspect the bundle quickly
48
+ - Search for keywords:
49
+ ```powershell
50
+ Select-String -Path external\iflow-cli-tarball\package\bundle\iflow.js -Pattern "sha256|code_challenge|authUrl|oauth2callback|getUserInfo|apiKey|Attempting to open" -SimpleMatch
51
+ ```
52
+ - View a nearby window of lines (example indices found via Select-String):
53
+ ```powershell
54
+ # read lines 940..1005 (example offsets)
55
+ (Get-Content -Path 'external\iflow-cli-tarball\package\bundle\iflow.js')[940..1005] -join "`n"
56
+ ```
57
+
58
+ - How to extract provider-specific values
59
+ 1. Search for `client_id`, `authorize`, `oauth` and `token` strings to find endpoint constants and client identifiers.
60
+ 2. Look for `authUrl` construction code to see which query parameters are included (e.g., `redirect`, `state`, `client_id`, `code_challenge`).
61
+ 3. Inspect token exchange code to see whether it sends `client_secret` or `code_verifier` (or both).
62
+
63
+ - Repro steps for someone implementing the auth flow in another language (concise)
64
+ 1. Locate the `authUrl` construction and confirm required query params (client_id, redirect, state, any `loginMethod`/`type` parameters).
65
+ 2. Confirm whether PKCE is required by checking for `code_challenge`/`code_challenge_method` in the auth request and `code_verifier` in the token request.
66
+ 3. Confirm redirect URI path (`/oauth2callback` pattern) and port if the CLI starts a local callback server.
67
+ 4. Confirm token exchange method and auth method (Basic auth with client_id:client_secret in Authorization header OR an exchange that uses client_secret in body, or an exchange that uses `code_verifier` and omits client_secret).
68
+ 5. Confirm post-token user info fetch that returns `apiKey` separately—recreate that call to obtain the API key.
69
+
70
+ - Recommendations / next actions for an agent
71
+ - If you need to replicate exactly, extract the authUrl and token request payloads verbatim from the bundle.
72
+ - If the bundle uses PKCE, add PKCE (code_verifier + code_challenge=S256) to your client implementation. If it uses Basic client_secret authentication instead, ensure you include the client_secret in exchanges.
73
+ - Use a modular approach: implement the browser + callback pattern, but make the PKCE piece optional/conditional so it can work with servers that require or permit it.
74
+
75
+ - References inside this repo (helpful snippets to reuse)
76
+ - Example PKCE helper exists in `src/rotator_library/providers/qwen_auth_base.py` — you can reuse code-verifier / code-challenge generation from there.
77
+
78
+ Appendix: quick checklist for auditors
79
+ - [ ] Confirm `authUrl` query parameters.
80
+ - [ ] Confirm presence of `code_challenge` in auth request.
81
+ - [ ] Confirm presence of `code_verifier` in token request.
82
+ - [ ] Confirm token exchange auth method (Basic, client_secret body param, or neither).
83
+ - [ ] Confirm local callback path and default port.
84
+ - [ ] Confirm user-info endpoint that returns `apiKey`.
85
+
86
+ -- End of summary
src/rotator_library/background_refresher.py CHANGED
@@ -17,11 +17,11 @@ class BackgroundRefresher:
17
  """
18
  def __init__(self, client: 'RotatingClient'):
19
  try:
20
- interval_str = os.getenv("OAUTH_REFRESH_INTERVAL", "3600")
21
  self._interval = int(interval_str)
22
  except ValueError:
23
- lib_logger.warning(f"Invalid OAUTH_REFRESH_INTERVAL '{interval_str}'. Falling back to 3600s.")
24
- self._interval = 3600
25
  self._client = client
26
  self._task: Optional[asyncio.Task] = None
27
 
@@ -46,8 +46,7 @@ class BackgroundRefresher:
46
  """The main loop for the background task."""
47
  while True:
48
  try:
49
- await asyncio.sleep(self._interval)
50
- lib_logger.info("Running proactive token refresh check...")
51
 
52
  oauth_configs = self._client.get_oauth_credentials()
53
  for provider, paths in oauth_configs.items():
@@ -58,6 +57,7 @@ class BackgroundRefresher:
58
  await provider_plugin.proactively_refresh(path)
59
  except Exception as e:
60
  lib_logger.error(f"Error during proactive refresh for '{path}': {e}")
 
61
  except asyncio.CancelledError:
62
  break
63
  except Exception as e:
 
17
  """
18
  def __init__(self, client: 'RotatingClient'):
19
  try:
20
+ interval_str = os.getenv("OAUTH_REFRESH_INTERVAL", "600")
21
  self._interval = int(interval_str)
22
  except ValueError:
23
+ lib_logger.warning(f"Invalid OAUTH_REFRESH_INTERVAL '{interval_str}'. Falling back to 600s.")
24
+ self._interval = 600
25
  self._client = client
26
  self._task: Optional[asyncio.Task] = None
27
 
 
46
  """The main loop for the background task."""
47
  while True:
48
  try:
49
+ #lib_logger.info("Running proactive token refresh check...")
 
50
 
51
  oauth_configs = self._client.get_oauth_credentials()
52
  for provider, paths in oauth_configs.items():
 
57
  await provider_plugin.proactively_refresh(path)
58
  except Exception as e:
59
  lib_logger.error(f"Error during proactive refresh for '{path}': {e}")
60
+ await asyncio.sleep(self._interval)
61
  except asyncio.CancelledError:
62
  break
63
  except Exception as e:
src/rotator_library/providers/gemini_auth_base.py CHANGED
@@ -23,7 +23,7 @@ CLIENT_ID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleuserconten
23
  CLIENT_SECRET = "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" #https://api.kilocode.ai/extension-config.json
24
  TOKEN_URI = "https://oauth2.googleapis.com/token"
25
  USER_INFO_URI = "https://www.googleapis.com/oauth2/v1/userinfo"
26
- REFRESH_EXPIRY_BUFFER_SECONDS = 300
27
 
28
  console = Console()
29
 
@@ -192,7 +192,7 @@ class GeminiAuthBase:
192
  if not force and not self._is_token_expired(self._credentials_cache.get(path, creds)):
193
  return self._credentials_cache.get(path, creds)
194
 
195
- lib_logger.info(f"Refreshing Gemini OAuth token for '{Path(path).name}' (forced: {force})...")
196
  refresh_token = creds.get("refresh_token")
197
  if not refresh_token:
198
  raise ValueError("No refresh_token found in credentials file.")
@@ -317,7 +317,7 @@ class GeminiAuthBase:
317
  # But log it for debugging purposes
318
 
319
  await self._save_credentials(path, creds)
320
- lib_logger.info(f"Successfully refreshed Gemini OAuth token for '{Path(path).name}'.")
321
  return creds
322
 
323
  async def proactively_refresh(self, credential_path: str):
 
23
  CLIENT_SECRET = "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" #https://api.kilocode.ai/extension-config.json
24
  TOKEN_URI = "https://oauth2.googleapis.com/token"
25
  USER_INFO_URI = "https://www.googleapis.com/oauth2/v1/userinfo"
26
+ REFRESH_EXPIRY_BUFFER_SECONDS = 30 * 60 # 30 minutes buffer before expiry
27
 
28
  console = Console()
29
 
 
192
  if not force and not self._is_token_expired(self._credentials_cache.get(path, creds)):
193
  return self._credentials_cache.get(path, creds)
194
 
195
+ lib_logger.debug(f"Refreshing Gemini OAuth token for '{Path(path).name}' (forced: {force})...")
196
  refresh_token = creds.get("refresh_token")
197
  if not refresh_token:
198
  raise ValueError("No refresh_token found in credentials file.")
 
317
  # But log it for debugging purposes
318
 
319
  await self._save_credentials(path, creds)
320
+ lib_logger.debug(f"Successfully refreshed Gemini OAuth token for '{Path(path).name}'.")
321
  return creds
322
 
323
  async def proactively_refresh(self, credential_path: str):
src/rotator_library/providers/iflow_auth_base.py CHANGED
@@ -414,7 +414,7 @@ class IFlowAuthBase:
414
 
415
  creds_from_file = self._credentials_cache[path]
416
 
417
- lib_logger.info(f"Refreshing iFlow OAuth token for '{Path(path).name}'...")
418
  refresh_token = creds_from_file.get("refresh_token")
419
  if not refresh_token:
420
  raise ValueError("No refresh_token found in iFlow credentials file.")
@@ -452,6 +452,9 @@ class IFlowAuthBase:
452
  except httpx.HTTPStatusError as e:
453
  last_error = e
454
  status_code = e.response.status_code
 
 
 
455
 
456
  # [STATUS CODE HANDLING]
457
  if status_code in (401, 403):
@@ -522,7 +525,7 @@ class IFlowAuthBase:
522
  creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
523
 
524
  await self._save_credentials(path, creds_from_file)
525
- lib_logger.info(f"Successfully refreshed iFlow OAuth token for '{Path(path).name}'.")
526
  return creds_from_file
527
 
528
  async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
 
414
 
415
  creds_from_file = self._credentials_cache[path]
416
 
417
+ lib_logger.debug(f"Refreshing iFlow OAuth token for '{Path(path).name}'...")
418
  refresh_token = creds_from_file.get("refresh_token")
419
  if not refresh_token:
420
  raise ValueError("No refresh_token found in iFlow credentials file.")
 
452
  except httpx.HTTPStatusError as e:
453
  last_error = e
454
  status_code = e.response.status_code
455
+ error_body = e.response.text
456
+
457
+ lib_logger.error(f"[REFRESH HTTP ERROR] HTTP {status_code} for '{Path(path).name}': {error_body}")
458
 
459
  # [STATUS CODE HANDLING]
460
  if status_code in (401, 403):
 
525
  creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
526
 
527
  await self._save_credentials(path, creds_from_file)
528
+ lib_logger.debug(f"Successfully refreshed iFlow OAuth token for '{Path(path).name}'.")
529
  return creds_from_file
530
 
531
  async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
src/rotator_library/providers/qwen_auth_base.py CHANGED
@@ -25,7 +25,7 @@ lib_logger = logging.getLogger('rotator_library')
25
  CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56" #https://api.kilocode.ai/extension-config.json
26
  SCOPE = "openid profile email model.completion"
27
  TOKEN_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/token"
28
- REFRESH_EXPIRY_BUFFER_SECONDS = 300
29
 
30
  console = Console()
31
 
@@ -186,9 +186,10 @@ class QwenAuthBase:
186
 
187
  creds_from_file = self._credentials_cache[path]
188
 
189
- lib_logger.info(f"Refreshing Qwen OAuth token for '{Path(path).name}'...")
190
  refresh_token = creds_from_file.get("refresh_token")
191
  if not refresh_token:
 
192
  raise ValueError("No refresh_token found in Qwen credentials file.")
193
 
194
  # [RETRY LOGIC] Implement exponential backoff for transient errors
@@ -197,6 +198,8 @@ class QwenAuthBase:
197
  last_error = None
198
 
199
  headers = {
 
 
200
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
201
  }
202
 
@@ -215,6 +218,8 @@ class QwenAuthBase:
215
  except httpx.HTTPStatusError as e:
216
  last_error = e
217
  status_code = e.response.status_code
 
 
218
 
219
  # [STATUS CODE HANDLING]
220
  if status_code in (401, 403):
@@ -265,7 +270,7 @@ class QwenAuthBase:
265
  creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
266
 
267
  await self._save_credentials(path, creds_from_file)
268
- lib_logger.info(f"Successfully refreshed Qwen OAuth token for '{Path(path).name}'.")
269
  return creds_from_file
270
 
271
  async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
 
25
  CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56" #https://api.kilocode.ai/extension-config.json
26
  SCOPE = "openid profile email model.completion"
27
  TOKEN_ENDPOINT = "https://chat.qwen.ai/api/v1/oauth2/token"
28
+ REFRESH_EXPIRY_BUFFER_SECONDS = 3 * 60 * 60 # 3 hours buffer before expiry
29
 
30
  console = Console()
31
 
 
186
 
187
  creds_from_file = self._credentials_cache[path]
188
 
189
+ lib_logger.debug(f"Refreshing Qwen OAuth token for '{Path(path).name}'...")
190
  refresh_token = creds_from_file.get("refresh_token")
191
  if not refresh_token:
192
+ lib_logger.error(f"No refresh_token found in '{Path(path).name}'")
193
  raise ValueError("No refresh_token found in Qwen credentials file.")
194
 
195
  # [RETRY LOGIC] Implement exponential backoff for transient errors
 
198
  last_error = None
199
 
200
  headers = {
201
+ "Content-Type": "application/x-www-form-urlencoded",
202
+ "Accept": "application/json",
203
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
204
  }
205
 
 
218
  except httpx.HTTPStatusError as e:
219
  last_error = e
220
  status_code = e.response.status_code
221
+ error_body = e.response.text
222
+ lib_logger.error(f"HTTP {status_code} for '{Path(path).name}': {error_body}")
223
 
224
  # [STATUS CODE HANDLING]
225
  if status_code in (401, 403):
 
270
  creds_from_file["_proxy_metadata"]["last_check_timestamp"] = time.time()
271
 
272
  await self._save_credentials(path, creds_from_file)
273
+ lib_logger.debug(f"Successfully refreshed Qwen OAuth token for '{Path(path).name}'.")
274
  return creds_from_file
275
 
276
  async def get_api_details(self, credential_identifier: str) -> Tuple[str, str]:
src/rotator_library/usage_manager.py CHANGED
@@ -105,7 +105,7 @@ class UsageManager:
105
  last_reset_dt is None
106
  or last_reset_dt < reset_threshold_today <= now_utc
107
  ):
108
- lib_logger.info(f"Performing daily reset for key ...{key[-6:]}")
109
  needs_saving = True
110
 
111
  # Reset cooldowns
 
105
  last_reset_dt is None
106
  or last_reset_dt < reset_threshold_today <= now_utc
107
  ):
108
+ lib_logger.debug(f"Performing daily reset for key ...{key[-6:]}")
109
  needs_saving = True
110
 
111
  # Reset cooldowns