@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
|
@@ -2,212 +2,217 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import type { CCCredential } from "./cc-credentials.js";
|
|
3
3
|
import type { ManagedAccount } from "./accounts.js";
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
findByIdentity,
|
|
6
|
+
identitiesMatch,
|
|
7
|
+
resolveIdentity,
|
|
8
|
+
resolveIdentityFromCCCredential,
|
|
9
|
+
resolveIdentityFromOAuthExchange,
|
|
10
|
+
serializeIdentity,
|
|
11
|
+
type AccountIdentity,
|
|
12
12
|
} from "./account-identity.js";
|
|
13
13
|
|
|
14
14
|
describe("account-identity", () => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
email: "alice@example.com",
|
|
19
|
-
refreshToken: "rt_oauth_123",
|
|
20
|
-
access: "at_oauth_123",
|
|
21
|
-
expires: Date.now() + 3600000,
|
|
22
|
-
tokenUpdatedAt: Date.now(),
|
|
23
|
-
addedAt: Date.now(),
|
|
24
|
-
lastUsed: 0,
|
|
25
|
-
enabled: true,
|
|
26
|
-
rateLimitResetTimes: {},
|
|
27
|
-
consecutiveFailures: 0,
|
|
28
|
-
lastFailureTime: null,
|
|
29
|
-
stats: {
|
|
30
|
-
requests: 0,
|
|
31
|
-
inputTokens: 0,
|
|
32
|
-
outputTokens: 0,
|
|
33
|
-
cacheReadTokens: 0,
|
|
34
|
-
cacheWriteTokens: 0,
|
|
35
|
-
lastReset: Date.now(),
|
|
36
|
-
},
|
|
37
|
-
source: "oauth",
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const ccAccount: ManagedAccount = {
|
|
41
|
-
id: "cc-1",
|
|
42
|
-
index: 1,
|
|
43
|
-
email: undefined,
|
|
44
|
-
label: "Claude Code-credentials:alice@example.com",
|
|
45
|
-
refreshToken: "rt_cc_456",
|
|
46
|
-
access: "at_cc_456",
|
|
47
|
-
expires: Date.now() + 3600000,
|
|
48
|
-
tokenUpdatedAt: Date.now(),
|
|
49
|
-
addedAt: Date.now(),
|
|
50
|
-
lastUsed: 0,
|
|
51
|
-
enabled: true,
|
|
52
|
-
rateLimitResetTimes: {},
|
|
53
|
-
consecutiveFailures: 0,
|
|
54
|
-
lastFailureTime: null,
|
|
55
|
-
stats: {
|
|
56
|
-
requests: 0,
|
|
57
|
-
inputTokens: 0,
|
|
58
|
-
outputTokens: 0,
|
|
59
|
-
cacheReadTokens: 0,
|
|
60
|
-
cacheWriteTokens: 0,
|
|
61
|
-
lastReset: Date.now(),
|
|
62
|
-
},
|
|
63
|
-
source: "cc-keychain",
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const legacyAccount: ManagedAccount = {
|
|
67
|
-
id: "legacy-1",
|
|
68
|
-
index: 2,
|
|
69
|
-
email: undefined,
|
|
70
|
-
refreshToken: "rt_legacy_789",
|
|
71
|
-
access: "at_legacy_789",
|
|
72
|
-
expires: Date.now() + 3600000,
|
|
73
|
-
tokenUpdatedAt: Date.now(),
|
|
74
|
-
addedAt: Date.now(),
|
|
75
|
-
lastUsed: 0,
|
|
76
|
-
enabled: true,
|
|
77
|
-
rateLimitResetTimes: {},
|
|
78
|
-
consecutiveFailures: 0,
|
|
79
|
-
lastFailureTime: null,
|
|
80
|
-
stats: {
|
|
81
|
-
requests: 0,
|
|
82
|
-
inputTokens: 0,
|
|
83
|
-
outputTokens: 0,
|
|
84
|
-
cacheReadTokens: 0,
|
|
85
|
-
cacheWriteTokens: 0,
|
|
86
|
-
lastReset: Date.now(),
|
|
87
|
-
},
|
|
88
|
-
source: undefined,
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const ccCredential: CCCredential = {
|
|
92
|
-
accessToken: "at_cc_456",
|
|
93
|
-
refreshToken: "rt_cc_456",
|
|
94
|
-
expiresAt: Date.now() + 3600000,
|
|
95
|
-
source: "cc-keychain",
|
|
96
|
-
label: "Claude Code-credentials:alice@example.com",
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
describe("resolveIdentity", () => {
|
|
100
|
-
it("should resolve OAuth account identity from email", () => {
|
|
101
|
-
const identity = resolveIdentity(oauthAccount);
|
|
102
|
-
|
|
103
|
-
expect(identity).toEqual({
|
|
104
|
-
kind: "oauth",
|
|
15
|
+
const oauthAccount: ManagedAccount = {
|
|
16
|
+
id: "oauth-1",
|
|
17
|
+
index: 0,
|
|
105
18
|
email: "alice@example.com",
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
19
|
+
refreshToken: "rt_oauth_123",
|
|
20
|
+
access: "at_oauth_123",
|
|
21
|
+
expires: Date.now() + 3600000,
|
|
22
|
+
tokenUpdatedAt: Date.now(),
|
|
23
|
+
addedAt: Date.now(),
|
|
24
|
+
lastUsed: 0,
|
|
25
|
+
enabled: true,
|
|
26
|
+
rateLimitResetTimes: {},
|
|
27
|
+
consecutiveFailures: 0,
|
|
28
|
+
lastFailureTime: null,
|
|
29
|
+
stats: {
|
|
30
|
+
requests: 0,
|
|
31
|
+
inputTokens: 0,
|
|
32
|
+
outputTokens: 0,
|
|
33
|
+
cacheReadTokens: 0,
|
|
34
|
+
cacheWriteTokens: 0,
|
|
35
|
+
lastReset: Date.now(),
|
|
36
|
+
},
|
|
37
|
+
source: "oauth",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const ccAccount: ManagedAccount = {
|
|
41
|
+
id: "cc-1",
|
|
42
|
+
index: 1,
|
|
43
|
+
email: undefined,
|
|
115
44
|
label: "Claude Code-credentials:alice@example.com",
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
45
|
+
refreshToken: "rt_cc_456",
|
|
46
|
+
access: "at_cc_456",
|
|
47
|
+
expires: Date.now() + 3600000,
|
|
48
|
+
tokenUpdatedAt: Date.now(),
|
|
49
|
+
addedAt: Date.now(),
|
|
50
|
+
lastUsed: 0,
|
|
51
|
+
enabled: true,
|
|
52
|
+
rateLimitResetTimes: {},
|
|
53
|
+
consecutiveFailures: 0,
|
|
54
|
+
lastFailureTime: null,
|
|
55
|
+
stats: {
|
|
56
|
+
requests: 0,
|
|
57
|
+
inputTokens: 0,
|
|
58
|
+
outputTokens: 0,
|
|
59
|
+
cacheReadTokens: 0,
|
|
60
|
+
cacheWriteTokens: 0,
|
|
61
|
+
lastReset: Date.now(),
|
|
62
|
+
},
|
|
63
|
+
source: "cc-keychain",
|
|
64
|
+
};
|
|
121
65
|
|
|
122
|
-
|
|
123
|
-
|
|
66
|
+
const legacyAccount: ManagedAccount = {
|
|
67
|
+
id: "legacy-1",
|
|
68
|
+
index: 2,
|
|
69
|
+
email: undefined,
|
|
124
70
|
refreshToken: "rt_legacy_789",
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
71
|
+
access: "at_legacy_789",
|
|
72
|
+
expires: Date.now() + 3600000,
|
|
73
|
+
tokenUpdatedAt: Date.now(),
|
|
74
|
+
addedAt: Date.now(),
|
|
75
|
+
lastUsed: 0,
|
|
76
|
+
enabled: true,
|
|
77
|
+
rateLimitResetTimes: {},
|
|
78
|
+
consecutiveFailures: 0,
|
|
79
|
+
lastFailureTime: null,
|
|
80
|
+
stats: {
|
|
81
|
+
requests: 0,
|
|
82
|
+
inputTokens: 0,
|
|
83
|
+
outputTokens: 0,
|
|
84
|
+
cacheReadTokens: 0,
|
|
85
|
+
cacheWriteTokens: 0,
|
|
86
|
+
lastReset: Date.now(),
|
|
87
|
+
},
|
|
88
|
+
source: undefined,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ccCredential: CCCredential = {
|
|
92
|
+
accessToken: "at_cc_456",
|
|
93
|
+
refreshToken: "rt_cc_456",
|
|
94
|
+
expiresAt: Date.now() + 3600000,
|
|
133
95
|
source: "cc-keychain",
|
|
134
96
|
label: "Claude Code-credentials:alice@example.com",
|
|
135
|
-
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
describe("resolveIdentity", () => {
|
|
100
|
+
it("should resolve OAuth account identity from email", () => {
|
|
101
|
+
const identity = resolveIdentity(oauthAccount);
|
|
102
|
+
|
|
103
|
+
expect(identity).toEqual({
|
|
104
|
+
kind: "oauth",
|
|
105
|
+
email: "alice@example.com",
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("should resolve CC account identity from source+label", () => {
|
|
110
|
+
const identity = resolveIdentity(ccAccount);
|
|
111
|
+
|
|
112
|
+
expect(identity).toEqual({
|
|
113
|
+
kind: "cc",
|
|
114
|
+
source: "cc-keychain",
|
|
115
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should resolve legacy identity from refreshToken when no email or source", () => {
|
|
120
|
+
const identity = resolveIdentity(legacyAccount);
|
|
121
|
+
|
|
122
|
+
expect(identity).toEqual({
|
|
123
|
+
kind: "legacy",
|
|
124
|
+
refreshToken: "rt_legacy_789",
|
|
125
|
+
});
|
|
126
|
+
});
|
|
136
127
|
});
|
|
137
128
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
129
|
+
describe("exchange helpers", () => {
|
|
130
|
+
it("should resolve CC identity from a Claude Code credential", () => {
|
|
131
|
+
expect(resolveIdentityFromCCCredential(ccCredential)).toEqual({
|
|
132
|
+
kind: "cc",
|
|
133
|
+
source: "cc-keychain",
|
|
134
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("should resolve OAuth exchange results with email as OAuth identity", () => {
|
|
139
|
+
expect(resolveIdentityFromOAuthExchange({ email: "alice@example.com", refresh: "rt_oauth_123" })).toEqual({
|
|
140
|
+
kind: "oauth",
|
|
141
|
+
email: "alice@example.com",
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should resolve OAuth exchange results without email as legacy identity", () => {
|
|
146
|
+
expect(resolveIdentityFromOAuthExchange({ refresh: "rt_legacy_789" })).toEqual({
|
|
147
|
+
kind: "legacy",
|
|
148
|
+
refreshToken: "rt_legacy_789",
|
|
149
|
+
});
|
|
150
|
+
});
|
|
143
151
|
});
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
{ kind: "cc", source: "cc-keychain", label: "label1" },
|
|
185
|
-
{ kind: "cc", source: "cc-keychain", label: "label2" },
|
|
186
|
-
),
|
|
187
|
-
).toBe(false);
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe("findByIdentity", () => {
|
|
192
|
-
const accounts: ManagedAccount[] = [oauthAccount, ccAccount, legacyAccount];
|
|
193
|
-
|
|
194
|
-
it("should find matching accounts by stable identity", () => {
|
|
195
|
-
expect(findByIdentity(accounts, { kind: "oauth", email: "alice@example.com" })).toBe(oauthAccount);
|
|
196
|
-
expect(
|
|
197
|
-
findByIdentity(accounts, {
|
|
198
|
-
kind: "cc",
|
|
199
|
-
source: "cc-keychain",
|
|
200
|
-
label: "Claude Code-credentials:alice@example.com",
|
|
201
|
-
}),
|
|
202
|
-
).toBe(ccAccount);
|
|
203
|
-
expect(findByIdentity(accounts, { kind: "legacy", refreshToken: "rt_legacy_789" })).toBe(legacyAccount);
|
|
204
|
-
expect(findByIdentity(accounts, { kind: "oauth", email: "unknown@example.com" })).toBeNull();
|
|
153
|
+
describe("identitiesMatch", () => {
|
|
154
|
+
it("should match OAuth accounts with same email", () => {
|
|
155
|
+
const identity1: AccountIdentity = { kind: "oauth", email: "alice@example.com" };
|
|
156
|
+
const identity2: AccountIdentity = { kind: "oauth", email: "alice@example.com" };
|
|
157
|
+
|
|
158
|
+
expect(identitiesMatch(identity1, identity2)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("should match CC accounts with same source+label", () => {
|
|
162
|
+
const identity1: AccountIdentity = { kind: "cc", source: "cc-keychain", label: "label1" };
|
|
163
|
+
const identity2: AccountIdentity = { kind: "cc", source: "cc-keychain", label: "label1" };
|
|
164
|
+
|
|
165
|
+
expect(identitiesMatch(identity1, identity2)).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("should not match different identity types", () => {
|
|
169
|
+
const oauthIdentity: AccountIdentity = { kind: "oauth", email: "alice@example.com" };
|
|
170
|
+
const ccIdentity: AccountIdentity = { kind: "cc", source: "cc-keychain", label: "label" };
|
|
171
|
+
const legacyIdentity: AccountIdentity = { kind: "legacy", refreshToken: "rt" };
|
|
172
|
+
|
|
173
|
+
expect(identitiesMatch(oauthIdentity, ccIdentity)).toBe(false);
|
|
174
|
+
expect(identitiesMatch(oauthIdentity, legacyIdentity)).toBe(false);
|
|
175
|
+
expect(identitiesMatch(ccIdentity, legacyIdentity)).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should not match different stable key fields", () => {
|
|
179
|
+
expect(
|
|
180
|
+
identitiesMatch(
|
|
181
|
+
{ kind: "oauth", email: "alice@example.com" },
|
|
182
|
+
{ kind: "oauth", email: "bob@example.com" },
|
|
183
|
+
),
|
|
184
|
+
).toBe(false);
|
|
185
|
+
expect(
|
|
186
|
+
identitiesMatch(
|
|
187
|
+
{ kind: "cc", source: "cc-keychain", label: "label1" },
|
|
188
|
+
{ kind: "cc", source: "cc-keychain", label: "label2" },
|
|
189
|
+
),
|
|
190
|
+
).toBe(false);
|
|
191
|
+
});
|
|
205
192
|
});
|
|
206
193
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
194
|
+
describe("findByIdentity", () => {
|
|
195
|
+
const accounts: ManagedAccount[] = [oauthAccount, ccAccount, legacyAccount];
|
|
196
|
+
|
|
197
|
+
it("should find matching accounts by stable identity", () => {
|
|
198
|
+
expect(findByIdentity(accounts, { kind: "oauth", email: "alice@example.com" })).toBe(oauthAccount);
|
|
199
|
+
expect(
|
|
200
|
+
findByIdentity(accounts, {
|
|
201
|
+
kind: "cc",
|
|
202
|
+
source: "cc-keychain",
|
|
203
|
+
label: "Claude Code-credentials:alice@example.com",
|
|
204
|
+
}),
|
|
205
|
+
).toBe(ccAccount);
|
|
206
|
+
expect(findByIdentity(accounts, { kind: "legacy", refreshToken: "rt_legacy_789" })).toBe(legacyAccount);
|
|
207
|
+
expect(findByIdentity(accounts, { kind: "oauth", email: "unknown@example.com" })).toBeNull();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("should serialize identities without leaking secrets", () => {
|
|
211
|
+
expect(serializeIdentity({ kind: "oauth", email: "alice@example.com" })).toBe("oauth:alice@example.com");
|
|
212
|
+
expect(serializeIdentity({ kind: "cc", source: "cc-keychain", label: "label1" })).toBe(
|
|
213
|
+
"cc:cc-keychain:label1",
|
|
214
|
+
);
|
|
215
|
+
expect(serializeIdentity({ kind: "legacy", refreshToken: "secret-refresh-token" })).toBe("legacy:redacted");
|
|
216
|
+
});
|
|
211
217
|
});
|
|
212
|
-
});
|
|
213
218
|
});
|
package/src/account-identity.ts
CHANGED
|
@@ -5,104 +5,106 @@ import type { AccountMetadata } from "./storage.js";
|
|
|
5
5
|
type CCAccountSource = "cc-keychain" | "cc-file";
|
|
6
6
|
|
|
7
7
|
export type AccountIdentity =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
| { kind: "oauth"; email: string }
|
|
9
|
+
| { kind: "cc"; source: CCAccountSource; label: string }
|
|
10
|
+
| { kind: "legacy"; refreshToken: string };
|
|
11
11
|
|
|
12
12
|
type IdentityAccount = ManagedAccount | AccountMetadata;
|
|
13
13
|
|
|
14
14
|
function isCCAccountSource(source: IdentityAccount["source"]): source is CCAccountSource {
|
|
15
|
-
|
|
15
|
+
return source === "cc-keychain" || source === "cc-file";
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
function isAccountIdentity(value: unknown): value is AccountIdentity {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
19
|
+
if (!value || typeof value !== "object") return false;
|
|
20
|
+
|
|
21
|
+
const candidate = value as Record<string, unknown>;
|
|
22
|
+
switch (candidate.kind) {
|
|
23
|
+
case "oauth":
|
|
24
|
+
return typeof candidate.email === "string" && candidate.email.length > 0;
|
|
25
|
+
case "cc":
|
|
26
|
+
return (
|
|
27
|
+
isCCAccountSource(candidate.source as IdentityAccount["source"]) && typeof candidate.label === "string"
|
|
28
|
+
);
|
|
29
|
+
case "legacy":
|
|
30
|
+
return typeof candidate.refreshToken === "string" && candidate.refreshToken.length > 0;
|
|
31
|
+
default:
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function resolveIdentity(account: IdentityAccount): AccountIdentity {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
if (isAccountIdentity(account.identity)) {
|
|
38
|
+
return account.identity;
|
|
39
|
+
}
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
if (account.source === "oauth" && account.email) {
|
|
42
|
+
return { kind: "oauth", email: account.email };
|
|
43
|
+
}
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
if (isCCAccountSource(account.source) && account.label) {
|
|
46
|
+
return { kind: "cc", source: account.source, label: account.label };
|
|
47
|
+
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
return { kind: "legacy", refreshToken: account.refreshToken };
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
export function resolveIdentityFromCCCredential(cred: CCCredential): AccountIdentity {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
return {
|
|
54
|
+
kind: "cc",
|
|
55
|
+
source: cred.source,
|
|
56
|
+
label: cred.label,
|
|
57
|
+
};
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function resolveIdentityFromOAuthExchange(result: { email?: string; refresh: string }): AccountIdentity {
|
|
59
|
-
|
|
61
|
+
if (result.email) {
|
|
62
|
+
return {
|
|
63
|
+
kind: "oauth",
|
|
64
|
+
email: result.email,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
60
68
|
return {
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
kind: "legacy",
|
|
70
|
+
refreshToken: result.refresh,
|
|
63
71
|
};
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
kind: "legacy",
|
|
68
|
-
refreshToken: result.refresh,
|
|
69
|
-
};
|
|
70
72
|
}
|
|
71
73
|
|
|
72
74
|
export function identitiesMatch(a: AccountIdentity, b: AccountIdentity): boolean {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
if (a.kind !== b.kind) return false;
|
|
76
|
+
|
|
77
|
+
switch (a.kind) {
|
|
78
|
+
case "oauth": {
|
|
79
|
+
return a.email === (b as Extract<AccountIdentity, { kind: "oauth" }>).email;
|
|
80
|
+
}
|
|
81
|
+
case "cc": {
|
|
82
|
+
const ccIdentity = b as Extract<AccountIdentity, { kind: "cc" }>;
|
|
83
|
+
return a.source === ccIdentity.source && a.label === ccIdentity.label;
|
|
84
|
+
}
|
|
85
|
+
case "legacy": {
|
|
86
|
+
return a.refreshToken === (b as Extract<AccountIdentity, { kind: "legacy" }>).refreshToken;
|
|
87
|
+
}
|
|
78
88
|
}
|
|
79
|
-
case "cc": {
|
|
80
|
-
const ccIdentity = b as Extract<AccountIdentity, { kind: "cc" }>;
|
|
81
|
-
return a.source === ccIdentity.source && a.label === ccIdentity.label;
|
|
82
|
-
}
|
|
83
|
-
case "legacy": {
|
|
84
|
-
return a.refreshToken === (b as Extract<AccountIdentity, { kind: "legacy" }>).refreshToken;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
export function findByIdentity<T extends IdentityAccount>(accounts: T[], id: AccountIdentity): T | null {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
for (const account of accounts) {
|
|
93
|
+
if (identitiesMatch(resolveIdentity(account), id)) {
|
|
94
|
+
return account;
|
|
95
|
+
}
|
|
93
96
|
}
|
|
94
|
-
}
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
return null;
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
export function serializeIdentity(id: AccountIdentity): string {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
switch (id.kind) {
|
|
103
|
+
case "oauth":
|
|
104
|
+
return `oauth:${id.email}`;
|
|
105
|
+
case "cc":
|
|
106
|
+
return `cc:${id.source}:${id.label}`;
|
|
107
|
+
case "legacy":
|
|
108
|
+
return "legacy:redacted";
|
|
109
|
+
}
|
|
108
110
|
}
|