opencode-claude-auth 2.1.4 → 2.1.6

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.
package/README.md CHANGED
@@ -151,14 +151,19 @@ This reads your stored credentials, calls Anthropic's OAuth token endpoint, and
151
151
 
152
152
  All configurable parameters can be overridden via environment variables. If Anthropic changes something before we publish an update, set an env var and keep working:
153
153
 
154
- | Variable | Description | Default |
155
- | ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
156
- | `ANTHROPIC_CLI_VERSION` | Claude CLI version for user-agent and billing headers | `config.ccVersion` in [`src/model-config.ts`](src/model-config.ts) |
157
- | `ANTHROPIC_USER_AGENT` | Full User-Agent string (overrides CLI version) | `claude-cli/{version} (external, sdk-cli)` |
158
- | `ANTHROPIC_BETA_FLAGS` | Comma-separated beta feature flags | `baseBetas` list in [`src/model-config.ts`](src/model-config.ts) |
159
- | `CLAUDE_AUTH_DEBUG` | Enable diagnostic logging (`1` for default path, or a custom file path) | disabled |
160
- | `CLAUDE_CONFIG_DIR` | Claude Code config directory used for the credentials-file fallback (reads `$CLAUDE_CONFIG_DIR/.credentials.json`). macOS still checks the Keychain first. | `~/.claude` |
161
- | `OPENCODE_CLAUDE_AUTH_MAX_RETRY_MS` | Max ms the plugin waits when honouring a 429/529 `retry-after` header. Beyond this cap the response surfaces immediately so OpenCode doesn't appear to hang on hour-long quota resets. | `30000` |
154
+ | Variable | Description | Default |
155
+ | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
156
+ | `ANTHROPIC_CLI_VERSION` | Claude CLI version for user-agent and billing headers | `config.ccVersion` in [`src/model-config.ts`](src/model-config.ts) |
157
+ | `ANTHROPIC_USER_AGENT` | Full User-Agent string (overrides CLI version) | `claude-cli/{version} (external, sdk-cli)` |
158
+ | `ANTHROPIC_BETA_FLAGS` | Comma-separated beta feature flags | `baseBetas` list in [`src/model-config.ts`](src/model-config.ts) |
159
+ | `CLAUDE_AUTH_DEBUG` | Enable diagnostic logging (`1` for default path, or a custom file path) | disabled |
160
+ | `CLAUDE_CONFIG_DIR` | Claude Code config directory used for the credentials-file fallback (reads `$CLAUDE_CONFIG_DIR/.credentials.json`). macOS still checks the Keychain first. | `~/.claude` |
161
+ | `OPENCODE_CLAUDE_AUTH_MAX_RETRY_MS` | Max ms the plugin waits when honouring a 429/529 `retry-after` header. Beyond this cap the response surfaces immediately so OpenCode doesn't appear to hang on hour-long quota resets. | `30000` |
162
+ | `OPENCODE_CLAUDE_AUTH_TOOL_REPAIR` | Strategy for reconciling `tool_use`/`tool_result` adjacency broken by OpenCode auto-compaction. `placeholder` synthesizes a paired result for orphaned `tool_use` blocks (lossless, preserves `thinking` blocks); `drop` removes orphaned blocks (omitting whole thinking turns). | `placeholder` |
163
+ | `OPENCODE_CLAUDE_AUTH_REFRESH_WAIT_MS` | Max ms a single request waits through a transient token-refresh rate-limit (429) before returning a retryable error instead of a hard "run `claude`". | `45000` |
164
+ | `OPENCODE_CLAUDE_AUTH_REFRESH_COOLDOWN_MS` | Base per-account cooldown after a rate-limited refresh, before the plugin retries the token endpoint. Escalates with consecutive failures and is jittered; capped at 60s. | `15000` |
165
+ | `OPENCODE_CLAUDE_AUTH_REFRESH_LOCK_TTL_MS` | TTL for the cross-process refresh lock. A held lock older than this is treated as stale (crashed holder) and taken over. | `20000` |
166
+ | `OPENCODE_CLAUDE_AUTH_REFRESH_LOCK_DIR` | Directory for the advisory cross-process refresh lock files. | OpenCode data dir (`~/.local/share/opencode`) |
162
167
 
163
168
  Example:
164
169
 
