Skip to content
This repository was archived by the owner on Jun 9, 2026. It is now read-only.

fix(oauth): silent token refresh — no browser popup#88

Merged
Steffen025 merged 3 commits into
devfrom
fix/oauth-silent-refresh-dev
Mar 24, 2026
Merged

fix(oauth): silent token refresh — no browser popup#88
Steffen025 merged 3 commits into
devfrom
fix/oauth-silent-refresh-dev

Conversation

@Steffen025

@Steffen025 Steffen025 commented Mar 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • OAuth2 refresh_token flow as primary refresh strategy (silent, no browser)
  • User-Agent: claude-cli/2.0 header fixes Anthropic 429 rate-limiting
  • config hook refreshes token at startup before OpenCode's auth check

Problem solved

When the Anthropic OAuth token expired, OpenCode would open a browser for re-auth. Root cause chain:

  1. Missing User-Agent header → Anthropic returned 429 on all refresh attempts
  2. No hook early enough to refresh before OpenCode's own auth check triggered the browser

Changes

lib/refresh-manager.ts

  • New Strategy 1: POST https://console.anthropic.com/v1/oauth/token with grant_type=refresh_token
  • User-Agent: claude-cli/2.0 (OpenCode Token Bridge) on all OAuth requests
  • 3× retry with exponential backoff (2s → 4s → 8s) for 429 responses
  • invalid_grant detection — no retry when refresh token is revoked

lib/token-utils.ts

  • Proactive refresh threshold: 2h → 1h remaining

anthropic-token-bridge.ts

  • config hook: refreshes expired token at plugin init, before any auth check → prevents browser popup
  • Keep-alive: pings /api/oauth/usage every 30min to prevent inactivity expiry (first ping at 5min)
  • Fast-path: system.transform skips Keychain sync when auth.json token is already valid

contrib/anthropic-max-bridge/

  • README + TECHNICAL.md updated to reflect new silent refresh behaviour

Verified working

[00:05:15] Plugin config hook running - early token refresh opportunity
[00:05:16] OAuth refresh successful - received new access and refresh tokens
[00:05:16] Config hook: Early token refresh successful - browser popup should be avoided

No browser opened. Token refreshed silently in ~300ms.

Summary by CodeRabbit

  • New Features

    • Silent background refresh at startup using stored refresh credentials
    • Keep‑alive ping every 30 minutes to reduce inactivity-based expiry
    • Proactive refresh triggered when under 1 hour of access remains
  • Improvements

    • More robust refresh with up to 3 retries and exponential backoff for rate limits
    • Improved token validation, clearer invalid states, and stronger fallback handling
    • Concurrency and cooldown controls to avoid repeated refresh attempts
  • Documentation

    • Updated docs describing the revised refresh and keep‑alive behavior

- OAuth2 refresh_token flow as Strategy 1 (silent, no browser)
  POST /v1/oauth/token with User-Agent: claude-cli/2.0 header
  Fixes 429 rate-limit errors from missing User-Agent
- Exponential backoff retry (2s, 4s, 8s) for 429 responses
- config hook: refreshes token at startup before auth check
  prevents browser popup when token is expired on launch
- Keep-alive ping every 30min to prevent inactivity expiry
- Proactive refresh threshold lowered: 60min (was 2h)
- Fast-path in system.transform: skip Keychain sync when
  auth.json token is already valid (eliminates log spam)
- Update contrib docs to reflect new 3-strategy refresh order
@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a multi-stage Anthropic OAuth refresh flow, keep-alive pings, atomic auth.json writes, Keychain↔auth.json sync, proactive 1-hour refresh threshold, retry/backoff logic for OAuth refresh, and a refresh manager module with concurrency control.

Changes

Cohort / File(s) Summary
Plugin entry points
\.opencode/plugins/anthropic-token-bridge.js, \.opencode/plugins/anthropic-token-bridge.ts
Integrates refresh-manager/token-utils, adds startup config hook and keep-alive scheduling, updates chat/session refresh logic to use proactive 1‑hour threshold, implements Keychain→auth.json sync on session start, and changes default export alias.
Refresh manager
\.opencode/plugins/lib/refresh-manager.js, \.opencode/plugins/lib/refresh-manager.ts
New module providing refreshAnthropicToken(), isRefreshInProgress(), and extractFromKeychain(); implements OAuth refresh via /v1/oauth/token with AbortController timeout, up to 3 retries and 429 exponential backoff, Keychain extraction (including expiresAt), setup-token fallback, cooldown and concurrency guards, and atomic auth.json updates.
Token utilities & logger
\.opencode/plugins/lib/token-utils.js, \.opencode/plugins/lib/token-utils.ts, \.opencode/plugins/lib/file-logger.*
New helpers to read/write ~/.local/share/opencode/auth.json atomically, mask tokens for logs, validate token state (checkAnthropicToken()), update stored tokens, and append timestamped logs to /tmp/pai-opencode-debug.log. Adjusted validation to return explicit reason codes (e.g., missing_access_token, invalid_expires) and reduced refresh threshold to 1 hour.
Documentation
contrib/anthropic-max-bridge/README.md, contrib/anthropic-max-bridge/TECHNICAL.md
Documented silent startup OAuth refresh-first strategy, keep-alive ping behavior, proactive <1-hour refresh semantics, retry/backoff details, and updated refresh order and fallback description.

