opencode-anthropic-fix 0.0.17 → 0.0.18

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/package.json CHANGED
@@ -1,7 +1,13 @@
1
1
  {
2
2
  "name": "opencode-anthropic-fix",
3
- "version": "0.0.17",
3
+ "version": "0.0.18",
4
4
  "main": "./index.mjs",
5
+ "files": [
6
+ "index.mjs",
7
+ "lib/**/*.mjs",
8
+ "README.md",
9
+ "LICENSE"
10
+ ],
5
11
  "scripts": {
6
12
  "build": "node scripts/build.mjs",
7
13
  "test": "vitest run",
@@ -1,31 +0,0 @@
1
- name: Publish to npm
2
-
3
- on:
4
- workflow_dispatch:
5
- inputs:
6
- bump:
7
- description: "Version bump type"
8
- required: false
9
- default: "patch"
10
-
11
- permissions:
12
- id-token: write
13
- contents: read
14
-
15
- jobs:
16
- publish:
17
- runs-on: ubuntu-latest
18
- steps:
19
- - uses: actions/checkout@v4
20
-
21
- - uses: actions/setup-node@v4
22
- with:
23
- node-version: "24"
24
- registry-url: "https://registry.npmjs.org"
25
-
26
- - run: npm install
27
-
28
- - run: npm publish --access public
29
- env:
30
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
31
- NPM_CONFIG_PROVENANCE: false
package/.husky/pre-commit DELETED
@@ -1,2 +0,0 @@
1
- npm test
2
- npx lint-staged
package/.husky/pre-push DELETED
@@ -1,3 +0,0 @@
1
- npm test
2
- npx prettier --check .
3
- npx eslint .
@@ -1,221 +0,0 @@
1
- # CLI Enhancements: Full Account Management
2
-
3
- **Status:** Complete
4
- **Created:** 2026-02-09
5
- **Completed:** 2026-02-09
6
-
7
- ---
8
-
9
- ## Problem Statement
10
-
11
- The plugin CLI (`opencode-anthropic-auth`) can **manage** existing accounts (enable, disable, remove, switch, reset) but cannot **authenticate** new ones or re-authenticate broken ones. The only way to add an account is through `opencode auth login` → selecting "Claude Pro/Max" → browser OAuth flow inside the OpenCode TUI.
12
-
13
- This creates friction in several scenarios:
14
-
15
- 1. **Disabled account recovery** — Account #1 was disabled due to a token refresh failure. To fix it, you'd have to `remove` it via CLI, then re-login via `opencode auth login`, which is the exact workflow we want to avoid.
16
- 2. **No re-authentication** — There's no way to get fresh tokens for an existing broken account without deleting and re-adding it.
17
- 3. **No proper logout** — The `remove` command just deletes local data without revoking tokens server-side.
18
- 4. **Incomplete standalone tool** — The CLI should be a self-contained account management tool that doesn't depend on the OpenCode TUI for any operation.
19
-
20
- ## Solution Overview
21
-
22
- Add four new commands to `cli.mjs` that give full account lifecycle management:
23
-
24
- | Command | Alias | Description |
25
- | -------------- | ----- | -------------------------------------------------- |
26
- | `login` | `ln` | Add a new account via browser OAuth flow |
27
- | `logout <N>` | `lo` | Revoke tokens + remove a specific account |
28
- | `logout --all` | — | Revoke all tokens + clear all accounts |
29
- | `reauth <N>` | `ra` | Re-authenticate an account with fresh OAuth tokens |
30
- | `refresh <N>` | `rf` | Attempt token refresh without browser (quick fix) |
31
-
32
- ### Architecture Change
33
-
34
- Extract the OAuth `authorize()` and `exchange()` functions from `index.mjs` into a new shared module `lib/oauth.mjs`, plus add a new `revoke()` function. Both `index.mjs` (plugin) and `cli.mjs` (standalone) import from this shared module.
35
-
36
- ```
37
- lib/oauth.mjs (NEW)
38
- ├── authorize(mode) — Generate PKCE + build auth URL
39
- ├── exchange(code, verifier) — Exchange auth code for tokens
40
- └── revoke(refreshToken) — Revoke a refresh token (best-effort)
41
- ```
42
-
43
- ## Design Decisions
44
-
45
- | Decision | Choice | Rationale |
46
- | --------------------- | -------------------- | ----------------------------------------------------------------------- |
47
- | Reauth matching | Replace at index | Simpler UX; user explicitly chooses which slot to replace |
48
- | Logout behavior | Revoke + remove | Clean break; user can re-add via `login` |
49
- | Token refresh command | Yes, add `refresh` | Quick fix for expired access tokens without browser round-trip |
50
- | Browser opening | Auto-open + show URL | Best UX: tries `open`/`xdg-open`/`start`, always prints URL as fallback |
51
-
52
- ## Detailed Command Specifications
53
-
54
- ### `login` (alias: `ln`)
55
-
56
- **Flow:**
57
-
58
- 1. Call `authorize("max")` to get PKCE URL + verifier
59
- 2. Auto-open the URL in the user's default browser
60
- 3. Print the URL to terminal as fallback
61
- 4. Prompt user to paste the authorization code (readline)
62
- 5. Call `exchange(code, verifier)` to get tokens
63
- 6. Deduplicate: check if an account with the same refresh token already exists
64
- 7. Add the account to `anthropic-accounts.json`, save, print confirmation
65
-
66
- **Edge cases:**
67
-
68
- - If exchange fails, print error and exit with code 1
69
- - If max accounts (10) reached, print error
70
- - If duplicate refresh token, update existing account instead of adding
71
-
72
- ### `logout <N>` (alias: `lo <N>`)
73
-
74
- **Flow:**
75
-
76
- 1. Validate index N exists
77
- 2. Attempt `revoke(account.refreshToken)` — best-effort, won't block on failure
78
- 3. Remove the account from array (splice)
79
- 4. Adjust `activeIndex` if needed (same logic as existing `remove` command)
80
- 5. Save + print confirmation
81
-
82
- **Flags:**
83
-
84
- - `--force` — Skip confirmation prompt (same as `remove`)
85
- - `--all` — Logout all accounts
86
-
87
- ### `logout --all`
88
-
89
- **Flow:**
90
-
91
- 1. Iterate all accounts, attempt `revoke()` for each
92
- 2. Write explicit empty storage `{ version: 1, accounts: [], activeIndex: 0 }`
93
- 3. Print confirmation with count of revoked accounts
94
-
95
- **Important behavior:** empty storage is treated as authoritative by `AccountManager.load()`, so plugin instances stay logged out and do not re-bootstrap from fallback auth when the accounts file explicitly contains zero accounts.
96
-
97
- ### `reauth <N>` (alias: `ra <N>`)
98
-
99
- **Flow:**
100
-
101
- 1. Validate index N exists
102
- 2. Run full OAuth flow (same as `login` steps 1-5)
103
- 3. Replace the account at index N with the new tokens
104
- 4. Preserve: `id`, `addedAt`, `stats`
105
- 5. Reset: `enabled=true`, `consecutiveFailures=0`, `lastFailureTime=null`, `rateLimitResetTimes={}`
106
- 6. Save + print confirmation
107
-
108
- **Key detail:** Replaces whatever's at index N with whatever the user logs in with, regardless of email matching.
109
-
110
- ### `refresh <N>` (alias: `rf <N>`)
111
-
112
- **Flow:**
113
-
114
- 1. Validate index N exists
115
- 2. Call the existing `refreshAccessToken(account)` function
116
- 3. If success: update account, re-enable if disabled, save + print new token expiry
117
- 4. If failure: print error and suggest `reauth <N>` instead
118
-
119
- ## Files Modified
120
-
121
- | File | Change | Status |
122
- | ------------------- | -------------------------------------------------------------------- | ------ |
123
- | `lib/oauth.mjs` | **NEW** — Extracted `authorize()`, `exchange()`, plus new `revoke()` | Done |
124
- | `index.mjs` | Replace inline OAuth functions with imports from `lib/oauth.mjs` | Done |
125
- | `cli.mjs` | Add 5 new commands + browser opener utility | Done |
126
- | `scripts/build.mjs` | No change needed — esbuild follows imports automatically | N/A |
127
-
128
- ## Task List
129
-
130
- - [x] **Task 1:** Create `lib/oauth.mjs` — extract `authorize()` and `exchange()` from `index.mjs`, add `revoke()`
131
- - [x] **Task 2:** Update `index.mjs` — replace inline functions with imports from `lib/oauth.mjs`
132
- - [x] **Task 3:** Add `openBrowser()` utility to `cli.mjs`
133
- - [x] **Task 4:** Implement `cmdLogin` in `cli.mjs`
134
- - [x] **Task 5:** Implement `cmdLogout` and `cmdLogoutAll` in `cli.mjs`
135
- - [x] **Task 6:** Implement `cmdReauth` in `cli.mjs`
136
- - [x] **Task 7:** Implement `cmdRefresh` in `cli.mjs`
137
- - [x] **Task 8:** Wire new commands into `main()` dispatcher and update `cmdHelp()`
138
- - [x] **Task 9:** Build — passes (esbuild picks up `lib/oauth.mjs` automatically)
139
- - [x] **Task 10:** Tests — all 349 tests pass across 7 test files, zero failures
140
- - [x] **Task 11:** Lint — eslint passes cleanly
141
- - [x] **Task 12:** Live verification — `refresh 1` correctly fails (broken token), `refresh 2` succeeds (7h 59m expiry)
142
- - [x] **Task 13:** Update this document with completion status
143
-
144
- ## Token Revocation Notes
145
-
146
- Anthropic's OAuth implementation may or may not support RFC 7009 token revocation at `POST /v1/oauth/revoke`. The `revoke()` function is implemented as best-effort:
147
-
148
- - Attempts `POST https://console.anthropic.com/v1/oauth/revoke` with `{ token: refreshToken, token_type_hint: "refresh_token", client_id: CLIENT_ID }`
149
- - Returns `true` if 2xx response, `false` otherwise
150
- - `logout` proceeds with local cleanup regardless of revocation result
151
- - No error is surfaced to the user if revocation fails — only a dim note
152
-
153
- ## Immediate Use Case
154
-
155
- To fix the disabled account #1:
156
-
157
- ```bash
158
- # Option A: Try a token refresh first (no browser)
159
- opencode-anthropic-auth refresh 1
160
- # Tested: correctly fails with "token refresh failed" and suggests reauth
161
-
162
- # Option B: If refresh fails, do a full re-auth
163
- opencode-anthropic-auth reauth 1
164
-
165
- # Option C: Full nuke and re-login
166
- opencode-anthropic-auth logout 1
167
- opencode-anthropic-auth login
168
- ```
169
-
170
- **Verified:** `refresh 2` (healthy account) succeeds and reports "Token refreshed, expires in 7h 59m".
171
-
172
- ## Updated Help Text (Preview)
173
-
174
- ```
175
- Anthropic Multi-Account Auth CLI
176
-
177
- Usage:
178
- opencode-anthropic-auth [command] [args]
179
-
180
- Commands:
181
- list Show all accounts with status (default)
182
- status Compact one-liner for scripts/prompts
183
- login Add a new account via browser OAuth flow
184
- logout <N> Revoke tokens and remove account N
185
- logout --all Revoke all tokens and clear all accounts
186
- reauth <N> Re-authenticate account N with fresh OAuth tokens
187
- refresh <N> Attempt token refresh (no browser needed)
188
- switch <N> Set account N as active
189
- enable <N> Enable a disabled account
190
- disable <N> Disable an account (skipped in rotation)
191
- remove <N> Remove an account permanently
192
- reset <N|all> Clear rate-limit / failure tracking
193
- stats Show per-account usage statistics
194
- reset-stats [N|all] Reset usage statistics
195
- strategy [name] Show or change selection strategy
196
- config Show configuration and file paths
197
- manage Interactive account management menu
198
- help Show this help message
199
- ```
200
-
201
- ## Post-Review Remediations
202
-
203
- Following delegated code review, the following fixes were applied:
204
-
205
- 1. **Logout-all semantics hardened**
206
- - `cmdLogoutAll` now writes explicit empty account storage instead of deleting the accounts file.
207
- - `AccountManager.load` now treats existing storage (including empty storage) as authoritative, preventing fallback re-bootstrap when users explicitly logout-all.
208
-
209
- 2. **Windows browser launch fixed**
210
- - `openBrowser` now uses `cmd /c start "" <url>` on Windows for correct URL handling.
211
-
212
- 3. **Reauth state message bug fixed**
213
- - `cmdReauth` now captures prior disabled state (`wasDisabled`) before mutation and correctly prints "re-enabled" message when applicable.
214
-
215
- 4. **Login dedupe ordering corrected**
216
- - `cmdLogin` now performs duplicate-token update path before applying max-account rejection, so existing accounts can still be refreshed when at capacity.
217
-
218
- 5. **Unit test coverage expanded for new auth commands**
219
- - Added direct CLI unit tests for `cmdLogin`, `cmdLogout` (single + `--all`), `cmdReauth`, and `cmdRefresh`.
220
- - Added main routing tests for new aliases/paths: `ln`, `lo --all --force`, `ra`, and `rf`.
221
- - Added help text assertions for new commands/examples.
@@ -1,201 +0,0 @@
1
- # Architecture
2
-
3
- **Analysis Date:** 2026-02-07
4
-
5
- ## Pattern Overview
6
-
7
- **Overall:** Plugin + CLI dual-entry-point architecture with shared library layer
8
-
9
- **Key Characteristics:**
10
-
11
- - OpenCode plugin providing OAuth-based multi-account Anthropic authentication
12
- - Standalone CLI binary for account management (shares storage layer with plugin)
13
- - Fetch interceptor pattern: the plugin wraps HTTP requests to Anthropic API, injecting OAuth tokens, transforming request/response bodies, and rotating accounts on failures
14
- - Stateful account management with disk persistence and in-memory health tracking
15
- - No framework — pure Node.js ESM modules with zero runtime dependencies beyond `@openauthjs/openauth`
16
-
17
- ## Layers
18
-
19
- **Plugin Entry (`index.mjs`):**
20
-
21
- - Purpose: OpenCode plugin that intercepts Anthropic API calls with OAuth multi-account support
22
- - Location: `index.mjs`
23
- - Contains: OAuth authorize/exchange helpers, request/response transform functions, `AnthropicAuthPlugin` factory, token refresh logic, CLI-style interactive prompts for auth flow
24
- - Depends on: `lib/accounts.mjs`, `lib/config.mjs`, `lib/storage.mjs`, `lib/backoff.mjs`, `@openauthjs/openauth`
25
- - Used by: OpenCode runtime (loaded as plugin)
26
-
27
- **CLI Entry (`cli.mjs`):**
28
-
29
- - Purpose: Standalone CLI for managing accounts outside of the OpenCode TUI
30
- - Location: `cli.mjs`
31
- - Contains: All CLI commands (list, switch, enable, disable, remove, reset, stats, strategy, config, manage), formatting/color helpers, usage quota fetching
32
- - Depends on: `lib/storage.mjs`, `lib/config.mjs`
33
- - Used by: End users via `opencode-anthropic-auth` binary
34
-
35
- **Account Management (`lib/accounts.mjs`):**
36
-
37
- - Purpose: In-memory account pool with health scoring, rotation, and disk sync
38
- - Location: `lib/accounts.mjs`
39
- - Contains: `AccountManager` class — the core runtime coordinator for account selection, failure tracking, rate limit handling, and usage statistics
40
- - Depends on: `lib/storage.mjs`, `lib/rotation.mjs`, `lib/backoff.mjs`
41
- - Used by: `index.mjs`
42
-
43
- **Rotation Logic (`lib/rotation.mjs`):**
44
-
45
- - Purpose: Account selection algorithms and health/token tracking
46
- - Location: `lib/rotation.mjs`
47
- - Contains: `HealthScoreTracker` class, `TokenBucketTracker` class, `selectAccount()` function implementing sticky/round-robin/hybrid strategies
48
- - Depends on: `lib/config.mjs` (for defaults only)
49
- - Used by: `lib/accounts.mjs`
50
-
51
- **Backoff Logic (`lib/backoff.mjs`):**
52
-
53
- - Purpose: Error classification and retry backoff calculation
54
- - Location: `lib/backoff.mjs`
55
- - Contains: `isAccountSpecificError()`, `parseRateLimitReason()`, `calculateBackoffMs()`, `parseRetryAfterHeader()`
56
- - Depends on: Nothing
57
- - Used by: `index.mjs`, `lib/accounts.mjs`
58
-
59
- **Configuration (`lib/config.mjs`):**
60
-
61
- - Purpose: Config file loading, validation, and env var overrides
62
- - Location: `lib/config.mjs`
63
- - Contains: `loadConfig()`, `saveConfig()`, `DEFAULT_CONFIG`, `CLIENT_ID`, strategy validation, XDG-compliant path resolution
64
- - Depends on: Node.js builtins only
65
- - Used by: `index.mjs`, `cli.mjs`, `lib/accounts.mjs`, `lib/rotation.mjs`
66
-
67
- **Storage (`lib/storage.mjs`):**
68
-
69
- - Purpose: Atomic JSON file persistence for account data
70
- - Location: `lib/storage.mjs`
71
- - Contains: `loadAccounts()`, `saveAccounts()`, `clearAccounts()`, account validation/deduplication, `.gitignore` management
72
- - Depends on: `lib/config.mjs` (for config dir path)
73
- - Used by: `index.mjs`, `cli.mjs`, `lib/accounts.mjs`
74
-
75
- ## Data Flow
76
-
77
- **API Request (Plugin fetch interceptor):**
78
-
79
- 1. OpenCode calls `fetch(input, init)` on the plugin's custom fetch
80
- 2. `transformRequestBody()` sanitizes system prompts and prefixes tool names with `mcp_`
81
- 3. `transformRequestUrl()` adds `?beta=true` to `/v1/messages` endpoint
82
- 4. `AccountManager.getCurrentAccount()` selects best account via strategy (sticky/round-robin/hybrid)
83
- 5. If token expired, `refreshAccountTokenSingleFlight()` refreshes OAuth token (single-flight deduplication)
84
- 6. `buildRequestHeaders()` sets Authorization bearer, anthropic-beta headers, removes x-api-key
85
- 7. Actual `fetch()` to Anthropic API executes
86
- 8. On account-specific error (429, 401, 400/403 with account language): mark account, try next account (up to N accounts)
87
- 9. On service-wide error (529, 503, 500): return immediately (switching won't help)
88
- 10. On success: `transformResponse()` wraps the SSE stream to strip `mcp_` prefix from tool names, extract token usage, and detect mid-stream account errors
89
-
90
- **Account Selection (per-request):**
91
-
92
- 1. `AccountManager.syncActiveIndexFromDisk()` reconciles with CLI changes
93
- 2. Build candidate list: enabled accounts, not in per-request exclusion set
94
- 3. `selectAccount()` applies configured strategy:
95
- - **sticky**: Stay on current account unless rate-limited/failed
96
- - **round-robin**: Rotate cursor through available accounts
97
- - **hybrid**: Score accounts by health (weighted ×2) + token bucket (weighted ×5) + freshness, with stickiness bonus (150 points) and switch threshold (100 points)
98
- 4. Consume a token bucket token for the selected account
99
- 5. Return selected `ManagedAccount`
100
-
101
- **OAuth Login Flow:**
102
-
103
- 1. User triggers `opencode auth login` → plugin's `auth.methods[0].authorize()` runs
104
- 2. If existing accounts: prompt menu (add/fresh/manage/cancel)
105
- 3. Generate PKCE challenge via `@openauthjs/openauth`
106
- 4. Build authorize URL for `claude.ai/oauth/authorize`
107
- 5. User pastes authorization code
108
- 6. `exchange()` POSTs to `console.anthropic.com/v1/oauth/token` for tokens
109
- 7. `AccountManager.addAccount()` stores new credentials (deduplicates by refresh token)
110
- 8. `AccountManager.saveToDisk()` persists to `~/.config/opencode/anthropic-accounts.json`
111
-
112
- **State Management:**
113
-
114
- - **In-memory (plugin process):** `AccountManager` with `HealthScoreTracker` and `TokenBucketTracker` — these are ephemeral, rebuilt on plugin load
115
- - **On-disk (shared):** `anthropic-accounts.json` stores account credentials, failure tracking, rate limit reset times, usage stats, and active index
116
- - **Sync mechanism:** Plugin reads `activeIndex` from disk before each request to pick up CLI changes; debounced writes (1s) persist plugin-side changes back
117
- - **Stats merge-on-save:** To handle concurrent instances, `saveToDisk()` reads disk values and applies in-memory deltas rather than overwriting
118
-
119
- ## Key Abstractions
120
-
121
- **AccountManager (`lib/accounts.mjs`):**
122
-
123
- - Purpose: Central coordinator for multi-account state and request routing
124
- - Pattern: Singleton per plugin instance with private state (`#accounts`, `#healthTracker`, `#tokenTracker`)
125
- - Static factory: `AccountManager.load(config, authFallback)` bootstraps from disk + OpenCode auth
126
- - Key methods: `getCurrentAccount()`, `markRateLimited()`, `markSuccess()`, `markFailure()`, `recordUsage()`, `syncActiveIndexFromDisk()`
127
-
128
- **HealthScoreTracker (`lib/rotation.mjs`):**
129
-
130
- - Purpose: Track per-account health with passive time-based recovery
131
- - Pattern: In-memory score map with configurable initial/penalty/recovery values
132
- - Score range: 0–100, min_usable threshold (default 50) gates account selection
133
-
134
- **TokenBucketTracker (`lib/rotation.mjs`):**
135
-
136
- - Purpose: Rate-limit requests per account using token bucket algorithm
137
- - Pattern: Per-account bucket with configurable max tokens, regeneration rate
138
- - Default: 50 tokens, 6/minute regen
139
-
140
- **ManagedAccount (typedef in `lib/accounts.mjs`):**
141
-
142
- - Purpose: Runtime representation of an Anthropic OAuth account
143
- - Contains: id, index, email, refreshToken, access, expires, enabled, rateLimitResetTimes, consecutiveFailures, stats
144
- - Pattern: Mutable in-memory state with periodic disk sync
145
-
146
- ## Entry Points
147
-
148
- **Plugin (`index.mjs` → `AnthropicAuthPlugin`):**
149
-
150
- - Location: `index.mjs`
151
- - Triggers: Loaded by OpenCode plugin system as ESM module
152
- - Responsibilities: Exports an async plugin factory function returning hooks for `experimental.chat.system.transform`, `auth.loader`, and `auth.methods`
153
- - Exported as: `export async function AnthropicAuthPlugin({ client })`
154
-
155
- **CLI (`cli.mjs` → `main()`):**
156
-
157
- - Location: `cli.mjs`
158
- - Triggers: Executed as `opencode-anthropic-auth [command]` via `~/.local/bin` symlink/copy
159
- - Responsibilities: Parse argv, route to command handlers, manage accounts via direct storage manipulation
160
- - Self-detection: Uses `import.meta.url === pathToFileURL(process.argv[1]).href` with symlink resolution
161
-
162
- **Build (`scripts/build.mjs`):**
163
-
164
- - Location: `scripts/build.mjs`
165
- - Triggers: `npm run build`
166
- - Responsibilities: Bundle `index.mjs` → `dist/opencode-anthropic-auth-plugin.js` and `cli.mjs` → `dist/opencode-anthropic-auth-cli.mjs` using esbuild (ESM, node20 target, all deps bundled except node: builtins)
167
-
168
- **Install (`scripts/install.mjs`):**
169
-
170
- - Location: `scripts/install.mjs`
171
- - Triggers: `npm run install:link` (dev) or `npm run install:copy` (production)
172
- - Responsibilities: Symlink or copy plugin to `~/.config/opencode/plugin/` and CLI to `~/.local/bin/`
173
-
174
- ## Error Handling
175
-
176
- **Strategy:** Account-specific vs. service-wide error classification with automatic account rotation
177
-
178
- **Patterns:**
179
-
180
- - **Account-specific errors** (429, 401, 400/403 with account error body): Mark account with backoff, rotate to next account. Auth failures (401) invalidate cached token.
181
- - **Service-wide errors** (529, 503, 500): Return immediately to caller — switching accounts won't help.
182
- - **Token refresh failures**: Transient errors skip account for current request; terminal errors (400, 401, 403, `invalid_grant`) disable the account permanently.
183
- - **Mid-stream SSE errors**: Detected in response stream transformer. Account marked for next request (not retried mid-stream).
184
- - **Single-flight token refresh**: Concurrent requests to the same account share one in-flight refresh promise to prevent race conditions.
185
- - **Backoff calculation** (`lib/backoff.mjs`): Retry-After header takes precedence. Otherwise, reason-based: AUTH_FAILED=5s, RATE_LIMIT_EXCEEDED=30s, QUOTA_EXHAUSTED=escalating [1m, 5m, 30m, 2h].
186
-
187
- ## Cross-Cutting Concerns
188
-
189
- **Logging:** Debug logging via `console.error("[opencode-anthropic-auth]", ...)` gated by `config.debug` flag (env: `OPENCODE_ANTHROPIC_DEBUG=1`). CLI uses direct `console.log` with ANSI color helpers.
190
-
191
- **Validation:** Config values are clamped to valid ranges in `lib/config.mjs` (`clampNumber()`). Account data is validated on load with type checks and fallbacks in `lib/storage.mjs` (`validateAccount()`). Duplicate refresh tokens are deduplicated.
192
-
193
- **Persistence:** All disk writes use atomic temp-file-and-rename pattern. Account storage uses `randomBytes(6).hex` temp names; config uses PID-based temp names. Files are written with `mode: 0o600` for security.
194
-
195
- **Configuration Precedence:** Config file (`~/.config/opencode/anthropic-auth.json`) → environment variable overrides (`OPENCODE_ANTHROPIC_STRATEGY`, `OPENCODE_ANTHROPIC_DEBUG`, `OPENCODE_ANTHROPIC_QUIET`).
196
-
197
- **Toast Notifications:** Plugin sends TUI toasts via `client.tui?.showToast()` for account usage, switching, and errors. Toasts are debounced per category and suppressible via quiet mode.
198
-
199
- ---
200
-
201
- _Architecture analysis: 2026-02-07_