@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
|
@@ -6,693 +6,693 @@ import type * as StorageModule from "./storage.js";
|
|
|
6
6
|
import type * as ConfigModule from "./config.js";
|
|
7
7
|
|
|
8
8
|
type CCCredential = {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
9
|
+
accessToken: string;
|
|
10
|
+
refreshToken: string;
|
|
11
|
+
expiresAt: number;
|
|
12
|
+
source: "cc-keychain" | "cc-file";
|
|
13
|
+
label: string;
|
|
14
|
+
subscriptionType?: string;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
type LoadManagerOptions = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
authFallback?: {
|
|
19
|
+
refresh: string;
|
|
20
|
+
access?: string;
|
|
21
|
+
expires?: number;
|
|
22
|
+
} | null;
|
|
23
|
+
ccCredentials?: CCCredential[];
|
|
24
|
+
config?: typeof DEFAULT_CONFIG;
|
|
25
|
+
initialStorage?: AccountStorage;
|
|
26
26
|
};
|
|
27
27
|
|
|
28
28
|
type ExchangeSuccess = {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
type: "success";
|
|
30
|
+
refresh: string;
|
|
31
|
+
access: string;
|
|
32
|
+
expires: number;
|
|
33
|
+
email?: string;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
type LoadPluginOptions = {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
37
|
+
ccCredentials?: CCCredential[];
|
|
38
|
+
config?: typeof DEFAULT_CONFIG;
|
|
39
|
+
exchangeResult?: ExchangeSuccess;
|
|
40
|
+
initialStorage?: AccountStorage;
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
function makeStats(lastReset = Date.now()) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
44
|
+
return {
|
|
45
|
+
requests: 0,
|
|
46
|
+
inputTokens: 0,
|
|
47
|
+
outputTokens: 0,
|
|
48
|
+
cacheReadTokens: 0,
|
|
49
|
+
cacheWriteTokens: 0,
|
|
50
|
+
lastReset,
|
|
51
|
+
};
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
function makeCCCredential(overrides: Partial<CCCredential> = {}): CCCredential {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
return {
|
|
56
|
+
accessToken: "cc-access-fresh",
|
|
57
|
+
refreshToken: "cc-refresh-fresh",
|
|
58
|
+
expiresAt: Date.now() + 3_600_000,
|
|
59
|
+
source: "cc-keychain",
|
|
60
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
61
|
+
subscriptionType: "max",
|
|
62
|
+
...overrides,
|
|
63
|
+
};
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
async function loadManager(options: LoadManagerOptions = {}) {
|
|
67
|
-
|
|
67
|
+
vi.resetModules();
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
const storage = createInMemoryStorage(options.initialStorage);
|
|
70
|
+
const createDefaultStats = vi.fn((now?: number) => makeStats(now ?? Date.now()));
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
vi.doMock("./storage.js", async (importOriginal) => {
|
|
73
|
+
const actual = await importOriginal<typeof StorageModule>();
|
|
74
74
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
75
|
+
return {
|
|
76
|
+
...actual,
|
|
77
|
+
createDefaultStats,
|
|
78
|
+
loadAccounts: storage.loadAccountsMock,
|
|
79
|
+
saveAccounts: storage.saveAccountsMock,
|
|
80
|
+
};
|
|
81
|
+
});
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
vi.doMock("./cc-credentials.js", () => ({
|
|
84
|
+
readCCCredentials: () => options.ccCredentials ?? [],
|
|
85
|
+
}));
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
const { AccountManager } = await import("./accounts.js");
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
const manager = await AccountManager.load(options.config ?? DEFAULT_CONFIG, options.authFallback ?? null);
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
return {
|
|
92
|
+
manager,
|
|
93
|
+
storage,
|
|
94
|
+
};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
function makeClient() {
|
|
98
|
-
return {
|
|
99
|
-
auth: {
|
|
100
|
-
set: vi.fn().mockResolvedValue(undefined),
|
|
101
|
-
},
|
|
102
|
-
session: {
|
|
103
|
-
prompt: vi.fn().mockResolvedValue(undefined),
|
|
104
|
-
},
|
|
105
|
-
tui: {
|
|
106
|
-
showToast: vi.fn().mockResolvedValue(undefined),
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async function loadPlugin(options: LoadPluginOptions = {}) {
|
|
112
|
-
vi.resetModules();
|
|
113
|
-
|
|
114
|
-
const storage = createInMemoryStorage(options.initialStorage);
|
|
115
|
-
const createDefaultStats = vi.fn((now?: number) => makeStats(now ?? Date.now()));
|
|
116
|
-
const authorizeMock = vi.fn().mockResolvedValue({
|
|
117
|
-
url: "https://claude.ai/oauth/authorize?state=test-state",
|
|
118
|
-
verifier: "test-verifier",
|
|
119
|
-
state: "test-state",
|
|
120
|
-
});
|
|
121
|
-
const exchangeMock = vi.fn().mockResolvedValue(
|
|
122
|
-
options.exchangeResult ?? {
|
|
123
|
-
type: "success",
|
|
124
|
-
refresh: "oauth-refresh-fresh",
|
|
125
|
-
access: "oauth-access-fresh",
|
|
126
|
-
expires: Date.now() + 3_600_000,
|
|
127
|
-
email: "alice@example.com",
|
|
128
|
-
},
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
vi.doMock("./storage.js", () => ({
|
|
132
|
-
createDefaultStats,
|
|
133
|
-
loadAccounts: storage.loadAccountsMock,
|
|
134
|
-
saveAccounts: storage.saveAccountsMock,
|
|
135
|
-
clearAccounts: vi.fn().mockResolvedValue(undefined),
|
|
136
|
-
}));
|
|
137
|
-
|
|
138
|
-
vi.doMock("./cc-credentials.js", () => ({
|
|
139
|
-
readCCCredentials: () => options.ccCredentials ?? [],
|
|
140
|
-
}));
|
|
141
|
-
|
|
142
|
-
vi.doMock("./config.js", async (importOriginal) => {
|
|
143
|
-
const actual = await importOriginal<typeof ConfigModule>();
|
|
144
|
-
|
|
145
98
|
return {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
loadConfig: vi.fn(() => ({
|
|
149
|
-
...DEFAULT_CONFIG,
|
|
150
|
-
signature_emulation: {
|
|
151
|
-
...DEFAULT_CONFIG.signature_emulation,
|
|
152
|
-
fetch_claude_code_version_on_startup: false,
|
|
99
|
+
auth: {
|
|
100
|
+
set: vi.fn().mockResolvedValue(undefined),
|
|
153
101
|
},
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
enabled: false,
|
|
102
|
+
session: {
|
|
103
|
+
prompt: vi.fn().mockResolvedValue(undefined),
|
|
157
104
|
},
|
|
158
|
-
|
|
159
|
-
|
|
105
|
+
tui: {
|
|
106
|
+
showToast: vi.fn().mockResolvedValue(undefined),
|
|
160
107
|
},
|
|
161
|
-
...(options.config ?? {}),
|
|
162
|
-
})),
|
|
163
108
|
};
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
vi.doMock("./oauth.js", () => ({
|
|
167
|
-
authorize: authorizeMock,
|
|
168
|
-
exchange: exchangeMock,
|
|
169
|
-
}));
|
|
170
|
-
|
|
171
|
-
vi.doMock("./commands/prompts.js", () => ({
|
|
172
|
-
promptAccountMenu: vi.fn().mockResolvedValue("add"),
|
|
173
|
-
promptManageAccounts: vi.fn().mockResolvedValue(undefined),
|
|
174
|
-
}));
|
|
175
|
-
|
|
176
|
-
vi.doMock("./bun-fetch.js", () => ({
|
|
177
|
-
createBunFetch: () => ({
|
|
178
|
-
fetch: vi.fn(),
|
|
179
|
-
}),
|
|
180
|
-
}));
|
|
181
|
-
|
|
182
|
-
const { AnthropicAuthPlugin } = await import("./index.js");
|
|
183
|
-
|
|
184
|
-
return {
|
|
185
|
-
plugin: await AnthropicAuthPlugin({ client: makeClient() }),
|
|
186
|
-
storage,
|
|
187
|
-
};
|
|
188
109
|
}
|
|
189
110
|
|
|
190
|
-
function
|
|
191
|
-
const calls = (storage.saveAccountsMock as unknown as Mock).mock.calls as AccountStorage[][];
|
|
192
|
-
const saved = calls[calls.length - 1]?.[0] as AccountStorage | undefined;
|
|
193
|
-
expect(saved).toBeDefined();
|
|
194
|
-
return saved!;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
describe("AccountManager identity-based dedup RED", () => {
|
|
198
|
-
beforeEach(() => {
|
|
199
|
-
vi.useFakeTimers();
|
|
200
|
-
vi.setSystemTime(new Date("2026-04-10T12:00:00Z"));
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
afterEach(() => {
|
|
204
|
-
vi.useRealTimers();
|
|
205
|
-
vi.clearAllMocks();
|
|
111
|
+
async function loadPlugin(options: LoadPluginOptions = {}) {
|
|
206
112
|
vi.resetModules();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("updates an OAuth account by email instead of creating a duplicate on refresh rotation", async () => {
|
|
210
|
-
const initialStorage = makeAccountsData([
|
|
211
|
-
{
|
|
212
|
-
id: "oauth-1",
|
|
213
|
-
email: "alice@example.com",
|
|
214
|
-
refreshToken: "oauth-refresh-old",
|
|
215
|
-
access: "oauth-access-old",
|
|
216
|
-
source: "oauth",
|
|
217
|
-
},
|
|
218
|
-
]);
|
|
219
|
-
|
|
220
|
-
const { manager } = await loadManager({ initialStorage });
|
|
221
|
-
|
|
222
|
-
manager.addAccount("oauth-refresh-new", "oauth-access-new", Date.now() + 7_200_000, "alice@example.com");
|
|
223
|
-
|
|
224
|
-
const snapshot = manager.getAccountsSnapshot();
|
|
225
|
-
expect(snapshot).toHaveLength(1);
|
|
226
|
-
expect(snapshot[0]).toMatchObject({
|
|
227
|
-
id: "oauth-1",
|
|
228
|
-
refreshToken: "oauth-refresh-new",
|
|
229
|
-
access: "oauth-access-new",
|
|
230
|
-
email: "alice@example.com",
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
it("matches duplicates by identity rather than refresh token for OAuth accounts", async () => {
|
|
235
|
-
const initialStorage = makeAccountsData([
|
|
236
|
-
{
|
|
237
|
-
id: "oauth-identity",
|
|
238
|
-
email: "identity@example.com",
|
|
239
|
-
refreshToken: "oauth-refresh-a",
|
|
240
|
-
access: "oauth-access-a",
|
|
241
|
-
},
|
|
242
|
-
]);
|
|
243
|
-
|
|
244
|
-
const { manager } = await loadManager({ initialStorage });
|
|
245
|
-
|
|
246
|
-
const updated = manager.addAccount(
|
|
247
|
-
"oauth-refresh-b",
|
|
248
|
-
"oauth-access-b",
|
|
249
|
-
Date.now() + 7_200_000,
|
|
250
|
-
"identity@example.com",
|
|
251
|
-
);
|
|
252
113
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
{
|
|
260
|
-
id: "oauth-meta",
|
|
261
|
-
email: "meta@example.com",
|
|
262
|
-
refreshToken: "oauth-meta-old",
|
|
263
|
-
access: "oauth-meta-access-old",
|
|
264
|
-
addedAt: 1_111,
|
|
265
|
-
lastUsed: 2_222,
|
|
266
|
-
token_updated_at: 3_333,
|
|
267
|
-
lastSwitchReason: "sticky",
|
|
268
|
-
source: "oauth",
|
|
269
|
-
stats: {
|
|
270
|
-
requests: 9,
|
|
271
|
-
inputTokens: 90,
|
|
272
|
-
outputTokens: 45,
|
|
273
|
-
cacheReadTokens: 0,
|
|
274
|
-
cacheWriteTokens: 0,
|
|
275
|
-
lastReset: 4_444,
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
]);
|
|
279
|
-
|
|
280
|
-
const { manager } = await loadManager({ initialStorage });
|
|
281
|
-
|
|
282
|
-
manager.addAccount("oauth-meta-new", "oauth-meta-access-new", Date.now() + 9_000_000, "meta@example.com");
|
|
283
|
-
|
|
284
|
-
const snapshot = manager.getAccountsSnapshot();
|
|
285
|
-
expect(snapshot).toHaveLength(1);
|
|
286
|
-
expect(snapshot[0]).toMatchObject({
|
|
287
|
-
id: "oauth-meta",
|
|
288
|
-
addedAt: 1_111,
|
|
289
|
-
lastUsed: 2_222,
|
|
290
|
-
lastSwitchReason: "sticky",
|
|
291
|
-
source: "oauth",
|
|
292
|
-
stats: expect.objectContaining({
|
|
293
|
-
requests: 9,
|
|
294
|
-
inputTokens: 90,
|
|
295
|
-
outputTokens: 45,
|
|
296
|
-
}),
|
|
114
|
+
const storage = createInMemoryStorage(options.initialStorage);
|
|
115
|
+
const createDefaultStats = vi.fn((now?: number) => makeStats(now ?? Date.now()));
|
|
116
|
+
const authorizeMock = vi.fn().mockResolvedValue({
|
|
117
|
+
url: "https://claude.ai/oauth/authorize?state=test-state",
|
|
118
|
+
verifier: "test-verifier",
|
|
119
|
+
state: "test-state",
|
|
297
120
|
});
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
email: "alpha@example.com",
|
|
306
|
-
refreshToken: "oauth-alpha-old",
|
|
307
|
-
source: "oauth",
|
|
121
|
+
const exchangeMock = vi.fn().mockResolvedValue(
|
|
122
|
+
options.exchangeResult ?? {
|
|
123
|
+
type: "success",
|
|
124
|
+
refresh: "oauth-refresh-fresh",
|
|
125
|
+
access: "oauth-access-fresh",
|
|
126
|
+
expires: Date.now() + 3_600_000,
|
|
127
|
+
email: "alice@example.com",
|
|
308
128
|
},
|
|
309
|
-
{
|
|
310
|
-
id: "oauth-active-b",
|
|
311
|
-
email: "beta@example.com",
|
|
312
|
-
refreshToken: "oauth-beta",
|
|
313
|
-
source: "oauth",
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
{ activeIndex: 1 },
|
|
317
129
|
);
|
|
318
130
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
131
|
+
vi.doMock("./storage.js", () => ({
|
|
132
|
+
createDefaultStats,
|
|
133
|
+
loadAccounts: storage.loadAccountsMock,
|
|
134
|
+
saveAccounts: storage.saveAccountsMock,
|
|
135
|
+
clearAccounts: vi.fn().mockResolvedValue(undefined),
|
|
136
|
+
}));
|
|
322
137
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
138
|
+
vi.doMock("./cc-credentials.js", () => ({
|
|
139
|
+
readCCCredentials: () => options.ccCredentials ?? [],
|
|
140
|
+
}));
|
|
326
141
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
142
|
+
vi.doMock("./config.js", async (importOriginal) => {
|
|
143
|
+
const actual = await importOriginal<typeof ConfigModule>();
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
...actual,
|
|
147
|
+
DEFAULT_CONFIG,
|
|
148
|
+
loadConfig: vi.fn(() => ({
|
|
149
|
+
...DEFAULT_CONFIG,
|
|
150
|
+
signature_emulation: {
|
|
151
|
+
...DEFAULT_CONFIG.signature_emulation,
|
|
152
|
+
fetch_claude_code_version_on_startup: false,
|
|
153
|
+
},
|
|
154
|
+
idle_refresh: {
|
|
155
|
+
...DEFAULT_CONFIG.idle_refresh,
|
|
156
|
+
enabled: false,
|
|
157
|
+
},
|
|
158
|
+
cc_credential_reuse: {
|
|
159
|
+
...DEFAULT_CONFIG.cc_credential_reuse,
|
|
160
|
+
},
|
|
161
|
+
...(options.config ?? {}),
|
|
162
|
+
})),
|
|
163
|
+
};
|
|
347
164
|
});
|
|
348
165
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
id: "oauth-shared-email",
|
|
363
|
-
email: "shared@example.com",
|
|
364
|
-
refreshToken: "oauth-shared-refresh",
|
|
365
|
-
source: "oauth",
|
|
366
|
-
},
|
|
367
|
-
{
|
|
368
|
-
id: "cc-shared-email",
|
|
369
|
-
refreshToken: "cc-refresh-old",
|
|
370
|
-
access: "cc-access-old",
|
|
371
|
-
source: "cc-keychain",
|
|
372
|
-
},
|
|
373
|
-
]);
|
|
374
|
-
|
|
375
|
-
const { manager } = await loadManager({
|
|
376
|
-
initialStorage,
|
|
377
|
-
ccCredentials: [
|
|
378
|
-
makeCCCredential({
|
|
379
|
-
refreshToken: "cc-refresh-new",
|
|
380
|
-
accessToken: "cc-access-new",
|
|
381
|
-
source: "cc-keychain",
|
|
382
|
-
label: "Claude Code-credentials:shared@example.com",
|
|
166
|
+
vi.doMock("./oauth.js", () => ({
|
|
167
|
+
authorize: authorizeMock,
|
|
168
|
+
exchange: exchangeMock,
|
|
169
|
+
}));
|
|
170
|
+
|
|
171
|
+
vi.doMock("./commands/prompts.js", () => ({
|
|
172
|
+
promptAccountMenu: vi.fn().mockResolvedValue("add"),
|
|
173
|
+
promptManageAccounts: vi.fn().mockResolvedValue(undefined),
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
vi.doMock("./bun-fetch.js", () => ({
|
|
177
|
+
createBunFetch: () => ({
|
|
178
|
+
fetch: vi.fn(),
|
|
383
179
|
}),
|
|
384
|
-
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
const { AnthropicAuthPlugin } = await import("./index.js");
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
plugin: await AnthropicAuthPlugin({ client: makeClient() }),
|
|
186
|
+
storage,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function lastSavedStorage(storage: ReturnType<typeof createInMemoryStorage>): AccountStorage {
|
|
191
|
+
const calls = (storage.saveAccountsMock as unknown as Mock).mock.calls as AccountStorage[][];
|
|
192
|
+
const saved = calls[calls.length - 1]?.[0] as AccountStorage | undefined;
|
|
193
|
+
expect(saved).toBeDefined();
|
|
194
|
+
return saved!;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
describe("AccountManager identity-based dedup RED", () => {
|
|
198
|
+
beforeEach(() => {
|
|
199
|
+
vi.useFakeTimers();
|
|
200
|
+
vi.setSystemTime(new Date("2026-04-10T12:00:00Z"));
|
|
385
201
|
});
|
|
386
202
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it("Flow A: CC auto-detect re-auth updates the existing account without creating a duplicate", async () => {
|
|
394
|
-
const { plugin, storage } = await loadPlugin({
|
|
395
|
-
initialStorage: makeAccountsData([
|
|
396
|
-
{
|
|
397
|
-
id: "cc-flow-a",
|
|
398
|
-
refreshToken: "cc-refresh-stale",
|
|
399
|
-
access: "cc-access-stale",
|
|
400
|
-
source: "cc-keychain",
|
|
401
|
-
label: "Claude Code-credentials:alice@example.com",
|
|
402
|
-
},
|
|
403
|
-
]),
|
|
404
|
-
ccCredentials: [
|
|
405
|
-
makeCCCredential({
|
|
406
|
-
refreshToken: "cc-refresh-rotated",
|
|
407
|
-
accessToken: "cc-access-rotated",
|
|
408
|
-
source: "cc-keychain",
|
|
409
|
-
label: "Claude Code-credentials:alice@example.com",
|
|
410
|
-
}),
|
|
411
|
-
],
|
|
203
|
+
afterEach(() => {
|
|
204
|
+
vi.useRealTimers();
|
|
205
|
+
vi.clearAllMocks();
|
|
206
|
+
vi.resetModules();
|
|
412
207
|
});
|
|
413
208
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
209
|
+
it("updates an OAuth account by email instead of creating a duplicate on refresh rotation", async () => {
|
|
210
|
+
const initialStorage = makeAccountsData([
|
|
211
|
+
{
|
|
212
|
+
id: "oauth-1",
|
|
213
|
+
email: "alice@example.com",
|
|
214
|
+
refreshToken: "oauth-refresh-old",
|
|
215
|
+
access: "oauth-access-old",
|
|
216
|
+
source: "oauth",
|
|
217
|
+
},
|
|
218
|
+
]);
|
|
219
|
+
|
|
220
|
+
const { manager } = await loadManager({ initialStorage });
|
|
221
|
+
|
|
222
|
+
manager.addAccount("oauth-refresh-new", "oauth-access-new", Date.now() + 7_200_000, "alice@example.com");
|
|
223
|
+
|
|
224
|
+
const snapshot = manager.getAccountsSnapshot();
|
|
225
|
+
expect(snapshot).toHaveLength(1);
|
|
226
|
+
expect(snapshot[0]).toMatchObject({
|
|
227
|
+
id: "oauth-1",
|
|
228
|
+
refreshToken: "oauth-refresh-new",
|
|
229
|
+
access: "oauth-access-new",
|
|
230
|
+
email: "alice@example.com",
|
|
231
|
+
});
|
|
430
232
|
});
|
|
431
233
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
234
|
+
it("matches duplicates by identity rather than refresh token for OAuth accounts", async () => {
|
|
235
|
+
const initialStorage = makeAccountsData([
|
|
236
|
+
{
|
|
237
|
+
id: "oauth-identity",
|
|
238
|
+
email: "identity@example.com",
|
|
239
|
+
refreshToken: "oauth-refresh-a",
|
|
240
|
+
access: "oauth-access-a",
|
|
241
|
+
},
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
const { manager } = await loadManager({ initialStorage });
|
|
245
|
+
|
|
246
|
+
const updated = manager.addAccount(
|
|
247
|
+
"oauth-refresh-b",
|
|
248
|
+
"oauth-access-b",
|
|
249
|
+
Date.now() + 7_200_000,
|
|
250
|
+
"identity@example.com",
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
expect(updated?.id).toBe("oauth-identity");
|
|
254
|
+
expect(manager.getAccountsSnapshot()).toHaveLength(1);
|
|
445
255
|
});
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
256
|
+
|
|
257
|
+
it("preserves account metadata when an OAuth identity is updated", async () => {
|
|
258
|
+
const initialStorage = makeAccountsData([
|
|
259
|
+
{
|
|
260
|
+
id: "oauth-meta",
|
|
261
|
+
email: "meta@example.com",
|
|
262
|
+
refreshToken: "oauth-meta-old",
|
|
263
|
+
access: "oauth-meta-access-old",
|
|
264
|
+
addedAt: 1_111,
|
|
265
|
+
lastUsed: 2_222,
|
|
266
|
+
token_updated_at: 3_333,
|
|
267
|
+
lastSwitchReason: "sticky",
|
|
268
|
+
source: "oauth",
|
|
269
|
+
stats: {
|
|
270
|
+
requests: 9,
|
|
271
|
+
inputTokens: 90,
|
|
272
|
+
outputTokens: 45,
|
|
273
|
+
cacheReadTokens: 0,
|
|
274
|
+
cacheWriteTokens: 0,
|
|
275
|
+
lastReset: 4_444,
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
const { manager } = await loadManager({ initialStorage });
|
|
281
|
+
|
|
282
|
+
manager.addAccount("oauth-meta-new", "oauth-meta-access-new", Date.now() + 9_000_000, "meta@example.com");
|
|
283
|
+
|
|
284
|
+
const snapshot = manager.getAccountsSnapshot();
|
|
285
|
+
expect(snapshot).toHaveLength(1);
|
|
286
|
+
expect(snapshot[0]).toMatchObject({
|
|
287
|
+
id: "oauth-meta",
|
|
288
|
+
addedAt: 1_111,
|
|
289
|
+
lastUsed: 2_222,
|
|
290
|
+
lastSwitchReason: "sticky",
|
|
291
|
+
source: "oauth",
|
|
292
|
+
stats: expect.objectContaining({
|
|
293
|
+
requests: 9,
|
|
294
|
+
inputTokens: 90,
|
|
295
|
+
outputTokens: 45,
|
|
296
|
+
}),
|
|
297
|
+
});
|
|
466
298
|
});
|
|
467
299
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
300
|
+
it("preserves the active index when dedup updates an existing OAuth identity", async () => {
|
|
301
|
+
const initialStorage = makeAccountsData(
|
|
302
|
+
[
|
|
303
|
+
{
|
|
304
|
+
id: "oauth-active-a",
|
|
305
|
+
email: "alpha@example.com",
|
|
306
|
+
refreshToken: "oauth-alpha-old",
|
|
307
|
+
source: "oauth",
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
id: "oauth-active-b",
|
|
311
|
+
email: "beta@example.com",
|
|
312
|
+
refreshToken: "oauth-beta",
|
|
313
|
+
source: "oauth",
|
|
314
|
+
},
|
|
315
|
+
],
|
|
316
|
+
{ activeIndex: 1 },
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const { manager } = await loadManager({ initialStorage });
|
|
320
|
+
|
|
321
|
+
manager.addAccount("oauth-alpha-new", "oauth-alpha-access-new", Date.now() + 7_200_000, "alpha@example.com");
|
|
322
|
+
|
|
323
|
+
expect(manager.getAccountsSnapshot()).toHaveLength(2);
|
|
324
|
+
expect(manager.getCurrentIndex()).toBe(1);
|
|
491
325
|
});
|
|
492
326
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
327
|
+
it("deduplicates CC accounts by source and label across rotation cycles", async () => {
|
|
328
|
+
const initialStorage = makeAccountsData([
|
|
329
|
+
{
|
|
330
|
+
id: "cc-1",
|
|
331
|
+
refreshToken: "cc-refresh-old",
|
|
332
|
+
access: "cc-access-old",
|
|
333
|
+
source: "cc-keychain",
|
|
334
|
+
},
|
|
335
|
+
]);
|
|
336
|
+
|
|
337
|
+
const { manager } = await loadManager({
|
|
338
|
+
initialStorage,
|
|
339
|
+
ccCredentials: [
|
|
340
|
+
makeCCCredential({
|
|
341
|
+
refreshToken: "cc-refresh-new",
|
|
342
|
+
accessToken: "cc-access-new",
|
|
343
|
+
source: "cc-keychain",
|
|
344
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
345
|
+
}),
|
|
346
|
+
],
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
const snapshot = manager.getAccountsSnapshot();
|
|
350
|
+
expect(snapshot).toHaveLength(1);
|
|
351
|
+
expect(snapshot[0]).toMatchObject({
|
|
352
|
+
id: "cc-1",
|
|
353
|
+
refreshToken: "cc-refresh-new",
|
|
354
|
+
access: "cc-access-new",
|
|
355
|
+
source: "cc-keychain",
|
|
356
|
+
});
|
|
505
357
|
});
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it("enforces MAX_ACCOUNTS during CC auto-detect instead of overflowing capacity", async () => {
|
|
509
|
-
const initialStorage = makeAccountsData(
|
|
510
|
-
Array.from({ length: 10 }, (_, index) => ({
|
|
511
|
-
id: `oauth-${index + 1}`,
|
|
512
|
-
email: `user${index + 1}@example.com`,
|
|
513
|
-
refreshToken: `oauth-refresh-${index + 1}`,
|
|
514
|
-
source: "oauth" as const,
|
|
515
|
-
})),
|
|
516
|
-
);
|
|
517
358
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
359
|
+
it("keeps OAuth and CC accounts separate even when they share an email", async () => {
|
|
360
|
+
const initialStorage = makeAccountsData([
|
|
361
|
+
{
|
|
362
|
+
id: "oauth-shared-email",
|
|
363
|
+
email: "shared@example.com",
|
|
364
|
+
refreshToken: "oauth-shared-refresh",
|
|
365
|
+
source: "oauth",
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
id: "cc-shared-email",
|
|
369
|
+
refreshToken: "cc-refresh-old",
|
|
370
|
+
access: "cc-access-old",
|
|
371
|
+
source: "cc-keychain",
|
|
372
|
+
},
|
|
373
|
+
]);
|
|
374
|
+
|
|
375
|
+
const { manager } = await loadManager({
|
|
376
|
+
initialStorage,
|
|
377
|
+
ccCredentials: [
|
|
378
|
+
makeCCCredential({
|
|
379
|
+
refreshToken: "cc-refresh-new",
|
|
380
|
+
accessToken: "cc-access-new",
|
|
381
|
+
source: "cc-keychain",
|
|
382
|
+
label: "Claude Code-credentials:shared@example.com",
|
|
383
|
+
}),
|
|
384
|
+
],
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const snapshot = manager.getAccountsSnapshot();
|
|
388
|
+
expect(snapshot).toHaveLength(2);
|
|
389
|
+
expect(snapshot.filter((account) => account.source === "oauth")).toHaveLength(1);
|
|
390
|
+
expect(snapshot.filter((account) => account.source === "cc-keychain")).toHaveLength(1);
|
|
527
391
|
});
|
|
528
392
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
393
|
+
it("Flow A: CC auto-detect re-auth updates the existing account without creating a duplicate", async () => {
|
|
394
|
+
const { plugin, storage } = await loadPlugin({
|
|
395
|
+
initialStorage: makeAccountsData([
|
|
396
|
+
{
|
|
397
|
+
id: "cc-flow-a",
|
|
398
|
+
refreshToken: "cc-refresh-stale",
|
|
399
|
+
access: "cc-access-stale",
|
|
400
|
+
source: "cc-keychain",
|
|
401
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
402
|
+
},
|
|
403
|
+
]),
|
|
404
|
+
ccCredentials: [
|
|
405
|
+
makeCCCredential({
|
|
406
|
+
refreshToken: "cc-refresh-rotated",
|
|
407
|
+
accessToken: "cc-access-rotated",
|
|
408
|
+
source: "cc-keychain",
|
|
409
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
410
|
+
}),
|
|
411
|
+
],
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const method = plugin.auth.methods[0];
|
|
415
|
+
expect(method).toBeDefined();
|
|
416
|
+
if (!method) {
|
|
417
|
+
throw new Error("Expected Claude Code auth method");
|
|
418
|
+
}
|
|
419
|
+
expect(method.authorize).toBeTypeOf("function");
|
|
420
|
+
if (!method.authorize) {
|
|
421
|
+
throw new Error("Expected Claude Code authorize handler");
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const credentials = await method.authorize();
|
|
425
|
+
|
|
426
|
+
expect(credentials).toMatchObject({
|
|
427
|
+
type: "success",
|
|
428
|
+
refresh: "cc-refresh-rotated",
|
|
429
|
+
access: "cc-access-rotated",
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const saved = lastSavedStorage(storage);
|
|
433
|
+
expect(saved.accounts).toHaveLength(1);
|
|
434
|
+
expect(saved.accounts[0]).toMatchObject({
|
|
435
|
+
id: "cc-flow-a",
|
|
436
|
+
refreshToken: "cc-refresh-rotated",
|
|
437
|
+
access: "cc-access-rotated",
|
|
438
|
+
source: "cc-keychain",
|
|
439
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
440
|
+
identity: {
|
|
441
|
+
kind: "cc",
|
|
442
|
+
source: "cc-keychain",
|
|
443
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
});
|
|
554
447
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
448
|
+
it("Flow B: OAuth re-auth updates the existing account for the same email without creating a duplicate", async () => {
|
|
449
|
+
const { plugin, storage } = await loadPlugin({
|
|
450
|
+
initialStorage: makeAccountsData([
|
|
451
|
+
{
|
|
452
|
+
id: "oauth-flow-b",
|
|
453
|
+
email: "alice@example.com",
|
|
454
|
+
refreshToken: "oauth-refresh-stale",
|
|
455
|
+
access: "oauth-access-stale",
|
|
456
|
+
source: "oauth",
|
|
457
|
+
},
|
|
458
|
+
]),
|
|
459
|
+
exchangeResult: {
|
|
460
|
+
type: "success",
|
|
461
|
+
refresh: "oauth-refresh-rotated",
|
|
462
|
+
access: "oauth-access-rotated",
|
|
463
|
+
expires: Date.now() + 7_200_000,
|
|
464
|
+
email: "alice@example.com",
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const method = plugin.auth.methods[1];
|
|
469
|
+
expect(method).toBeDefined();
|
|
470
|
+
if (!method) {
|
|
471
|
+
throw new Error("Expected OAuth auth method");
|
|
472
|
+
}
|
|
473
|
+
expect(method.authorize).toBeTypeOf("function");
|
|
474
|
+
if (!method.authorize) {
|
|
475
|
+
throw new Error("Expected OAuth authorize handler");
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
const authResult = await method.authorize();
|
|
479
|
+
expect(authResult.callback).toBeTypeOf("function");
|
|
480
|
+
if (!authResult.callback) {
|
|
481
|
+
throw new Error("Expected OAuth callback");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const credentials = await authResult.callback("oauth-code#test-state");
|
|
485
|
+
|
|
486
|
+
expect(credentials).toMatchObject({
|
|
487
|
+
type: "success",
|
|
488
|
+
refresh: "oauth-refresh-rotated",
|
|
489
|
+
access: "oauth-access-rotated",
|
|
490
|
+
email: "alice@example.com",
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
const saved = lastSavedStorage(storage);
|
|
494
|
+
expect(saved.accounts).toHaveLength(1);
|
|
495
|
+
expect(saved.accounts[0]).toMatchObject({
|
|
496
|
+
id: "oauth-flow-b",
|
|
497
|
+
email: "alice@example.com",
|
|
498
|
+
refreshToken: "oauth-refresh-rotated",
|
|
499
|
+
access: "oauth-access-rotated",
|
|
500
|
+
source: "oauth",
|
|
501
|
+
identity: {
|
|
502
|
+
kind: "oauth",
|
|
503
|
+
email: "alice@example.com",
|
|
504
|
+
},
|
|
505
|
+
});
|
|
506
|
+
});
|
|
585
507
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
makeStoredAccount({
|
|
610
|
-
id: "oauth-disk-only",
|
|
611
|
-
email: "disk-only@example.com",
|
|
612
|
-
refreshToken: "oauth-disk-only",
|
|
613
|
-
source: "oauth",
|
|
614
|
-
addedAt: 9_999,
|
|
615
|
-
stats: makeStats(9_999),
|
|
616
|
-
}),
|
|
617
|
-
],
|
|
618
|
-
}));
|
|
508
|
+
it("enforces MAX_ACCOUNTS during CC auto-detect instead of overflowing capacity", async () => {
|
|
509
|
+
const initialStorage = makeAccountsData(
|
|
510
|
+
Array.from({ length: 10 }, (_, index) => ({
|
|
511
|
+
id: `oauth-${index + 1}`,
|
|
512
|
+
email: `user${index + 1}@example.com`,
|
|
513
|
+
refreshToken: `oauth-refresh-${index + 1}`,
|
|
514
|
+
source: "oauth" as const,
|
|
515
|
+
})),
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
const { manager } = await loadManager({
|
|
519
|
+
initialStorage,
|
|
520
|
+
ccCredentials: [
|
|
521
|
+
makeCCCredential({
|
|
522
|
+
refreshToken: "cc-refresh-overflow",
|
|
523
|
+
source: "cc-file",
|
|
524
|
+
label: "/Users/test/.claude/.credentials.json",
|
|
525
|
+
}),
|
|
526
|
+
],
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
expect(manager.getAccountsSnapshot()).toHaveLength(10);
|
|
530
|
+
});
|
|
619
531
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
addedAt: 8_888,
|
|
648
|
-
stats: makeStats(8_888),
|
|
649
|
-
}),
|
|
650
|
-
],
|
|
651
|
-
}));
|
|
532
|
+
it("preserves the source field when syncing a rotated account from disk", async () => {
|
|
533
|
+
const initialStorage = makeAccountsData([
|
|
534
|
+
{
|
|
535
|
+
id: "cc-sync-source",
|
|
536
|
+
refreshToken: "cc-sync-old",
|
|
537
|
+
access: "cc-sync-access-old",
|
|
538
|
+
source: "cc-file",
|
|
539
|
+
},
|
|
540
|
+
]);
|
|
541
|
+
|
|
542
|
+
const { manager, storage } = await loadManager({ initialStorage });
|
|
543
|
+
|
|
544
|
+
storage.mutateDiskOnly((disk) => ({
|
|
545
|
+
...disk,
|
|
546
|
+
accounts: disk.accounts.map((account) => ({
|
|
547
|
+
...account,
|
|
548
|
+
refreshToken: "cc-sync-new",
|
|
549
|
+
access: "cc-sync-access-new",
|
|
550
|
+
token_updated_at: Date.now() + 5_000,
|
|
551
|
+
source: "cc-file",
|
|
552
|
+
})),
|
|
553
|
+
}));
|
|
554
|
+
|
|
555
|
+
await manager.syncActiveIndexFromDisk();
|
|
556
|
+
|
|
557
|
+
expect(manager.getAccountsSnapshot()[0]?.source).toBe("cc-file");
|
|
558
|
+
});
|
|
652
559
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
560
|
+
it("preserves in-flight object references while syncing rotated auth from disk", async () => {
|
|
561
|
+
const initialStorage = makeAccountsData([
|
|
562
|
+
{
|
|
563
|
+
id: "oauth-ref-preserve",
|
|
564
|
+
email: "ref@example.com",
|
|
565
|
+
refreshToken: "oauth-ref-old",
|
|
566
|
+
access: "oauth-ref-access-old",
|
|
567
|
+
source: "oauth",
|
|
568
|
+
},
|
|
569
|
+
]);
|
|
570
|
+
|
|
571
|
+
const { manager, storage } = await loadManager({ initialStorage });
|
|
572
|
+
|
|
573
|
+
const currentAccount = manager.getCurrentAccount();
|
|
574
|
+
expect(currentAccount).not.toBeNull();
|
|
575
|
+
|
|
576
|
+
storage.mutateDiskOnly((disk) => ({
|
|
577
|
+
...disk,
|
|
578
|
+
accounts: disk.accounts.map((account) => ({
|
|
579
|
+
...account,
|
|
580
|
+
refreshToken: "oauth-ref-new",
|
|
581
|
+
access: "oauth-ref-access-new",
|
|
582
|
+
token_updated_at: Date.now() + 5_000,
|
|
583
|
+
})),
|
|
584
|
+
}));
|
|
585
|
+
|
|
586
|
+
await manager.syncActiveIndexFromDisk();
|
|
587
|
+
|
|
588
|
+
const activeAfterSync = manager.getCurrentAccount();
|
|
589
|
+
expect(activeAfterSync).toBe(currentAccount);
|
|
590
|
+
expect(currentAccount?.refreshToken).toBe("oauth-ref-new");
|
|
591
|
+
});
|
|
675
592
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
593
|
+
it("unions disk-only accounts during save instead of dropping them", async () => {
|
|
594
|
+
const initialStorage = makeAccountsData([
|
|
595
|
+
{
|
|
596
|
+
id: "oauth-save-primary",
|
|
597
|
+
email: "primary@example.com",
|
|
598
|
+
refreshToken: "oauth-save-primary",
|
|
599
|
+
source: "oauth",
|
|
600
|
+
},
|
|
601
|
+
]);
|
|
602
|
+
|
|
603
|
+
const { manager, storage } = await loadManager({ initialStorage });
|
|
604
|
+
|
|
605
|
+
storage.mutateDiskOnly((disk) => ({
|
|
606
|
+
...disk,
|
|
607
|
+
accounts: [
|
|
608
|
+
...disk.accounts,
|
|
609
|
+
makeStoredAccount({
|
|
610
|
+
id: "oauth-disk-only",
|
|
611
|
+
email: "disk-only@example.com",
|
|
612
|
+
refreshToken: "oauth-disk-only",
|
|
613
|
+
source: "oauth",
|
|
614
|
+
addedAt: 9_999,
|
|
615
|
+
stats: makeStats(9_999),
|
|
616
|
+
}),
|
|
617
|
+
],
|
|
618
|
+
}));
|
|
619
|
+
|
|
620
|
+
await manager.saveToDisk();
|
|
621
|
+
|
|
622
|
+
const saved = lastSavedStorage(storage);
|
|
623
|
+
expect(saved.accounts.map((account) => account.id).sort()).toEqual(["oauth-disk-only", "oauth-save-primary"]);
|
|
624
|
+
});
|
|
692
625
|
|
|
693
|
-
|
|
626
|
+
it("does not lose disk-only accounts on repeated saves", async () => {
|
|
627
|
+
const initialStorage = makeAccountsData([
|
|
628
|
+
{
|
|
629
|
+
id: "oauth-repeat-primary",
|
|
630
|
+
email: "repeat-primary@example.com",
|
|
631
|
+
refreshToken: "oauth-repeat-primary",
|
|
632
|
+
source: "oauth",
|
|
633
|
+
},
|
|
634
|
+
]);
|
|
635
|
+
|
|
636
|
+
const { manager, storage } = await loadManager({ initialStorage });
|
|
637
|
+
|
|
638
|
+
storage.mutateDiskOnly((disk) => ({
|
|
639
|
+
...disk,
|
|
640
|
+
accounts: [
|
|
641
|
+
...disk.accounts,
|
|
642
|
+
makeStoredAccount({
|
|
643
|
+
id: "oauth-repeat-disk-only",
|
|
644
|
+
email: "repeat-disk-only@example.com",
|
|
645
|
+
refreshToken: "oauth-repeat-disk-only",
|
|
646
|
+
source: "oauth",
|
|
647
|
+
addedAt: 8_888,
|
|
648
|
+
stats: makeStats(8_888),
|
|
649
|
+
}),
|
|
650
|
+
],
|
|
651
|
+
}));
|
|
652
|
+
|
|
653
|
+
await manager.saveToDisk();
|
|
654
|
+
await manager.saveToDisk();
|
|
655
|
+
|
|
656
|
+
const saved = lastSavedStorage(storage);
|
|
657
|
+
expect(saved.accounts.map((account) => account.id).sort()).toEqual([
|
|
658
|
+
"oauth-repeat-disk-only",
|
|
659
|
+
"oauth-repeat-primary",
|
|
660
|
+
]);
|
|
661
|
+
});
|
|
694
662
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
663
|
+
it("keeps the same active account when disk-only unions shift array positions", async () => {
|
|
664
|
+
const initialStorage = makeAccountsData(
|
|
665
|
+
[
|
|
666
|
+
{
|
|
667
|
+
id: "oauth-active-keep",
|
|
668
|
+
email: "active@example.com",
|
|
669
|
+
refreshToken: "oauth-active-keep",
|
|
670
|
+
source: "oauth",
|
|
671
|
+
},
|
|
672
|
+
],
|
|
673
|
+
{ activeIndex: 0 },
|
|
674
|
+
);
|
|
675
|
+
|
|
676
|
+
const { manager, storage } = await loadManager({ initialStorage });
|
|
677
|
+
|
|
678
|
+
storage.mutateDiskOnly((disk) => ({
|
|
679
|
+
...disk,
|
|
680
|
+
accounts: [
|
|
681
|
+
makeStoredAccount({
|
|
682
|
+
id: "oauth-prepended-disk-only",
|
|
683
|
+
email: "prepended@example.com",
|
|
684
|
+
refreshToken: "oauth-prepended-disk-only",
|
|
685
|
+
source: "oauth",
|
|
686
|
+
addedAt: 777,
|
|
687
|
+
stats: makeStats(777),
|
|
688
|
+
}),
|
|
689
|
+
...disk.accounts,
|
|
690
|
+
],
|
|
691
|
+
}));
|
|
692
|
+
|
|
693
|
+
await manager.saveToDisk();
|
|
694
|
+
|
|
695
|
+
const saved = lastSavedStorage(storage);
|
|
696
|
+
expect(saved.accounts[saved.activeIndex]?.id).toBe("oauth-active-keep");
|
|
697
|
+
});
|
|
698
698
|
});
|