@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
|
@@ -4,23 +4,23 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
|
|
5
5
|
|
|
6
6
|
vi.mock("node:child_process", () => ({
|
|
7
|
-
|
|
7
|
+
execSync: vi.fn(),
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
10
|
vi.mock("node:fs", () => ({
|
|
11
|
-
|
|
11
|
+
readFileSync: vi.fn(),
|
|
12
12
|
}));
|
|
13
13
|
|
|
14
14
|
vi.mock("node:os", () => ({
|
|
15
|
-
|
|
15
|
+
homedir: vi.fn(() => "/mock-home"),
|
|
16
16
|
}));
|
|
17
17
|
|
|
18
18
|
import type { CCCredential } from "../cc-credentials.js";
|
|
19
19
|
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
parseCCCredentialData,
|
|
21
|
+
readCCCredentials,
|
|
22
|
+
readCCCredentialsFromFile,
|
|
23
|
+
readCCCredentialsFromKeychain,
|
|
24
24
|
} from "../cc-credentials.js";
|
|
25
25
|
|
|
26
26
|
const mockExecSync = execSync as Mock;
|
|
@@ -29,298 +29,302 @@ const mockHomedir = homedir as Mock;
|
|
|
29
29
|
const originalPlatformDescriptor = Object.getOwnPropertyDescriptor(process, "platform");
|
|
30
30
|
|
|
31
31
|
function setPlatform(platform: NodeJS.Platform): void {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
Object.defineProperty(process, "platform", {
|
|
33
|
+
value: platform,
|
|
34
|
+
configurable: true,
|
|
35
|
+
});
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
function restorePlatform(): void {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
if (originalPlatformDescriptor) {
|
|
40
|
+
Object.defineProperty(process, "platform", originalPlatformDescriptor);
|
|
41
|
+
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function makeWrappedCredential(overrides: Record<string, unknown> = {}): string {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
45
|
+
return JSON.stringify({
|
|
46
|
+
claudeAiOauth: {
|
|
47
|
+
accessToken: "access-token",
|
|
48
|
+
refreshToken: "refresh-token",
|
|
49
|
+
expiresAt: 1_700_000_000_000,
|
|
50
|
+
subscriptionType: "max",
|
|
51
|
+
...overrides,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function makeFlatCredential(overrides: Record<string, unknown> = {}): string {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
return JSON.stringify({
|
|
58
|
+
accessToken: "flat-access",
|
|
59
|
+
refreshToken: "flat-refresh",
|
|
60
|
+
expiresAt: 1_800_000_000_000,
|
|
61
|
+
subscriptionType: "pro",
|
|
62
|
+
...overrides,
|
|
63
|
+
});
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function makeSecurityError(status?: number, code?: string): Error {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
const error = new Error("security failed") as Error & { status?: number; code?: string };
|
|
68
|
+
error.status = status;
|
|
69
|
+
error.code = code;
|
|
70
|
+
return error;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
describe("parseCCCredentialData", () => {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
afterEach(() => {
|
|
81
|
-
restorePlatform();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("parses wrapped Claude Code credentials", () => {
|
|
85
|
-
expect(parseCCCredentialData(makeWrappedCredential())).toEqual({
|
|
86
|
-
accessToken: "access-token",
|
|
87
|
-
refreshToken: "refresh-token",
|
|
88
|
-
expiresAt: 1_700_000_000_000,
|
|
89
|
-
subscriptionType: "max",
|
|
90
|
-
source: "cc-file",
|
|
91
|
-
label: "/mock-home/.claude/.credentials.json",
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
vi.resetAllMocks();
|
|
76
|
+
mockHomedir.mockReturnValue("/mock-home");
|
|
77
|
+
restorePlatform();
|
|
92
78
|
});
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
expect(parseCCCredentialData(makeFlatCredential())).toEqual({
|
|
97
|
-
accessToken: "flat-access",
|
|
98
|
-
refreshToken: "flat-refresh",
|
|
99
|
-
expiresAt: 1_800_000_000_000,
|
|
100
|
-
subscriptionType: "pro",
|
|
101
|
-
source: "cc-file",
|
|
102
|
-
label: "/mock-home/.claude/.credentials.json",
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
restorePlatform();
|
|
103
82
|
});
|
|
104
|
-
});
|
|
105
83
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
84
|
+
it("parses wrapped Claude Code credentials", () => {
|
|
85
|
+
expect(parseCCCredentialData(makeWrappedCredential())).toEqual({
|
|
86
|
+
accessToken: "access-token",
|
|
87
|
+
refreshToken: "refresh-token",
|
|
88
|
+
expiresAt: 1_700_000_000_000,
|
|
89
|
+
subscriptionType: "max",
|
|
90
|
+
source: "cc-file",
|
|
91
|
+
label: "/mock-home/.claude/.credentials.json",
|
|
92
|
+
});
|
|
93
|
+
});
|
|
109
94
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
95
|
+
it("parses flat credential format", () => {
|
|
96
|
+
expect(parseCCCredentialData(makeFlatCredential())).toEqual({
|
|
97
|
+
accessToken: "flat-access",
|
|
98
|
+
refreshToken: "flat-refresh",
|
|
99
|
+
expiresAt: 1_800_000_000_000,
|
|
100
|
+
subscriptionType: "pro",
|
|
101
|
+
source: "cc-file",
|
|
102
|
+
label: "/mock-home/.claude/.credentials.json",
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("filters MCP-only payloads", () => {
|
|
107
|
+
expect(parseCCCredentialData(JSON.stringify({ mcpOAuth: { accessToken: "mcp-only" } }))).toBeNull();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("returns null for malformed JSON", () => {
|
|
111
|
+
expect(parseCCCredentialData("not-json")).toBeNull();
|
|
112
|
+
});
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
describe("readCCCredentialsFromKeychain", () => {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
afterEach(() => {
|
|
123
|
-
restorePlatform();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("reads multiple Claude Code services from macOS Keychain", () => {
|
|
127
|
-
mockExecSync.mockImplementation((command: string) => {
|
|
128
|
-
if (command === "security dump-keychain") {
|
|
129
|
-
return [
|
|
130
|
-
' "svce"<blob>="Claude Code-credentials"',
|
|
131
|
-
' "svce"<blob>="Claude Code-credentials-abc123"',
|
|
132
|
-
' "svce"<blob>="ignored-service"',
|
|
133
|
-
].join("\n");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (command === "security find-generic-password -s 'Claude Code-credentials' -w") {
|
|
137
|
-
return makeWrappedCredential();
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (command === "security find-generic-password -s 'Claude Code-credentials-abc123' -w") {
|
|
141
|
-
return makeFlatCredential({ accessToken: "access-2", refreshToken: "refresh-2", subscriptionType: "team" });
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
throw new Error(`unexpected command: ${command}`);
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
vi.resetAllMocks();
|
|
118
|
+
mockHomedir.mockReturnValue("/mock-home");
|
|
119
|
+
setPlatform("darwin");
|
|
145
120
|
});
|
|
146
121
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
accessToken: "access-token",
|
|
150
|
-
refreshToken: "refresh-token",
|
|
151
|
-
expiresAt: 1_700_000_000_000,
|
|
152
|
-
subscriptionType: "max",
|
|
153
|
-
source: "cc-keychain",
|
|
154
|
-
label: "Claude Code-credentials",
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
accessToken: "access-2",
|
|
158
|
-
refreshToken: "refresh-2",
|
|
159
|
-
expiresAt: 1_800_000_000_000,
|
|
160
|
-
subscriptionType: "team",
|
|
161
|
-
source: "cc-keychain",
|
|
162
|
-
label: "Claude Code-credentials-abc123",
|
|
163
|
-
},
|
|
164
|
-
]);
|
|
165
|
-
|
|
166
|
-
expect(mockExecSync).toHaveBeenCalledWith("security dump-keychain", {
|
|
167
|
-
encoding: "utf-8",
|
|
168
|
-
timeout: 5000,
|
|
122
|
+
afterEach(() => {
|
|
123
|
+
restorePlatform();
|
|
169
124
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
125
|
+
|
|
126
|
+
it("reads multiple Claude Code services from macOS Keychain", () => {
|
|
127
|
+
mockExecSync.mockImplementation((command: string) => {
|
|
128
|
+
if (command === "security dump-keychain") {
|
|
129
|
+
return [
|
|
130
|
+
' "svce"<blob>="Claude Code-credentials"',
|
|
131
|
+
' "svce"<blob>="Claude Code-credentials-abc123"',
|
|
132
|
+
' "svce"<blob>="ignored-service"',
|
|
133
|
+
].join("\n");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (command === "security find-generic-password -s 'Claude Code-credentials' -w") {
|
|
137
|
+
return makeWrappedCredential();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (command === "security find-generic-password -s 'Claude Code-credentials-abc123' -w") {
|
|
141
|
+
return makeFlatCredential({
|
|
142
|
+
accessToken: "access-2",
|
|
143
|
+
refreshToken: "refresh-2",
|
|
144
|
+
subscriptionType: "team",
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
throw new Error(`unexpected command: ${command}`);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(readCCCredentialsFromKeychain()).toEqual<CCCredential[]>([
|
|
152
|
+
{
|
|
153
|
+
accessToken: "access-token",
|
|
154
|
+
refreshToken: "refresh-token",
|
|
155
|
+
expiresAt: 1_700_000_000_000,
|
|
156
|
+
subscriptionType: "max",
|
|
157
|
+
source: "cc-keychain",
|
|
158
|
+
label: "Claude Code-credentials",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
accessToken: "access-2",
|
|
162
|
+
refreshToken: "refresh-2",
|
|
163
|
+
expiresAt: 1_800_000_000_000,
|
|
164
|
+
subscriptionType: "team",
|
|
165
|
+
source: "cc-keychain",
|
|
166
|
+
label: "Claude Code-credentials-abc123",
|
|
167
|
+
},
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
expect(mockExecSync).toHaveBeenCalledWith("security dump-keychain", {
|
|
171
|
+
encoding: "utf-8",
|
|
172
|
+
timeout: 5000,
|
|
173
|
+
});
|
|
178
174
|
});
|
|
179
175
|
|
|
180
|
-
|
|
181
|
-
|
|
176
|
+
it.each([44, 36, 128])("returns null for handled security exit code %i", (status) => {
|
|
177
|
+
mockExecSync.mockImplementation((command: string) => {
|
|
178
|
+
if (command === "security dump-keychain") {
|
|
179
|
+
throw makeSecurityError(status);
|
|
180
|
+
}
|
|
181
|
+
return "";
|
|
182
|
+
});
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
mockExecSync.mockImplementation((command: string) => {
|
|
185
|
-
if (command === "security dump-keychain") {
|
|
186
|
-
throw makeSecurityError(undefined, "ETIMEDOUT");
|
|
187
|
-
}
|
|
188
|
-
return "";
|
|
184
|
+
expect(readCCCredentialsFromKeychain()).toBeNull();
|
|
189
185
|
});
|
|
190
186
|
|
|
191
|
-
|
|
192
|
-
|
|
187
|
+
it("returns null when security command times out", () => {
|
|
188
|
+
mockExecSync.mockImplementation((command: string) => {
|
|
189
|
+
if (command === "security dump-keychain") {
|
|
190
|
+
throw makeSecurityError(undefined, "ETIMEDOUT");
|
|
191
|
+
}
|
|
192
|
+
return "";
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(readCCCredentialsFromKeychain()).toBeNull();
|
|
196
|
+
});
|
|
193
197
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
198
|
+
it("returns null when a service payload is missing usable Claude credentials", () => {
|
|
199
|
+
mockExecSync.mockImplementation((command: string) => {
|
|
200
|
+
if (command === "security dump-keychain") {
|
|
201
|
+
return ' "svce"<blob>="Claude Code-credentials"';
|
|
202
|
+
}
|
|
199
203
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
204
|
+
if (command === "security find-generic-password -s 'Claude Code-credentials' -w") {
|
|
205
|
+
return JSON.stringify({ mcpOAuth: { accessToken: "mcp-only" } });
|
|
206
|
+
}
|
|
203
207
|
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
throw new Error(`unexpected command: ${command}`);
|
|
209
|
+
});
|
|
206
210
|
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
expect(readCCCredentialsFromKeychain()).toBeNull();
|
|
212
|
+
});
|
|
209
213
|
});
|
|
210
214
|
|
|
211
215
|
describe("readCCCredentialsFromFile", () => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
afterEach(() => {
|
|
219
|
-
restorePlatform();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("reads wrapped credentials from ~/.claude/.credentials.json", () => {
|
|
223
|
-
mockReadFileSync.mockReturnValue(makeWrappedCredential());
|
|
224
|
-
|
|
225
|
-
expect(readCCCredentialsFromFile()).toEqual<CCCredential>({
|
|
226
|
-
accessToken: "access-token",
|
|
227
|
-
refreshToken: "refresh-token",
|
|
228
|
-
expiresAt: 1_700_000_000_000,
|
|
229
|
-
subscriptionType: "max",
|
|
230
|
-
source: "cc-file",
|
|
231
|
-
label: "/mock-home/.claude/.credentials.json",
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
vi.resetAllMocks();
|
|
218
|
+
mockHomedir.mockReturnValue("/mock-home");
|
|
219
|
+
restorePlatform();
|
|
232
220
|
});
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
mockReadFileSync.mockReturnValue(makeFlatCredential());
|
|
237
|
-
|
|
238
|
-
expect(readCCCredentialsFromFile()).toEqual<CCCredential>({
|
|
239
|
-
accessToken: "flat-access",
|
|
240
|
-
refreshToken: "flat-refresh",
|
|
241
|
-
expiresAt: 1_800_000_000_000,
|
|
242
|
-
subscriptionType: "pro",
|
|
243
|
-
source: "cc-file",
|
|
244
|
-
label: "/mock-home/.claude/.credentials.json",
|
|
221
|
+
|
|
222
|
+
afterEach(() => {
|
|
223
|
+
restorePlatform();
|
|
245
224
|
});
|
|
246
|
-
});
|
|
247
225
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
226
|
+
it("reads wrapped credentials from ~/.claude/.credentials.json", () => {
|
|
227
|
+
mockReadFileSync.mockReturnValue(makeWrappedCredential());
|
|
228
|
+
|
|
229
|
+
expect(readCCCredentialsFromFile()).toEqual<CCCredential>({
|
|
230
|
+
accessToken: "access-token",
|
|
231
|
+
refreshToken: "refresh-token",
|
|
232
|
+
expiresAt: 1_700_000_000_000,
|
|
233
|
+
subscriptionType: "max",
|
|
234
|
+
source: "cc-file",
|
|
235
|
+
label: "/mock-home/.claude/.credentials.json",
|
|
236
|
+
});
|
|
253
237
|
});
|
|
254
238
|
|
|
255
|
-
|
|
256
|
-
|
|
239
|
+
it("reads flat credentials from ~/.claude/.credentials.json", () => {
|
|
240
|
+
mockReadFileSync.mockReturnValue(makeFlatCredential());
|
|
241
|
+
|
|
242
|
+
expect(readCCCredentialsFromFile()).toEqual<CCCredential>({
|
|
243
|
+
accessToken: "flat-access",
|
|
244
|
+
refreshToken: "flat-refresh",
|
|
245
|
+
expiresAt: 1_800_000_000_000,
|
|
246
|
+
subscriptionType: "pro",
|
|
247
|
+
source: "cc-file",
|
|
248
|
+
label: "/mock-home/.claude/.credentials.json",
|
|
249
|
+
});
|
|
250
|
+
});
|
|
257
251
|
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
it("returns null when the credentials file is missing", () => {
|
|
253
|
+
const error = new Error("missing") as Error & { code?: string };
|
|
254
|
+
error.code = "ENOENT";
|
|
255
|
+
mockReadFileSync.mockImplementation(() => {
|
|
256
|
+
throw error;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(readCCCredentialsFromFile()).toBeNull();
|
|
260
|
+
});
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
|
|
262
|
+
it("returns null when the file is malformed", () => {
|
|
263
|
+
mockReadFileSync.mockReturnValue("not-json");
|
|
264
|
+
|
|
265
|
+
expect(readCCCredentialsFromFile()).toBeNull();
|
|
266
|
+
});
|
|
263
267
|
});
|
|
264
268
|
|
|
265
269
|
describe("readCCCredentials", () => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
vi.resetAllMocks();
|
|
272
|
+
mockHomedir.mockReturnValue("/mock-home");
|
|
273
|
+
});
|
|
270
274
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
275
|
+
afterEach(() => {
|
|
276
|
+
restorePlatform();
|
|
277
|
+
});
|
|
274
278
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
it("skips Keychain on non-macOS platforms and still reads the file", () => {
|
|
280
|
+
setPlatform("linux");
|
|
281
|
+
mockReadFileSync.mockReturnValue(makeFlatCredential());
|
|
282
|
+
|
|
283
|
+
expect(readCCCredentials()).toEqual<CCCredential[]>([
|
|
284
|
+
{
|
|
285
|
+
accessToken: "flat-access",
|
|
286
|
+
refreshToken: "flat-refresh",
|
|
287
|
+
expiresAt: 1_800_000_000_000,
|
|
288
|
+
subscriptionType: "pro",
|
|
289
|
+
source: "cc-file",
|
|
290
|
+
label: "/mock-home/.claude/.credentials.json",
|
|
291
|
+
},
|
|
292
|
+
]);
|
|
293
|
+
expect(mockExecSync).not.toHaveBeenCalled();
|
|
294
|
+
});
|
|
278
295
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
296
|
+
it("combines Keychain and file credentials on macOS", () => {
|
|
297
|
+
setPlatform("darwin");
|
|
298
|
+
mockExecSync.mockImplementation((command: string) => {
|
|
299
|
+
if (command === "security dump-keychain") {
|
|
300
|
+
return ' "svce"<blob>="Claude Code-credentials"';
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (command === "security find-generic-password -s 'Claude Code-credentials' -w") {
|
|
304
|
+
return makeWrappedCredential();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
throw new Error(`unexpected command: ${command}`);
|
|
308
|
+
});
|
|
309
|
+
mockReadFileSync.mockReturnValue(makeFlatCredential());
|
|
310
|
+
|
|
311
|
+
expect(readCCCredentials()).toEqual<CCCredential[]>([
|
|
312
|
+
{
|
|
313
|
+
accessToken: "access-token",
|
|
314
|
+
refreshToken: "refresh-token",
|
|
315
|
+
expiresAt: 1_700_000_000_000,
|
|
316
|
+
subscriptionType: "max",
|
|
317
|
+
source: "cc-keychain",
|
|
318
|
+
label: "Claude Code-credentials",
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
accessToken: "flat-access",
|
|
322
|
+
refreshToken: "flat-refresh",
|
|
323
|
+
expiresAt: 1_800_000_000_000,
|
|
324
|
+
subscriptionType: "pro",
|
|
325
|
+
source: "cc-file",
|
|
326
|
+
label: "/mock-home/.claude/.credentials.json",
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
304
329
|
});
|
|
305
|
-
mockReadFileSync.mockReturnValue(makeFlatCredential());
|
|
306
|
-
|
|
307
|
-
expect(readCCCredentials()).toEqual<CCCredential[]>([
|
|
308
|
-
{
|
|
309
|
-
accessToken: "access-token",
|
|
310
|
-
refreshToken: "refresh-token",
|
|
311
|
-
expiresAt: 1_700_000_000_000,
|
|
312
|
-
subscriptionType: "max",
|
|
313
|
-
source: "cc-keychain",
|
|
314
|
-
label: "Claude Code-credentials",
|
|
315
|
-
},
|
|
316
|
-
{
|
|
317
|
-
accessToken: "flat-access",
|
|
318
|
-
refreshToken: "flat-refresh",
|
|
319
|
-
expiresAt: 1_800_000_000_000,
|
|
320
|
-
subscriptionType: "pro",
|
|
321
|
-
source: "cc-file",
|
|
322
|
-
label: "/mock-home/.claude/.credentials.json",
|
|
323
|
-
},
|
|
324
|
-
]);
|
|
325
|
-
});
|
|
326
330
|
});
|