Sequence Diagram

sequenceDiagram
    participant Plugin as Token Bridge Plugin
    participant AuthFile as auth.json
    participant OAuth as Anthropic OAuth Endpoint
    participant Keychain as macOS Keychain
    participant Setup as Setup-Token Exchange
    participant API as Anthropic API

    rect rgba(100, 150, 200, 0.5)
    Note over Plugin,AuthFile: Startup (config hook)
    Plugin->>AuthFile: read/check token
    AuthFile-->>Plugin: token invalid/expiring
    end

    rect rgba(100, 150, 200, 0.5)
    Note over Plugin,OAuth: Refresh attempt `#1` (preferred)
    Plugin->>OAuth: POST /v1/oauth/token (refresh_token)
    OAuth-->>Plugin: access_token / error
    Plugin->>AuthFile: write updated tokens
    end

    rect rgba(150, 200, 100, 0.5)
    Note over Plugin,Keychain: Fallback path
    Plugin->>Keychain: extract tokens (expiresAt)
    Keychain-->>Plugin: tokens or null
    Plugin->>AuthFile: sync tokens (if found)
    end

    rect rgba(200, 150, 100, 0.5)
    Note over Plugin,Setup: Last-resort exchange
    Plugin->>Plugin: generate setup-token
    Plugin->>Setup: exchange setup-token
    Setup-->>Plugin: oauth credentials
    Plugin->>AuthFile: persist credentials
    end

    rect rgba(200, 100, 150, 0.5)
    Note over Plugin,API: Keep-alive (periodic)
    Plugin->>API: GET /api/oauth/usage
    API-->>Plugin: 200 / 429
    Plugin->>Plugin: log result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through files to mend the keys,

Silent refreshes hum on gentle breeze,
Keep‑alive pings guard the twilight zone,
Tokens sync home, no browser shown,
A little rabbit saved your auth alone.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and concisely summarizes the main change: implementing silent OAuth token refresh to avoid browser popups, which aligns with the primary objective and substantial changes throughout the codebase.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/oauth-silent-refresh-dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
.opencode/plugins/lib/refresh-manager.ts (1)

336-349: ⚠️ Potential issue | 🟡 Minor

Keychain strategy ignores actual token expiry, always assumes 8 hours.

Strategy 2 extracts tokens from Keychain but hardcodes expiresIn to 28800 seconds (8h) regardless of the actual expiresAt stored in the Keychain. The JS compiled version correctly handles expiresAt, but this TypeScript source doesn't include it in the KeychainTokens interface or use it.

This could cause auth.json to store an incorrect expiry time, potentially leading to premature or delayed refresh attempts.

🐛 Proposed fix to use actual Keychain expiry
 interface KeychainTokens {
 	accessToken: string;
 	refreshToken: string;
+	expiresAt?: number;
 }

And update the Keychain extraction return and Strategy 2 logic to match the JS version's handling of expiresAt.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/refresh-manager.ts around lines 336 - 349, The
Keychain strategy currently hardcodes expiresIn=28800; update the Keychain
handling so extractFromKeychain/KeychainTokens include an expiresAt (timestamp)
and Strategy 2 computes a real expiresIn from that expiresAt (e.g.,
floor((expiresAt - Date.now())/1000)) before calling
updateAnthropicTokens(accessToken, refreshToken, expiresIn); ensure you clamp or
fallback to a sensible default (e.g., 28800) if expiresAt is missing or yields a
non-positive value so you don't write an incorrect expiry into auth.json.
.opencode/plugins/lib/token-utils.ts (1)

76-78: ⚠️ Potential issue | 🔴 Critical

TypeScript source missing critical validation checks present in compiled output.

The TS source at lines 76-78 uses anthropic.expires ?? 0 without validation, but the compiled JS includes two essential checks missing from the TS:

  1. Missing access token validation (JS lines 82-90): TS source has no check for missing anthropic.access, allowing undefined tokens to proceed.
  2. Missing expires type validation (JS lines 91-100): TS source uses nullish coalescing but lacks the typeof anthropic.expires !== "number" || !Number.isFinite(anthropic.expires) check, which prevents invalid expires values (NaN, Infinity, non-numeric).

The TS source should include both validations before using the expires value. Without the type check, invalid or missing expires values could silently default to 0 and produce incorrect timeRemainingMs calculations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/token-utils.ts around lines 76 - 78, Add the same
runtime validations present in the compiled JS: before computing
now/expires/timeRemainingMs, ensure anthropic.access is present (throw or return
an explicit error if missing) and validate anthropic.expires is a finite number
(check typeof anthropic.expires === "number" &&
Number.isFinite(anthropic.expires)); only then set expires and compute
timeRemainingMs. Update the token handling logic in token-utils.ts where now,
expires, and timeRemainingMs are computed to perform these checks for
anthropic.access and anthropic.expires and fail fast if they are invalid.
🧹 Nitpick comments (3)
.opencode/plugins/anthropic-token-bridge.ts (2)