@@ -176,10 +181,15 @@ export ANTHROPIC_CLI_VERSION=2.2.0
176
181
  - Sets required API headers (beta flags, billing, user-agent) with model-aware selection
177
182
  - On macOS, enumerates all `Claude Code-credentials*` Keychain entries and labels them by subscription tier
178
183
  - Provides an account switcher via `opencode auth login` when multiple accounts are found; persists selection to `~/.local/share/opencode/claude-account-source.txt`
179
- - Syncs credentials to `auth.json` on startup and every 5 minutes as a fallback (sync never triggers refresh; refresh is lazy, only on API requests)
184
+ - Syncs credentials to `auth.json` on startup and every 5 minutes as a fallback; that same tick proactively refreshes once the token is within an hour of expiry
180
185
  - On Windows, writes to both `%USERPROFILE%\.local\share\opencode\auth.json` and `%LOCALAPPDATA%\opencode\auth.json`
186
+ - Re-reads the credential source on every cache miss, so an account rotated by something other than this plugin — the `claude` CLI in another terminal, a second OpenCode instance, or a switcher like [claude-swap](https://github.com/realiti4/claude-swap) — gets picked up mid-session without a restart. Bounded by the same 30s cache, so it adds at most about two source reads a minute under load. A stored token is adopted whenever it is usable, and when it isn't only if the one already held is also unusable — otherwise a failed write-back would resurrect the pre-refresh token it left behind
187
+ - Guards credential write-back with the access token the refresh started from, so a switch landing mid-refresh can't write one account's rotated tokens into another account's slot
181
188
  - Retries API requests on 429 (rate limit) and 529 (overloaded) with exponential backoff, respecting `retry-after` headers
182
- - When a token is within 60 seconds of expiry, refreshes directly via `POST https://claude.ai/v1/oauth/token` (no LLM tokens consumed). Falls back to `claude` CLI if the direct refresh fails. New tokens are written back to Keychain (macOS) or credentials file (Linux/Windows) to keep stored credentials in sync with rotated refresh tokens
189
+ - On a 429 that outlives those backoff retries, re-reads the source once and retries only if the access token changed, so a rate limit another process has already resolved by switching accounts isn't surfaced. A changed token isn't proof of a switch a routine refresh of the same account changes it too so this costs at most one extra request
190
+ - On a 401, recovers in place rather than surfacing it: adopts an externally rotated token if the source now holds one, otherwise forces an OAuth refresh, then retries the request. Bounded at two attempts, so a rejected token costs at most three API calls. A 401 that survives recovery is returned unmodified, without SSE stream transformation, since it carries an error body rather than a stream
191
+ - Refreshes directly via `POST https://claude.ai/v1/oauth/token` using the runtime's own `fetch` (no LLM tokens consumed, no subprocess). Requests are triggered within 60 seconds of expiry on the API request path and within an hour on the background tick; concurrent refreshes of one account share a single request, since each rotation invalidates the previous refresh token
192
+ - Falls back to the `claude` CLI only within the 60-second window, the point at which Claude Code will actually rotate the token — running it earlier costs a real API request and returns the same token. New tokens are written back to Keychain (macOS) or credentials file (Linux/Windows) to keep stored credentials in sync with rotated refresh tokens
183
193
  - If credentials aren't OAuth-based, the auth loader returns `{}` and falls through to API key auth
184
194
  - If credentials are unavailable or unreadable, the plugin disables itself and OpenCode continues without Claude auth
185
195
 
@@ -1,4 +1,5 @@
1
1
  import { type ClaudeAccount, type ClaudeCredentials } from "./keychain.ts";
2
+ import { type RefreshFailureKind } from "./refresh-backoff.ts";
2
3
  export type { ClaudeAccount } from "./keychain.ts";
3
4
  export type { ClaudeCredentials } from "./keychain.ts";
4
5
  export declare function initAccounts(accounts: ClaudeAccount[]): void;
@@ -11,7 +12,59 @@ export declare function syncAuthJson(creds: ClaudeCredentials): void;
11
12
  export declare const OAUTH_TOKEN_URL = "https://claude.ai/v1/oauth/token";
12
13
  export declare const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
13
14
  export declare function parseOAuthResponse(raw: string, currentRefreshToken: string, now?: number): ClaudeCredentials | null;
14
- export declare function refreshViaOAuth(refreshToken: string): ClaudeCredentials | null;
15
+ /**
16
+ * Extract the non-secret failure reason from an OAuth token-endpoint error
17
+ * body so a refresh failure is diagnosable from the debug log. Handles both the
18
+ * OAuth shape (`{ error, error_description }`) and Anthropic's API error
19
+ * envelope (`{ error: { type, message } }`). Values are truncated and never
20
+ * include tokens; the logger additionally redacts anything JWT-shaped.
21
+ */
22
+ export declare function extractOAuthError(raw: string): {
23
+ oauthError?: string;
24
+ oauthErrorDescription?: string;
25
+ };
26
+ /**
27
+ * Exchanges a refresh token for fresh credentials using the runtime's own
28
+ * fetch.
29
+ *
30
+ * This previously ran the request inside a child process spawned as
31
+ * `process.execPath -e <script>`. That assumed process.execPath is a
32
+ * JavaScript runtime, which does not hold inside OpenCode: the plugin runs
33
+ * in a compiled single-file executable, so process.execPath is the OpenCode
34
+ * binary itself and `-e` is not a script to evaluate. Every refresh exited
35
+ * non-zero with empty stdout and silently fell through to the claude CLI.
36
+ * Node 18+ and Bun both expose a global fetch, so no subprocess is needed.
37
+ */
38
+ /**
39
+ * Classified result of an OAuth refresh. A `transient` outcome (429/5xx/network
40
+ * /`rate_limit_error`) means the refresh token is still good and the caller
41
+ * should back off and retry rather than surface a hard error; a `terminal`
42
+ * outcome (`invalid_grant`, ...) means the refresh token is dead.
43
+ */
44
+ export type RefreshOutcome = {
45
+ kind: "ok";
46
+ creds: ClaudeCredentials;
47
+ } | {
48
+ kind: "transient";
49
+ status: number;
50
+ oauthError?: string;
51
+ retryAfterMs?: number;
52
+ } | {
53
+ kind: "terminal";
54
+ status: number;
55
+ oauthError?: string;
56
+ };
57
+ /**
58
+ * Exchange a refresh token for fresh credentials and classify the result.
59
+ * See {@link RefreshOutcome}. Uses the runtime's own fetch (no subprocess).
60
+ */
61
+ export declare function refreshViaOAuthDetailed(refreshToken: string, timeoutMs?: number): Promise<RefreshOutcome>;
62
+ /**
63
+ * Backward-compatible wrapper: returns credentials on success, else null.
64
+ * Prefer {@link refreshViaOAuthDetailed} when the transient/terminal
65
+ * distinction matters (cooldown, CLI-fallback gating).
66
+ */
67
+ export declare function refreshViaOAuth(refreshToken: string, timeoutMs?: number): Promise<ClaudeCredentials | null>;
15
68
  /**
16
69
  * Refreshes the given (or active) account's credentials if they are within
17
70
  * `thresholdMs` of expiry. Defaults to 60s, matching the reactive
@@ -21,13 +74,19 @@ export declare function refreshViaOAuth(refreshToken: string): ClaudeCredentials
21
74
  * of threshold, so this always operates on the currently active account
22
75
  * unless one is explicitly passed in.
23
76
  */
24
- export declare function refreshIfNeeded(account?: ClaudeAccount, thresholdMs?: number): ClaudeCredentials | null;
77
+ export declare function refreshIfNeeded(account?: ClaudeAccount, thresholdMs?: number): Promise<ClaudeCredentials | null>;
25
78
  export declare function getCredentialsForSync(): ClaudeCredentials | null;
26
79
  /**
27
80
  * Re-read only the active account's credentials from its source (single
28
- * keychain service read or credentials file) and update them in place.
29
- * Used on 401 so an externally refreshed token is picked up without a
30
- * full multi-account keychain rescan.
81
+ * keychain service read or credentials file) and update them in place,
82
+ * so an externally refreshed token is picked up without a full
83
+ * multi-account keychain rescan.
84
+ *
85
+ * Currently has no call sites: the 401 path uses
86
+ * reloadCredentialsFromSource, which additionally validates the result
87
+ * and refreshes the cache. Wiring this up or deleting it is tracked as a
88
+ * follow-up; until then it must stay consistent with the read paths that
89
+ * are live, hence the configDir below.
31
90
  */
32
91
  export declare function reloadActiveAccount(): void;
33
92
  /**
@@ -37,7 +96,7 @@ export declare function reloadActiveAccount(): void;
37
96
  * On success the account, its source, and the cache are all updated.
38
97
  * The refresh function is injectable for tests.
39
98
  */
40
- export declare function forceRefreshActiveAccount(refresh?: (refreshToken: string) => ClaudeCredentials | null): ClaudeCredentials | null;
99
+ export declare function forceRefreshActiveAccount(refresh?: (refreshToken: string) => Promise<ClaudeCredentials | null>): Promise<ClaudeCredentials | null>;
41
100
  /**
42
101
  * Drop the active account's cached credentials so the next
43
102
  * getCachedCredentials() call re-reads from the source, bypassing the
@@ -45,6 +104,30 @@ export declare function forceRefreshActiveAccount(refresh?: (refreshToken: strin
45
104
  * valid locally.
46
105
  */
47
106
  export declare function invalidateCredentialCache(): void;
48
- export declare function getCachedCredentials(): ClaudeCredentials | null;
107
+ export declare function getCachedCredentials(): Promise<ClaudeCredentials | null>;
108
+ export interface CredentialWaitOptions {
109
+ maxWaitMs?: number;
110
+ pollMs?: number;
111
+ signal?: AbortSignal;
112
+ now?: () => number;
113
+ sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
114
+ rng?: () => number;
115
+ }
116
+ /**
117
+ * Resolve credentials, waiting through a transient refresh rate-limit rather
118
+ * than failing hard. Returns as soon as a token is available — ours refreshed
119
+ * once the cooldown clears, or a sibling OpenCode instance / the `claude` CLI
120
+ * wrote a fresh one to the shared store. Returns null promptly on a terminal
121
+ * failure (dead refresh token) or when the wait budget is exhausted, so the
122
+ * caller can decide between a retryable response and a hard error.
123
+ */
124
+ export declare function getCredentialsWithBackoff(opts?: CredentialWaitOptions): Promise<ClaudeCredentials | null>;
125
+ /**
126
+ * Whether the active account's most recent refresh failure was transient
127
+ * (rate-limited/retryable) or terminal (dead refresh token), for callers
128
+ * deciding between a retryable response and a hard "re-authenticate" error.
129
+ * An active cooldown implies a transient failure.
130
+ */
131
+ export declare function getActiveRefreshFailureKind(): RefreshFailureKind | null;
49
132
  export declare function reloadCredentialsFromSource(): ClaudeCredentials | null;
50
133
  //# sourceMappingURL=credentials.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACvB,MAAM,eAAe,CAAA;AAItB,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAClD,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAA;AAWtD,wBAAgB,YAAY,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,IAAI,CAE5D;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAQ3D;AAED,wBAAgB,mBAAmB,IAAI,aAAa,EAAE,CAUrD;AAED,wBAAgB,gBAAgB,IAAI,aAAa,GAAG,IAAI,CAOvD;AAYD,wBAAgB,0BAA0B,IAAI,MAAM,GAAG,IAAI,CAU1D;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAStD;AA4CD,wBAAgB,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAc3D;AAED,eAAO,MAAM,eAAe,qCAAqC,CAAA;AACjE,eAAO,MAAM,eAAe,yCAAyC,CAAA;AAErE,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,mBAAmB,EAAE,MAAM,EAC3B,GAAG,GAAE,MAAmB,GACvB,iBAAiB,GAAG,IAAI,CAoB1B;AAED,wBAAgB,eAAe,CAC7B,YAAY,EAAE,MAAM,GACnB,iBAAiB,GAAG,IAAI,CAiD1B;AA0CD;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,OAAO,CAAC,EAAE,aAAa,EACvB,WAAW,SAAS,GACnB,iBAAiB,GAAG,IAAI,CA0E1B;AAyCD,wBAAgB,qBAAqB,IAAI,iBAAiB,GAAG,IAAI,CAUhE;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,IAAI,IAAI,CAY1C;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,GAAE,CAAC,YAAY,EAAE,MAAM,KAAK,iBAAiB,GAAG,IAAsB,GAC5E,iBAAiB,GAAG,IAAI,CAqB1B;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,IAAI,IAAI,CAMhD;AAED,wBAAgB,oBAAoB,IAAI,iBAAiB,GAAG,IAAI,CAgC/D;AAED,wBAAgB,2BAA2B,IAAI,iBAAiB,GAAG,IAAI,CA0CtE"}
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"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"}