@vacbo/opencode-anthropic-fix 0.1.7 → 0.1.9
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 +88 -88
- package/dist/opencode-anthropic-auth-cli.mjs +804 -507
- package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
- package/package.json +67 -59
- package/src/__tests__/billing-edge-cases.test.ts +59 -59
- package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
- package/src/__tests__/cc-comparison.test.ts +87 -87
- package/src/__tests__/cc-credentials.test.ts +254 -250
- package/src/__tests__/cch-drift-checker.test.ts +51 -51
- package/src/__tests__/cch-native-style.test.ts +56 -56
- package/src/__tests__/debug-gating.test.ts +42 -42
- package/src/__tests__/decomposition-smoke.test.ts +68 -68
- package/src/__tests__/fingerprint-regression.test.ts +575 -566
- package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
- package/src/__tests__/helpers/conversation-history.ts +119 -119
- package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
- package/src/__tests__/helpers/deferred.ts +69 -69
- package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
- package/src/__tests__/helpers/in-memory-storage.ts +88 -88
- package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
- package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
- package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
- package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
- package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
- package/src/__tests__/helpers/sse.ts +209 -209
- package/src/__tests__/index.parallel.test.ts +605 -595
- package/src/__tests__/sanitization-regex.test.ts +112 -112
- package/src/__tests__/state-bounds.test.ts +90 -90
- package/src/account-identity.test.ts +197 -192
- package/src/account-identity.ts +69 -67
- package/src/account-state.test.ts +86 -86
- package/src/account-state.ts +25 -25
- package/src/accounts/matching.test.ts +335 -0
- package/src/accounts/matching.ts +167 -0
- package/src/accounts/persistence.test.ts +345 -0
- package/src/accounts/persistence.ts +432 -0
- package/src/accounts/repair.test.ts +276 -0
- package/src/accounts/repair.ts +407 -0
- package/src/accounts.dedup.test.ts +621 -621
- package/src/accounts.test.ts +933 -929
- package/src/accounts.ts +633 -989
- package/src/backoff.test.ts +345 -345
- package/src/backoff.ts +219 -219
- package/src/betas.ts +124 -124
- package/src/bun-fetch.test.ts +345 -342
- package/src/bun-fetch.ts +424 -424
- package/src/bun-proxy.test.ts +25 -25
- package/src/bun-proxy.ts +209 -209
- package/src/cc-credentials.ts +111 -111
- package/src/circuit-breaker.test.ts +184 -184
- package/src/circuit-breaker.ts +169 -169
- package/src/cli/commands/auth.ts +963 -0
- package/src/cli/commands/config.ts +547 -0
- package/src/cli/formatting.test.ts +406 -0
- package/src/cli/formatting.ts +219 -0
- package/src/cli.ts +255 -2022
- package/src/commands/handlers/betas.ts +100 -0
- package/src/commands/handlers/config.ts +99 -0
- package/src/commands/handlers/files.ts +375 -0
- package/src/commands/oauth-flow.ts +181 -166
- package/src/commands/prompts.ts +61 -61
- package/src/commands/router.test.ts +421 -0
- package/src/commands/router.ts +143 -635
- package/src/config.test.ts +482 -482
- package/src/config.ts +412 -404
- package/src/constants.ts +48 -48
- package/src/drift/cch-constants.ts +95 -95
- package/src/env.ts +111 -105
- package/src/headers/billing.ts +33 -33
- package/src/headers/builder.ts +130 -130
- package/src/headers/cch.ts +75 -75
- package/src/headers/stainless.ts +25 -25
- package/src/headers/user-agent.ts +23 -23
- package/src/index.ts +436 -828
- package/src/models.ts +27 -27
- package/src/oauth.test.ts +102 -102
- package/src/oauth.ts +178 -178
- package/src/parent-pid-watcher.test.ts +148 -148
- package/src/parent-pid-watcher.ts +69 -69
- package/src/plugin-helpers.ts +82 -82
- package/src/refresh-helpers.ts +145 -139
- package/src/refresh-lock.test.ts +94 -94
- package/src/refresh-lock.ts +93 -93
- package/src/request/body.history.test.ts +579 -571
- package/src/request/body.ts +255 -255
- package/src/request/metadata.ts +65 -65
- package/src/request/retry.test.ts +156 -156
- package/src/request/retry.ts +67 -67
- package/src/request/url.ts +21 -21
- package/src/request-orchestration-helpers.ts +648 -0
- package/src/response/index.ts +5 -5
- package/src/response/mcp.ts +58 -58
- package/src/response/streaming.test.ts +313 -311
- package/src/response/streaming.ts +412 -410
- package/src/rotation.test.ts +304 -301
- package/src/rotation.ts +205 -205
- package/src/storage.test.ts +547 -547
- package/src/storage.ts +315 -291
- package/src/system-prompt/builder.ts +38 -38
- package/src/system-prompt/index.ts +5 -5
- package/src/system-prompt/normalize.ts +60 -60
- package/src/system-prompt/sanitize.ts +30 -30
- package/src/thinking.ts +21 -20
- package/src/token-refresh.test.ts +265 -265
- package/src/token-refresh.ts +219 -214
- package/src/types.ts +30 -30
- package/dist/bun-proxy.mjs +0 -291
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Load-time repair pass for corrupted CC account records.
|
|
3
|
+
//
|
|
4
|
+
// Historical context
|
|
5
|
+
// ------------------
|
|
6
|
+
// An older version of the plugin (or a partial write outside the current
|
|
7
|
+
// mutation sites) could leave a CC-imported row with `source="oauth"`,
|
|
8
|
+
// `identity={kind:"legacy",…}`, and missing `label`/`email`. The row's `id`
|
|
9
|
+
// still starts with `cc-cc-keychain-…` / `cc-cc-file-…` because the id
|
|
10
|
+
// prefix is set once at CC-import time (see accounts.ts createManagedAccount
|
|
11
|
+
// call for the import branch) and never rewritten by any current code path.
|
|
12
|
+
//
|
|
13
|
+
// When the loader sees that corrupted row it computes its identity as
|
|
14
|
+
// `{kind:"legacy", refreshToken}`. After CC rotates the refresh token
|
|
15
|
+
// through `claude -p . --model haiku`, the row can no longer dedupe
|
|
16
|
+
// against the current CC credential — the identity kinds differ (`legacy`
|
|
17
|
+
// vs `cc`) and the refresh tokens differ — so a second CC row gets
|
|
18
|
+
// created, producing two rows for the same upstream Anthropic account.
|
|
19
|
+
//
|
|
20
|
+
// What this pass does
|
|
21
|
+
// -------------------
|
|
22
|
+
// 1. Detect rows whose `id` proves they were born as CC imports.
|
|
23
|
+
// 2. If the row looks corrupted (wrong source, legacy identity, or
|
|
24
|
+
// missing label/identity), try to rehydrate from the live CC
|
|
25
|
+
// credentials. If that fails, at least restore `source` from the
|
|
26
|
+
// id prefix and clear the bogus legacy identity so the existing
|
|
27
|
+
// unlabeled fallback in AccountManager.load() can finish the heal.
|
|
28
|
+
// 3. Collapse any duplicates that now share a `{kind:"cc",…}` identity.
|
|
29
|
+
// When merging, keep the freshest auth by `tokenUpdatedAt`, keep
|
|
30
|
+
// `max(lastUsed)`, OR-enable, and preserve the richer identity.
|
|
31
|
+
// Stats are NOT summed — both rows represent the same upstream
|
|
32
|
+
// account and summation would double-count local counters.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
import type { CCCredential } from "../cc-credentials.js";
|
|
36
|
+
import { readCCCredentials } from "../cc-credentials.js";
|
|
37
|
+
import { identitiesMatch, resolveIdentity, type AccountIdentity } from "../account-identity.js";
|
|
38
|
+
import type { ManagedAccount } from "../accounts.js";
|
|
39
|
+
import type { AccountMetadata, AccountStorage } from "../storage.js";
|
|
40
|
+
import { loadAccounts, saveAccounts } from "../storage.js";
|
|
41
|
+
|
|
42
|
+
type CCAccountSource = "cc-keychain" | "cc-file";
|
|
43
|
+
|
|
44
|
+
const CC_ID_PATTERN = /^cc-(cc-keychain|cc-file)-\d+:/;
|
|
45
|
+
|
|
46
|
+
export interface RepairResult {
|
|
47
|
+
repaired: number;
|
|
48
|
+
collapsed: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Infer the intended CC source from an account id generated by the CC
|
|
53
|
+
* auto-import path. Returns null for ids that were not born as CC imports.
|
|
54
|
+
*/
|
|
55
|
+
export function inferCCSourceFromId(id: string): CCAccountSource | null {
|
|
56
|
+
const match = CC_ID_PATTERN.exec(id);
|
|
57
|
+
if (!match) return null;
|
|
58
|
+
const source = match[1];
|
|
59
|
+
return source === "cc-keychain" || source === "cc-file" ? source : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isCCIdentity(identity: AccountIdentity | undefined): identity is Extract<AccountIdentity, { kind: "cc" }> {
|
|
63
|
+
return identity?.kind === "cc";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function looksCorrupted(account: ManagedAccount, inferredSource: CCAccountSource): boolean {
|
|
67
|
+
if (account.source !== inferredSource) return true;
|
|
68
|
+
if (!isCCIdentity(account.identity)) return true;
|
|
69
|
+
if (!account.label) return true;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function findMatchingCredential(
|
|
74
|
+
account: ManagedAccount,
|
|
75
|
+
inferredSource: CCAccountSource,
|
|
76
|
+
ccCredentials: CCCredential[],
|
|
77
|
+
): CCCredential | null {
|
|
78
|
+
const sameSource = ccCredentials.filter((credential) => credential.source === inferredSource);
|
|
79
|
+
if (sameSource.length === 0) return null;
|
|
80
|
+
|
|
81
|
+
const byRefresh = sameSource.find((credential) => credential.refreshToken === account.refreshToken);
|
|
82
|
+
if (byRefresh) return byRefresh;
|
|
83
|
+
|
|
84
|
+
if (account.access) {
|
|
85
|
+
const byAccess = sameSource.find((credential) => credential.accessToken === account.access);
|
|
86
|
+
if (byAccess) return byAccess;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Last-resort: when there's exactly one live credential of the inferred
|
|
90
|
+
// source, the row must be that credential — CC's keychain holds at most
|
|
91
|
+
// one credential per service name, so a single-source match is unique.
|
|
92
|
+
// This mirrors the unlabeled-fallback in AccountManager.load() at the
|
|
93
|
+
// CC auto-import loop and is what enables collapse against a healthy
|
|
94
|
+
// sibling row that was already promoted by auto-import.
|
|
95
|
+
if (sameSource.length === 1) return sameSource[0]!;
|
|
96
|
+
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function repairAccount(
|
|
101
|
+
account: ManagedAccount,
|
|
102
|
+
inferredSource: CCAccountSource,
|
|
103
|
+
ccCredentials: CCCredential[],
|
|
104
|
+
): boolean {
|
|
105
|
+
const matched = findMatchingCredential(account, inferredSource, ccCredentials);
|
|
106
|
+
|
|
107
|
+
if (matched) {
|
|
108
|
+
account.source = matched.source;
|
|
109
|
+
account.label = matched.label;
|
|
110
|
+
account.identity = {
|
|
111
|
+
kind: "cc",
|
|
112
|
+
source: matched.source,
|
|
113
|
+
label: matched.label,
|
|
114
|
+
};
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Partial heal: restore the source from the id prefix and drop the bogus
|
|
119
|
+
// legacy identity. The unlabeled-CC fallback in AccountManager.load() at
|
|
120
|
+
// the CC auto-detect step can then reclaim the row on the same load pass.
|
|
121
|
+
let mutated = false;
|
|
122
|
+
if (account.source !== inferredSource) {
|
|
123
|
+
account.source = inferredSource;
|
|
124
|
+
mutated = true;
|
|
125
|
+
}
|
|
126
|
+
if (account.identity?.kind === "legacy") {
|
|
127
|
+
account.identity = undefined;
|
|
128
|
+
mutated = true;
|
|
129
|
+
}
|
|
130
|
+
return mutated;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function mergeDuplicate(keep: ManagedAccount, loser: ManagedAccount): void {
|
|
134
|
+
const keepTokenTs = keep.tokenUpdatedAt ?? 0;
|
|
135
|
+
const loserTokenTs = loser.tokenUpdatedAt ?? 0;
|
|
136
|
+
const loserHasFresherAuth = loserTokenTs > keepTokenTs;
|
|
137
|
+
|
|
138
|
+
if (loserHasFresherAuth) {
|
|
139
|
+
keep.refreshToken = loser.refreshToken;
|
|
140
|
+
keep.access = loser.access;
|
|
141
|
+
keep.expires = loser.expires;
|
|
142
|
+
keep.tokenUpdatedAt = loserTokenTs;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
keep.lastUsed = Math.max(keep.lastUsed, loser.lastUsed);
|
|
146
|
+
keep.enabled = keep.enabled || loser.enabled;
|
|
147
|
+
keep.addedAt = Math.min(keep.addedAt, loser.addedAt);
|
|
148
|
+
|
|
149
|
+
// Prefer a non-legacy identity. If keep is legacy and loser is richer,
|
|
150
|
+
// adopt loser's identity metadata. This preserves the best representation
|
|
151
|
+
// of the underlying account.
|
|
152
|
+
const keepIsLegacy = keep.identity?.kind === "legacy" || keep.identity === undefined;
|
|
153
|
+
const loserIsRicher = loser.identity !== undefined && loser.identity.kind !== "legacy";
|
|
154
|
+
if (keepIsLegacy && loserIsRicher) {
|
|
155
|
+
keep.identity = loser.identity;
|
|
156
|
+
if (loser.source) keep.source = loser.source;
|
|
157
|
+
if (loser.label !== undefined) keep.label = loser.label;
|
|
158
|
+
if (loser.email !== undefined) keep.email = loser.email;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function collapseDuplicates(accounts: ManagedAccount[]): { kept: ManagedAccount[]; collapsed: number } {
|
|
163
|
+
const kept: ManagedAccount[] = [];
|
|
164
|
+
let collapsed = 0;
|
|
165
|
+
|
|
166
|
+
for (const account of accounts) {
|
|
167
|
+
const identity = resolveIdentity(account);
|
|
168
|
+
// Never collapse rows whose identity is `legacy` — those are not
|
|
169
|
+
// confidently the same account, and collapsing them could destroy
|
|
170
|
+
// a legitimately distinct user's record.
|
|
171
|
+
if (identity.kind === "legacy") {
|
|
172
|
+
kept.push(account);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const duplicate = kept.find((existing) => identitiesMatch(resolveIdentity(existing), identity));
|
|
177
|
+
if (duplicate) {
|
|
178
|
+
mergeDuplicate(duplicate, account);
|
|
179
|
+
collapsed += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
kept.push(account);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { kept, collapsed };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Apply the full repair pass to a list of managed accounts. Mutates rows in
|
|
191
|
+
* place for healing; returns a possibly-shorter array when duplicates are
|
|
192
|
+
* collapsed. Call from AccountManager.load() after records are loaded into
|
|
193
|
+
* memory but before the CC auto-import loop.
|
|
194
|
+
*/
|
|
195
|
+
export function repairCorruptedCCAccounts(
|
|
196
|
+
accounts: ManagedAccount[],
|
|
197
|
+
ccCredentials: CCCredential[],
|
|
198
|
+
): { accounts: ManagedAccount[]; result: RepairResult } {
|
|
199
|
+
let repaired = 0;
|
|
200
|
+
|
|
201
|
+
for (const account of accounts) {
|
|
202
|
+
const inferredSource = inferCCSourceFromId(account.id);
|
|
203
|
+
if (!inferredSource) continue;
|
|
204
|
+
if (!looksCorrupted(account, inferredSource)) continue;
|
|
205
|
+
if (repairAccount(account, inferredSource, ccCredentials)) {
|
|
206
|
+
repaired += 1;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { kept, collapsed } = collapseDuplicates(accounts);
|
|
211
|
+
return {
|
|
212
|
+
accounts: kept,
|
|
213
|
+
result: { repaired, collapsed },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Storage-level variant — operates on AccountMetadata so the standalone CLI
|
|
219
|
+
// commands (which use raw loadAccounts) get the same heal that AccountManager
|
|
220
|
+
// already gets in-memory. Field shapes are nearly identical to ManagedAccount
|
|
221
|
+
// except for `token_updated_at` (snake_case) instead of `tokenUpdatedAt`.
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
function looksCorruptedStored(account: AccountMetadata, inferredSource: CCAccountSource): boolean {
|
|
225
|
+
if (account.source !== inferredSource) return true;
|
|
226
|
+
if (!isCCIdentity(account.identity)) return true;
|
|
227
|
+
if (!account.label) return true;
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function findMatchingCredentialStored(
|
|
232
|
+
account: AccountMetadata,
|
|
233
|
+
inferredSource: CCAccountSource,
|
|
234
|
+
ccCredentials: CCCredential[],
|
|
235
|
+
): CCCredential | null {
|
|
236
|
+
const sameSource = ccCredentials.filter((credential) => credential.source === inferredSource);
|
|
237
|
+
if (sameSource.length === 0) return null;
|
|
238
|
+
|
|
239
|
+
const byRefresh = sameSource.find((credential) => credential.refreshToken === account.refreshToken);
|
|
240
|
+
if (byRefresh) return byRefresh;
|
|
241
|
+
|
|
242
|
+
if (account.access) {
|
|
243
|
+
const byAccess = sameSource.find((credential) => credential.accessToken === account.access);
|
|
244
|
+
if (byAccess) return byAccess;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (sameSource.length === 1) return sameSource[0]!;
|
|
248
|
+
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function repairStoredAccount(
|
|
253
|
+
account: AccountMetadata,
|
|
254
|
+
inferredSource: CCAccountSource,
|
|
255
|
+
ccCredentials: CCCredential[],
|
|
256
|
+
): boolean {
|
|
257
|
+
const matched = findMatchingCredentialStored(account, inferredSource, ccCredentials);
|
|
258
|
+
|
|
259
|
+
if (matched) {
|
|
260
|
+
account.source = matched.source;
|
|
261
|
+
account.label = matched.label;
|
|
262
|
+
account.identity = {
|
|
263
|
+
kind: "cc",
|
|
264
|
+
source: matched.source,
|
|
265
|
+
label: matched.label,
|
|
266
|
+
};
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let mutated = false;
|
|
271
|
+
if (account.source !== inferredSource) {
|
|
272
|
+
account.source = inferredSource;
|
|
273
|
+
mutated = true;
|
|
274
|
+
}
|
|
275
|
+
if (account.identity?.kind === "legacy") {
|
|
276
|
+
account.identity = undefined;
|
|
277
|
+
mutated = true;
|
|
278
|
+
}
|
|
279
|
+
return mutated;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function mergeStoredDuplicate(keep: AccountMetadata, loser: AccountMetadata): void {
|
|
283
|
+
const keepTokenTs = keep.token_updated_at ?? 0;
|
|
284
|
+
const loserTokenTs = loser.token_updated_at ?? 0;
|
|
285
|
+
|
|
286
|
+
if (loserTokenTs > keepTokenTs) {
|
|
287
|
+
keep.refreshToken = loser.refreshToken;
|
|
288
|
+
keep.access = loser.access;
|
|
289
|
+
keep.expires = loser.expires;
|
|
290
|
+
keep.token_updated_at = loserTokenTs;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
keep.lastUsed = Math.max(keep.lastUsed, loser.lastUsed);
|
|
294
|
+
keep.enabled = keep.enabled || loser.enabled;
|
|
295
|
+
keep.addedAt = Math.min(keep.addedAt, loser.addedAt);
|
|
296
|
+
|
|
297
|
+
const keepIsLegacy = keep.identity?.kind === "legacy" || keep.identity === undefined;
|
|
298
|
+
const loserIsRicher = loser.identity !== undefined && loser.identity.kind !== "legacy";
|
|
299
|
+
if (keepIsLegacy && loserIsRicher) {
|
|
300
|
+
keep.identity = loser.identity;
|
|
301
|
+
if (loser.source) keep.source = loser.source;
|
|
302
|
+
if (loser.label !== undefined) keep.label = loser.label;
|
|
303
|
+
if (loser.email !== undefined) keep.email = loser.email;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function collapseStoredDuplicates(accounts: AccountMetadata[]): {
|
|
308
|
+
kept: AccountMetadata[];
|
|
309
|
+
collapsed: number;
|
|
310
|
+
droppedIds: string[];
|
|
311
|
+
} {
|
|
312
|
+
const kept: AccountMetadata[] = [];
|
|
313
|
+
const droppedIds: string[] = [];
|
|
314
|
+
let collapsed = 0;
|
|
315
|
+
|
|
316
|
+
for (const account of accounts) {
|
|
317
|
+
const identity = resolveIdentity(account);
|
|
318
|
+
if (identity.kind === "legacy") {
|
|
319
|
+
kept.push(account);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const duplicate = kept.find((existing) => identitiesMatch(resolveIdentity(existing), identity));
|
|
324
|
+
if (duplicate) {
|
|
325
|
+
mergeStoredDuplicate(duplicate, account);
|
|
326
|
+
droppedIds.push(account.id);
|
|
327
|
+
collapsed += 1;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
kept.push(account);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { kept, collapsed, droppedIds };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Storage-level variant of `repairCorruptedCCAccounts` that operates on raw
|
|
339
|
+
* `AccountMetadata` rows. Used by `loadAccountsWithRepair` so the standalone
|
|
340
|
+
* CLI commands and any other consumer of `loadAccounts()` get the same heal
|
|
341
|
+
* that `AccountManager.load()` already provides in-memory. The returned
|
|
342
|
+
* `droppedIds` must be forwarded to `saveAccounts` so the disk-only union
|
|
343
|
+
* does not restore the collapsed rows.
|
|
344
|
+
*/
|
|
345
|
+
export function repairStoredCCAccounts(
|
|
346
|
+
accounts: AccountMetadata[],
|
|
347
|
+
ccCredentials: CCCredential[],
|
|
348
|
+
): { accounts: AccountMetadata[]; result: RepairResult; droppedIds: string[] } {
|
|
349
|
+
let repaired = 0;
|
|
350
|
+
|
|
351
|
+
for (const account of accounts) {
|
|
352
|
+
const inferredSource = inferCCSourceFromId(account.id);
|
|
353
|
+
if (!inferredSource) continue;
|
|
354
|
+
if (!looksCorruptedStored(account, inferredSource)) continue;
|
|
355
|
+
if (repairStoredAccount(account, inferredSource, ccCredentials)) {
|
|
356
|
+
repaired += 1;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { kept, collapsed, droppedIds } = collapseStoredDuplicates(accounts);
|
|
361
|
+
return {
|
|
362
|
+
accounts: kept,
|
|
363
|
+
result: { repaired, collapsed },
|
|
364
|
+
droppedIds,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Drop-in replacement for `loadAccounts()` that runs the load-time repair
|
|
370
|
+
* pass and persists the healed state when something changed. Returns the
|
|
371
|
+
* healed storage object, or null if the file does not exist.
|
|
372
|
+
*/
|
|
373
|
+
export async function loadAccountsWithRepair(): Promise<AccountStorage | null> {
|
|
374
|
+
const stored = await loadAccounts();
|
|
375
|
+
if (!stored) return stored;
|
|
376
|
+
|
|
377
|
+
let ccCredentials: CCCredential[];
|
|
378
|
+
try {
|
|
379
|
+
ccCredentials = readCCCredentials();
|
|
380
|
+
} catch {
|
|
381
|
+
ccCredentials = [];
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const { accounts, result, droppedIds } = repairStoredCCAccounts(stored.accounts, ccCredentials);
|
|
385
|
+
|
|
386
|
+
if (result.repaired === 0 && result.collapsed === 0) {
|
|
387
|
+
return stored;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const healed: AccountStorage = {
|
|
391
|
+
...stored,
|
|
392
|
+
accounts,
|
|
393
|
+
activeIndex: accounts.length === 0 ? 0 : Math.max(0, Math.min(stored.activeIndex, accounts.length - 1)),
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
try {
|
|
397
|
+
await saveAccounts(healed, { droppedIds: new Set(droppedIds) });
|
|
398
|
+
} catch (err) {
|
|
399
|
+
// Best-effort persistence: the in-memory healed state is still correct
|
|
400
|
+
// for this call's caller even if writing back failed. Surface the error
|
|
401
|
+
// on stderr so misconfigurations don't fail silently.
|
|
402
|
+
// eslint-disable-next-line no-console -- diagnostic for an unexpected save failure
|
|
403
|
+
console.error("[opencode-anthropic-auth] loadAccountsWithRepair: save failed:", (err as Error).message);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return healed;
|
|
407
|
+
}
|