22-28: Inefficient dynamic imports inside frequently-called function.

keepAlivePing dynamically imports node:fs, node:path, and node:os on every call (every 30 minutes). These modules are already available at module scope in other files. Move imports to the top level.

♻️ Move imports to module level
 import { fileLog, fileLogError } from "./lib/file-logger.ts";
 import { extractFromKeychain, isRefreshInProgress, refreshAnthropicToken } from "./lib/refresh-manager.ts";
 import { checkAnthropicToken, updateAnthropicTokens } from "./lib/token-utils.ts";
+import * as fs from "node:fs";
+import * as path from "node:path";
+import * as os from "node:os";
+
+const AUTH_FILE_PATH = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");

 // ... later in keepAlivePing:
-const fs = await import("node:fs");
-const path = await import("node:path");
-const os = await import("node:os");
-const authFile = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
-const content = fs.readFileSync(authFile, "utf8");
+const content = fs.readFileSync(AUTH_FILE_PATH, "utf8");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/anthropic-token-bridge.ts around lines 22 - 28, The
keepAlivePing function currently does dynamic await imports of node:fs,
node:path, and node:os on every run; move these imports to module scope by
adding top-level imports (or top-level const requires) for fs, path, and os and
then replace the await import(...) usages in keepAlivePing with those top-level
symbols (referencing authFile, content, auth, accessToken usage in the same
function) so the modules are loaded once instead of on every call.

106-106: Redundant condition when thresholds are identical.

status.expiresSoon is already true when timeRemainingMs < REFRESH_THRESHOLD_MS (1 hour). Since PROACTIVE_REFRESH_THRESHOLD_MS is also 1 hour, the condition status.timeRemainingMs < PROACTIVE_REFRESH_THRESHOLD_MS is redundant when expiresSoon is checked.

Either remove the redundant check, or if the thresholds are intended to differ in the future, add a comment explaining the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/anthropic-token-bridge.ts at line 106, The if condition
redundantly checks status.timeRemainingMs < PROACTIVE_REFRESH_THRESHOLD_MS
because status.expiresSoon is already true when timeRemainingMs <
REFRESH_THRESHOLD_MS (both set to 1 hour); update the conditional in
.opencode/plugins/anthropic-token-bridge.ts to remove the redundant clause so it
reads only if (!status.valid || status.expiresSoon) or, if you intend different
thresholds later, add a brief comment next to the condition explaining that
PROACTIVE_REFRESH_THRESHOLD_MS may diverge from REFRESH_THRESHOLD_MS in the
future and should therefore be checked explicitly (referencing status,
expiresSoon, timeRemainingMs, PROACTIVE_REFRESH_THRESHOLD_MS, and
REFRESH_THRESHOLD_MS).
.opencode/plugins/lib/token-utils.js (1)

48-57: Non-atomic file write risks data corruption on concurrent access or crash.

writeAuthFile writes directly to auth.json without an atomic write pattern (write-to-temp-then-rename). If the process crashes mid-write or multiple refresh paths run concurrently, the file can end up corrupted or partially written.

♻️ Proposed atomic write pattern
 function writeAuthFile(authData) {
   try {
-    fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2) + `
-`, { mode: 384 });
+    const tempFile = AUTH_FILE + ".tmp";
+    fs.writeFileSync(tempFile, JSON.stringify(authData, null, 2) + "\n", { mode: 384 });
+    fs.renameSync(tempFile, AUTH_FILE);
     return true;
   } catch (err) {
     error("Failed to write auth.json", { error: String(err) });
     return false;
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/token-utils.js around lines 48 - 57, The writeAuthFile
function currently writes directly to AUTH_FILE causing risk of partial/corrupt
files; change it to an atomic write: write the JSON to a uniquely named
temporary file in the same directory (e.g. AUTH_FILE + '.tmp.' + process.pid +
'.' + Date.now()), fsync the temp file to flush, set the desired mode (was
384/0o600), then rename the temp file over AUTH_FILE using fs.renameSync (atomic
on POSIX), and ensure you remove the temp file on errors; update error handling
in writeAuthFile to reflect these steps and still return true/false as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.opencode/plugins/anthropic-token-bridge.ts:
- Around line 89-129: The config hook currently awaits refreshAnthropicToken(),
which can block plugin init; change the logic in the config method so the
early-refresh is non-blocking: when !status.valid || status.expiresSoon ||
status.timeRemainingMs < PROACTIVE_REFRESH_THRESHOLD_MS, do not await
refreshAnthropicToken() directly—instead, if !isRefreshInProgress() kick off a
fire-and-forget refresh (e.g., start an unawaited Promise) or run
refreshAnthropicToken() with a short timeout/deadline and handle/log its result
asynchronously; ensure startKeepAlive() is still invoked synchronously and
preserve calls to fileLog/fileLogError and the isRefreshInProgress() check so
behavior and logging remain consistent.

In @.opencode/plugins/lib/refresh-manager.js:
- Around line 219-233: The code in refreshWithOAuthToken currently calls
JSON.parse(errorText) which can throw if the response body isn't valid JSON,
bypassing retry/error handling; modify the block inside the !response.ok branch
to parse errorText inside a try-catch, e.g. attempt to set a local errorData by
JSON.parse(errorText) and on parse failure set errorData = {} (and preserve
errorText for logging), then continue using errorData.error?.type for the
invalid_grant check and ensure the 429 retry logic and subsequent error(...)
calls still run without throwing; update references in this function
(refreshWithOAuthToken, MAX_RETRIES, BASE_DELAY_MS, warn, error) accordingly so
a non-JSON response does not crash the flow.

In @.opencode/plugins/lib/refresh-manager.ts:
- Around line 196-216: The code calls JSON.parse on errorText which will throw
if the response body isn't valid JSON; update the error parsing in
refreshWithOAuthToken to safely parse errorText (wrap JSON.parse in a try/catch
or use a safeParse helper) and fall back to an empty object or a minimal {
error?: { message?: string; type?: string } } structure so the existing branches
(429 retry, invalid_grant check, and final error logging) still run; ensure
error logs (error(...) and warn(...)) include the raw errorText when parse fails
so you retain full response details.

---

Outside diff comments:
In @.opencode/plugins/lib/refresh-manager.ts:
- Around line 336-349: The Keychain strategy currently hardcodes
expiresIn=28800; update the Keychain handling so
extractFromKeychain/KeychainTokens include an expiresAt (timestamp) and Strategy
2 computes a real expiresIn from that expiresAt (e.g., floor((expiresAt -
Date.now())/1000)) before calling updateAnthropicTokens(accessToken,
refreshToken, expiresIn); ensure you clamp or fallback to a sensible default
(e.g., 28800) if expiresAt is missing or yields a non-positive value so you
don't write an incorrect expiry into auth.json.

In @.opencode/plugins/lib/token-utils.ts:
- Around line 76-78: Add the same runtime validations present in the compiled
JS: before computing now/expires/timeRemainingMs, ensure anthropic.access is
present (throw or return an explicit error if missing) and validate
anthropic.expires is a finite number (check typeof anthropic.expires ===
"number" && Number.isFinite(anthropic.expires)); only then set expires and
compute timeRemainingMs. Update the token handling logic in token-utils.ts where
now, expires, and timeRemainingMs are computed to perform these checks for
anthropic.access and anthropic.expires and fail fast if they are invalid.

---

Nitpick comments:
In @.opencode/plugins/anthropic-token-bridge.ts:
- Around line 22-28: The keepAlivePing function currently does dynamic await
imports of node:fs, node:path, and node:os on every run; move these imports to
module scope by adding top-level imports (or top-level const requires) for fs,
path, and os and then replace the await import(...) usages in keepAlivePing with
those top-level symbols (referencing authFile, content, auth, accessToken usage
in the same function) so the modules are loaded once instead of on every call.
- Line 106: The if condition redundantly checks status.timeRemainingMs <
PROACTIVE_REFRESH_THRESHOLD_MS because status.expiresSoon is already true when
timeRemainingMs < REFRESH_THRESHOLD_MS (both set to 1 hour); update the
conditional in .opencode/plugins/anthropic-token-bridge.ts to remove the
redundant clause so it reads only if (!status.valid || status.expiresSoon) or,
if you intend different thresholds later, add a brief comment next to the
condition explaining that PROACTIVE_REFRESH_THRESHOLD_MS may diverge from
REFRESH_THRESHOLD_MS in the future and should therefore be checked explicitly
(referencing status, expiresSoon, timeRemainingMs,
PROACTIVE_REFRESH_THRESHOLD_MS, and REFRESH_THRESHOLD_MS).

In @.opencode/plugins/lib/token-utils.js:
- Around line 48-57: The writeAuthFile function currently writes directly to
AUTH_FILE causing risk of partial/corrupt files; change it to an atomic write:
write the JSON to a uniquely named temporary file in the same directory (e.g.
AUTH_FILE + '.tmp.' + process.pid + '.' + Date.now()), fsync the temp file to
flush, set the desired mode (was 384/0o600), then rename the temp file over
AUTH_FILE using fs.renameSync (atomic on POSIX), and ensure you remove the temp
file on errors; update error handling in writeAuthFile to reflect these steps
and still return true/false as before.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0dc386af-3474-4bd3-8eed-cc0f62f0eed7

📥 Commits

Reviewing files that changed from the base of the PR and between 43adf8d and f41e5cf.

📒 Files selected for processing (8)
  • .opencode/plugins/anthropic-token-bridge.js
  • .opencode/plugins/anthropic-token-bridge.ts
  • .opencode/plugins/lib/refresh-manager.js
  • .opencode/plugins/lib/refresh-manager.ts
  • .opencode/plugins/lib/token-utils.js
  • .opencode/plugins/lib/token-utils.ts
  • contrib/anthropic-max-bridge/README.md
  • contrib/anthropic-max-bridge/TECHNICAL.md

Comment thread .opencode/plugins/anthropic-token-bridge.ts
Comment thread .opencode/plugins/lib/refresh-manager.js
Comment thread .opencode/plugins/lib/refresh-manager.ts
- anthropic-token-bridge: config hook is now fire-and-forget (non-blocking
  plugin init); module-scope imports for fs/path/os replace per-call dynamic
  imports in keepAlivePing; remove redundant timeRemainingMs check (expiresSoon
  already captures it at the same 1h threshold)
- refresh-manager: wrap JSON.parse(errorText) in try/catch so non-JSON
  responses (HTML error pages) do not crash the retry/error flow; add
  KeychainTokens.expiresAt field and return it from extractFromKeychain; compute
  real expiresIn from Keychain expiresAt in Strategy 2 (fallback 8h if missing);
  rename existingRefreshToken param to oauthToken to pass CI secret scanner;
  export extractFromKeychain so anthropic-token-bridge can use it
- token-utils: fail-fast validation for missing access token and non-finite
  expires value before computing timeRemainingMs; atomic writeAuthFile via
  tmp-file + renameSync with mode 0o600

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
.opencode/plugins/anthropic-token-bridge.js (1)

409-409: ⚠️ Potential issue | 🟡 Minor

Same pipeline failure as refresh-manager.js — variable name triggers secret scanner.

This is the same false positive discussed in refresh-manager.js. The fix should be applied in the TypeScript source and recompiled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/anthropic-token-bridge.js at line 409, The variable name
storedRefreshToken in the anthropic token bridge should be renamed in the
TypeScript source to avoid triggering the secret scanner (e.g., rename to
existingRefreshToken or storedRefreshTokenSafe) and all references updated to
the new identifier; update the declaration that currently calls
getExistingRefreshToken() and any subsequent usages (in the same module or
exported APIs) to use the new name, then recompile the TypeScript to regenerate
.js artifacts so the change is reflected in
.opencode/plugins/anthropic-token-bridge.js.
🧹 Nitpick comments (5)
.opencode/plugins/anthropic-token-bridge.ts (3)

24-28: Consider reusing readAuthFile() from token-utils.

This duplicates the auth.json reading logic. Using readAuthFile() would provide consistent error handling and reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/anthropic-token-bridge.ts around lines 24 - 28, Replace
the duplicated auth.json read logic in anthropic-token-bridge.ts by importing
and calling readAuthFile() from token-utils; use the returned object to obtain
auth.anthropic?.access (assign to accessToken) and preserve the existing
missing-token handling; update any local variables (auth, accessToken) to use
the readAuthFile() result so you reuse its standardized error handling and
parsing instead of fs.readFileSync/JSON.parse.

195-201: Minor: Log message says "after fallback check" but this is the fast-path.

Line 199 logs "(after fallback check)" but this code block is the fast-path that skips Keychain sync when the token is already valid. The same message appears at line 276 where it's accurate. Consider clarifying the fast-path log.

📝 Suggested log message fix
-				fileLog(`Token valid for ${hoursRemaining}h at session start (after fallback check)`, "info");
+				fileLog(`Token valid for ${hoursRemaining}h at session start (fast-path, skipping Keychain sync)`, "info");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/anthropic-token-bridge.ts around lines 195 - 201, The log
message inside the fast-path that checks checkAnthropicToken() is misleading;
update the fileLog call that currently says "Token valid for ${hoursRemaining}h
at session start (after fallback check)" to reflect the fast-path (e.g., "Token
valid for ${hoursRemaining}h at session start (fast-path)"), referencing the
quickCheck variable and keeping the same hoursRemaining calculation so the
behavior in the if block (quickCheck.valid && !quickCheck.expiresSoon) and the
early return remain unchanged.

156-158: Redundant condition in needsRefresh check.

Since PROACTIVE_REFRESH_THRESHOLD_MS equals REFRESH_THRESHOLD_MS (both 60 minutes), and status.expiresSoon is already true when timeRemainingMs < REFRESH_THRESHOLD_MS, the third condition is redundant.

♻️ Simplified condition
-			const needsRefresh = !status.valid || status.expiresSoon || status.timeRemainingMs < PROACTIVE_REFRESH_THRESHOLD_MS;
+			const needsRefresh = !status.valid || status.expiresSoon;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/anthropic-token-bridge.ts around lines 156 - 158, The
needsRefresh expression contains a redundant check: because
PROACTIVE_REFRESH_THRESHOLD_MS equals REFRESH_THRESHOLD_MS and
status.expiresSoon is set when timeRemainingMs < REFRESH_THRESHOLD_MS, remove
the duplicate timeRemainingMs comparison. Update the computation of needsRefresh
(the variable named needsRefresh) to simply be: !status.valid ||
status.expiresSoon (or, if you intended a different threshold, adjust
PROACTIVE_REFRESH_THRESHOLD_MS instead), referencing status.expiresSoon,
status.valid, and PROACTIVE_REFRESH_THRESHOLD_MS/REFRESH_THRESHOLD_MS to locate
the logic.
.opencode/plugins/lib/refresh-manager.ts (1)

374-376: Minor: Stale comment references "Strategy 3" twice.

The comment at line 374-375 says "Strategy 3" but the setup-token exchange is part of the same strategy as generating the token. Consider updating for clarity.

📝 Suggested comment fix
-		// Strategy 3: Exchange setup token for OAuth credentials
-		// (numbered 3 here because Strategy 1=OAuth refresh, Strategy 2=Keychain)
+		// Exchange setup token for OAuth credentials
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/refresh-manager.ts around lines 374 - 376, Update the
stale comment above the call to exchangeSetupToken(setupToken) to reflect that
this step is part of the same strategy as generating the setup token (e.g.,
"Strategy 2 (continued): Exchange setup token for OAuth credentials" or similar)
so it's not labeled "Strategy 3"; locate the comment near the exchangeSetupToken
call in refresh-manager.ts and adjust wording to clarify continuation of the
previous strategy.
.opencode/plugins/lib/token-utils.js (1)

38-38: Consider extracting REFRESH_THRESHOLD_MS to a shared constant.

This constant (60 * 60 * 1000) is duplicated as PROACTIVE_REFRESH_THRESHOLD_MS in anthropic-token-bridge.ts (line 9). While both currently represent 60 minutes, independent definitions could drift over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/token-utils.js at line 38, Extract the duplicated
magic number into a single shared constant (e.g., export const ONE_HOUR_MS or
REFRESH_THRESHOLD_MS) in a common module and replace both REFRESH_THRESHOLD_MS
in token-utils.js and PROACTIVE_REFRESH_THRESHOLD_MS in
anthropic-token-bridge.ts with imports from that module; update references to
use the shared constant name and remove the local definitions so both modules
consume the same source of truth and avoid drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.opencode/plugins/lib/refresh-manager.js:
- Line 319: Rename the variable storedRefreshToken to a non-secret-looking
identifier (e.g., existingRefresh or refreshTokenValue) where it's declared and
used (the call site using getExistingRefreshToken()) in refresh-manager so the
secret scanner no longer triggers; alternatively ensure the TS source comment
"// pragma: allowlist secret" remains on the declaration in refresh-manager.ts
and is preserved through compilation so the compiled output contains the
allowlist pragma. Make the change consistently across the function/method that
references getExistingRefreshToken() and any downstream uses to avoid breaking
references.

---

Duplicate comments:
In @.opencode/plugins/anthropic-token-bridge.js:
- Line 409: The variable name storedRefreshToken in the anthropic token bridge
should be renamed in the TypeScript source to avoid triggering the secret
scanner (e.g., rename to existingRefreshToken or storedRefreshTokenSafe) and all
references updated to the new identifier; update the declaration that currently
calls getExistingRefreshToken() and any subsequent usages (in the same module or
exported APIs) to use the new name, then recompile the TypeScript to regenerate
.js artifacts so the change is reflected in
.opencode/plugins/anthropic-token-bridge.js.

---

Nitpick comments:
In @.opencode/plugins/anthropic-token-bridge.ts:
- Around line 24-28: Replace the duplicated auth.json read logic in
anthropic-token-bridge.ts by importing and calling readAuthFile() from
token-utils; use the returned object to obtain auth.anthropic?.access (assign to
accessToken) and preserve the existing missing-token handling; update any local
variables (auth, accessToken) to use the readAuthFile() result so you reuse its
standardized error handling and parsing instead of fs.readFileSync/JSON.parse.
- Around line 195-201: The log message inside the fast-path that checks
checkAnthropicToken() is misleading; update the fileLog call that currently says
"Token valid for ${hoursRemaining}h at session start (after fallback check)" to
reflect the fast-path (e.g., "Token valid for ${hoursRemaining}h at session
start (fast-path)"), referencing the quickCheck variable and keeping the same
hoursRemaining calculation so the behavior in the if block (quickCheck.valid &&
!quickCheck.expiresSoon) and the early return remain unchanged.
- Around line 156-158: The needsRefresh expression contains a redundant check:
because PROACTIVE_REFRESH_THRESHOLD_MS equals REFRESH_THRESHOLD_MS and
status.expiresSoon is set when timeRemainingMs < REFRESH_THRESHOLD_MS, remove
the duplicate timeRemainingMs comparison. Update the computation of needsRefresh
(the variable named needsRefresh) to simply be: !status.valid ||
status.expiresSoon (or, if you intended a different threshold, adjust
PROACTIVE_REFRESH_THRESHOLD_MS instead), referencing status.expiresSoon,
status.valid, and PROACTIVE_REFRESH_THRESHOLD_MS/REFRESH_THRESHOLD_MS to locate
the logic.

In @.opencode/plugins/lib/refresh-manager.ts:
- Around line 374-376: Update the stale comment above the call to
exchangeSetupToken(setupToken) to reflect that this step is part of the same
strategy as generating the setup token (e.g., "Strategy 2 (continued): Exchange
setup token for OAuth credentials" or similar) so it's not labeled "Strategy 3";
locate the comment near the exchangeSetupToken call in refresh-manager.ts and
adjust wording to clarify continuation of the previous strategy.

In @.opencode/plugins/lib/token-utils.js:
- Line 38: Extract the duplicated magic number into a single shared constant
(e.g., export const ONE_HOUR_MS or REFRESH_THRESHOLD_MS) in a common module and
replace both REFRESH_THRESHOLD_MS in token-utils.js and
PROACTIVE_REFRESH_THRESHOLD_MS in anthropic-token-bridge.ts with imports from
that module; update references to use the shared constant name and remove the
local definitions so both modules consume the same source of truth and avoid
drift.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 90545ab0-b6c7-47e8-bc82-3ba1f90198eb

📥 Commits

Reviewing files that changed from the base of the PR and between f41e5cf and ebf920b.

📒 Files selected for processing (6)
  • .opencode/plugins/anthropic-token-bridge.js
  • .opencode/plugins/anthropic-token-bridge.ts
  • .opencode/plugins/lib/refresh-manager.js
  • .opencode/plugins/lib/refresh-manager.ts
  • .opencode/plugins/lib/token-utils.js
  • .opencode/plugins/lib/token-utils.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .opencode/plugins/lib/token-utils.ts

lastRefreshAttempt = Date.now();
try {
info("Starting token refresh process");
const storedRefreshToken = getExistingRefreshToken();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Pipeline failure: Secret scanner false positive on variable name.

The CI secret scanner flagged storedRefreshToken because the variable name matches common secret patterns. This is a false positive—the value is read at runtime from auth.json, not hardcoded.

To resolve the CI failure, consider renaming the variable or ensuring the TS source's // pragma: allowlist secret comment is preserved in the compiled output.

🔧 Option: Rename variable to avoid scanner pattern
-  const storedRefreshToken = getExistingRefreshToken();
+  const existingOauthRefresh = getExistingRefreshToken();

Apply this change in the TypeScript source (.opencode/plugins/lib/refresh-manager.ts line 324) and recompile.

🧰 Tools
🪛 GitHub Actions: CI — Lint, Typecheck, Secrets

[error] 319-319: Secret-scan matched this line while searching for common secret patterns (e.g., sk- keys or api_key/client_secret/password/token assignments with 20+ char values). Context shown: "const storedRefreshToken = getExistingRefreshToken();"

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/refresh-manager.js at line 319, Rename the variable
storedRefreshToken to a non-secret-looking identifier (e.g., existingRefresh or
refreshTokenValue) where it's declared and used (the call site using
getExistingRefreshToken()) in refresh-manager so the secret scanner no longer
triggers; alternatively ensure the TS source comment "// pragma: allowlist
secret" remains on the declaration in refresh-manager.ts and is preserved
through compilation so the compiled output contains the allowlist pragma. Make
the change consistently across the function/method that references
getExistingRefreshToken() and any downstream uses to avoid breaking references.

- rename storedRefreshToken → oauthRefreshValue so CI secret scanner no
  longer flags the variable at the getExistingRefreshToken() call site
- keepAlivePing: replace manual fs.readFileSync/JSON.parse with readAuthFile()
  from token-utils; export readAuthFile and AuthFile interface for reuse
- remove unused fs/os/path module-scope imports from anthropic-token-bridge
  (no longer needed after keepAlivePing refactor)
- remove redundant timeRemainingMs comparison in chat.message needsRefresh:
  expiresSoon already captures it at the same 1h threshold
- fix misleading log message in system.transform fast-path:
  'after fallback check' → 'fast-path, no Keychain sync needed'
- fix stale 'Strategy 3' duplicate comment above exchangeSetupToken call:
  now reads 'Strategy 3 (continued)'
@Steffen025

Copy link
Copy Markdown
Owner Author

@coderabbitai is the review for #63b121d still in the way?

@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List all review comments on PR `#88` and their resolution status
gh pr view 88 --repo Steffen025/pai-opencode --json reviews,comments

# Also check review threads
gh api repos/Steffen025/pai-opencode/pulls/88/reviews --jq '.[] | {id, state, body, submitted_at}'

Length of output: 83936


🏁 Script executed:

#!/bin/bash
# List all inline pull request review comments and their state
gh api repos/Steffen025/pai-opencode/pulls/88/comments \
  --jq '.[] | {id, path, line, body: (.body | .[0:150]), in_reply_to_id, created_at}' \
  2>&1 | head -200

echo "---"
# Also check if commit 63b121d exists in this PR
gh api repos/Steffen025/pai-opencode/pulls/88/commits \
  --jq '.[] | {sha, commit: .commit.message}' 2>&1

Length of output: 3957


@Steffen025 No, commit 63b121d is not blocking — in fact, it's the latest commit on this PR and it fully addresses all the outstanding review comments from both review rounds:

  • Secret scanner false positive (storedRefreshTokenoauthRefreshValue)
  • Duplicated auth.json read logic in keepAlivePing (now uses readAuthFile())
  • Redundant timeRemainingMs check removed from chat.message needsRefresh
  • Misleading log message fixed ("after fallback check""fast-path, no Keychain sync needed")
  • Stale "Strategy 3" comment corrected

The previous commit ebf920b also addressed the earlier round (non-blocking config hook, JSON.parse safety, KeychainTokens.expiresAt, token-utils fail-fast validation).

All actionable comments from my reviews have been resolved in code. Would you like me to resolve all my open comment threads on this PR?

[resolve]


🧠 Learnings used
Learnt from: CR
Repo: Steffen025/pai-opencode PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-16T22:43:53.426Z
Learning: Resolve [BLOCKING] CodeRabbit comments before merging a PR

@Steffen025
Steffen025 merged commit 86d1d6f into dev Mar 24, 2026
2 checks passed
@Steffen025
Steffen025 deleted the fix/oauth-silent-refresh-dev branch March 24, 2026 04:06
Steffen025 added a commit that referenced this pull request Mar 24, 2026
…ync (#88+#89)

- OAuth2 refresh_token strategy as Strategy 1: direct POST to
  console.anthropic.com/v1/oauth/token — silent, no browser, no
  claude CLI dependency. Fixes Weston/Ben failures on day 2 when
  tokens expired.
- Atomic writeAuthFile (tmp file + rename) prevents corrupt auth.json
  on interrupted writes.
- startKeepAlive 30-min pings to prevent token expiry.
- config() startup hook for early token check on plugin load.
- Refresh threshold tightened: 2h → 1h proactive refresh window.
- anthropic-max-bridge.js: remove broken output.system normalization
  guard that caused map() on undefined.
- All three copies kept in sync: .opencode/plugins, contrib/plugins,
  and TS source (lib/).
Steffen025 added a commit that referenced this pull request Mar 24, 2026
… sync (#90)

* fix(oauth): OAuth2 silent refresh, atomic writes, full token-bridge sync (#88+#89)

- OAuth2 refresh_token strategy as Strategy 1: direct POST to
  console.anthropic.com/v1/oauth/token — silent, no browser, no
  claude CLI dependency. Fixes Weston/Ben failures on day 2 when
  tokens expired.
- Atomic writeAuthFile (tmp file + rename) prevents corrupt auth.json
  on interrupted writes.
- startKeepAlive 30-min pings to prevent token expiry.
- config() startup hook for early token check on plugin load.
- Refresh threshold tightened: 2h → 1h proactive refresh window.
- anthropic-max-bridge.js: remove broken output.system normalization
  guard that caused map() on undefined.
- All three copies kept in sync: .opencode/plugins, contrib/plugins,
  and TS source (lib/).

* fix(oauth): address CodeRabbit review findings

- anthropic-token-bridge.ts: add keepaliveInitialTimeout module-level var;
  startKeepAlive() now clears it before creating a new setTimeout, so
  repeated calls don't stack untracked initial timeouts. Assign return
  value to keepaliveInitialTimeout; set keepaliveTimer via setInterval
  inside the callback as before.

- refresh-manager.ts extractFromKeychain: destructure real stderr from
  execCommand (was shadowing stdout into the stderr field of the error log).

- refresh-manager.ts exchangeSetupToken: add AbortController + 10s timeout
  with clearTimeout in finally — mirrors refreshWithOAuthToken; prevents
  the fetch hanging indefinitely.

- refresh-manager.ts getExistingRefreshToken: replace manual fs read/parse
  with the shared readAuthFile() helper from token-utils; remove now-unused
  fs/os/path imports and AUTH_FILE2 variable from refresh-manager.

- anthropic-token-bridge.js config hook: remove redundant
  timeRemainingMs < PROACTIVE_REFRESH_THRESHOLD_MS check; expiresSoon
  already encodes this via REFRESH_THRESHOLD_MS (same value). Same fix
  in chat.message handler.

- anthropic-token-bridge.js keepAlivePing: replace dynamic
  import('node:fs/path/os') block with a call to the already-available
  readAuthFile() helper.

- contrib/anthropic-max-bridge/README.md: insert blank line between
  the [!important] and [!tip] callout blocks so parsers render them
  independently (MD028).

- contrib/anthropic-max-bridge/TECHNICAL.md: add 'http' language
  identifier to the two bare fenced code blocks (oauth-beta header
  and Bearer/x-api-key comparison).

- contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js:
  synced byte-for-byte with .opencode/plugins copy.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant