@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,30 +4,30 @@ import { createDeferred, nextTick } from "./__tests__/helpers/deferred.js";
|
|
|
4
4
|
import { createRefreshHelpers } from "./refresh-helpers.js";
|
|
5
5
|
|
|
6
6
|
vi.mock("node:child_process", () => ({
|
|
7
|
-
|
|
7
|
+
execSync: vi.fn(),
|
|
8
8
|
}));
|
|
9
9
|
|
|
10
10
|
vi.mock("./cc-credentials.js", () => ({
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
readCCCredentials: vi.fn(),
|
|
12
|
+
readCCCredentialsFromFile: vi.fn(),
|
|
13
13
|
}));
|
|
14
14
|
|
|
15
15
|
vi.mock("./oauth.js", () => ({
|
|
16
|
-
|
|
16
|
+
refreshToken: vi.fn(),
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
19
|
vi.mock("./refresh-lock.js", () => ({
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
acquireRefreshLock: vi.fn().mockResolvedValue({
|
|
21
|
+
acquired: true,
|
|
22
|
+
lockPath: null,
|
|
23
|
+
owner: null,
|
|
24
|
+
lockInode: null,
|
|
25
|
+
}),
|
|
26
|
+
releaseRefreshLock: vi.fn().mockResolvedValue(undefined),
|
|
27
27
|
}));
|
|
28
28
|
|
|
29
29
|
vi.mock("./storage.js", () => ({
|
|
30
|
-
|
|
30
|
+
loadAccounts: vi.fn().mockResolvedValue(null),
|
|
31
31
|
}));
|
|
32
32
|
|
|
33
33
|
import type { ManagedAccount } from "./accounts.js";
|
|
@@ -42,276 +42,276 @@ const mockReadCCCredentialsFromFile = readCCCredentialsFromFile as Mock;
|
|
|
42
42
|
const mockRefreshToken = refreshToken as Mock;
|
|
43
43
|
|
|
44
44
|
function makeAccount(overrides: Partial<ManagedAccount> = {}): ManagedAccount {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
45
|
+
return {
|
|
46
|
+
id: "acct-1",
|
|
47
|
+
index: 0,
|
|
48
|
+
email: "user@example.com",
|
|
49
|
+
refreshToken: "refresh-old",
|
|
50
|
+
access: "access-old",
|
|
51
|
+
expires: Date.now() - 60_000,
|
|
52
|
+
tokenUpdatedAt: 0,
|
|
53
|
+
addedAt: Date.now() - 120_000,
|
|
54
|
+
lastUsed: 0,
|
|
55
|
+
enabled: true,
|
|
56
|
+
rateLimitResetTimes: {},
|
|
57
|
+
consecutiveFailures: 0,
|
|
58
|
+
lastFailureTime: null,
|
|
59
|
+
stats: {
|
|
60
|
+
requests: 0,
|
|
61
|
+
inputTokens: 0,
|
|
62
|
+
outputTokens: 0,
|
|
63
|
+
cacheReadTokens: 0,
|
|
64
|
+
cacheWriteTokens: 0,
|
|
65
|
+
lastReset: 0,
|
|
66
|
+
},
|
|
67
|
+
source: "oauth",
|
|
68
|
+
...overrides,
|
|
69
|
+
};
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
function makeCredential(overrides: Partial<CCCredential> = {}): CCCredential {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
return {
|
|
74
|
+
accessToken: "access-fresh",
|
|
75
|
+
refreshToken: "refresh-fresh",
|
|
76
|
+
expiresAt: Date.now() + 3600_000,
|
|
77
|
+
subscriptionType: "max",
|
|
78
|
+
source: "cc-file",
|
|
79
|
+
label: "/mock-home/.claude/.credentials.json",
|
|
80
|
+
...overrides,
|
|
81
|
+
};
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
describe("refreshAccountToken", () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("re-reads keychain-backed CC accounts without calling OAuth HTTP refresh", async () => {
|
|
91
|
-
const account = makeAccount({
|
|
92
|
-
source: "cc-keychain",
|
|
93
|
-
refreshToken: "refresh-old",
|
|
94
|
-
access: "access-old",
|
|
95
|
-
expires: Date.now() - 1_000,
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
vi.resetAllMocks();
|
|
87
|
+
vi.spyOn(Date, "now").mockReturnValue(new Date("2026-03-25T12:00:00Z").getTime());
|
|
96
88
|
});
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
access
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
90
|
+
it("re-reads keychain-backed CC accounts without calling OAuth HTTP refresh", async () => {
|
|
91
|
+
const account = makeAccount({
|
|
92
|
+
source: "cc-keychain",
|
|
93
|
+
refreshToken: "refresh-old",
|
|
94
|
+
access: "access-old",
|
|
95
|
+
expires: Date.now() - 1_000,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
mockReadCCCredentials.mockReturnValue([
|
|
99
|
+
makeCredential({
|
|
100
|
+
source: "cc-keychain",
|
|
101
|
+
label: "Claude Code-credentials",
|
|
102
|
+
refreshToken: "refresh-old",
|
|
103
|
+
}),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const client = {
|
|
107
|
+
auth: {
|
|
108
|
+
set: vi.fn().mockResolvedValue(undefined),
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
await expect(refreshAccountToken(account, client)).resolves.toBe("access-fresh");
|
|
113
|
+
|
|
114
|
+
expect(mockReadCCCredentials).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(mockReadCCCredentialsFromFile).not.toHaveBeenCalled();
|
|
116
|
+
expect(mockRefreshToken).not.toHaveBeenCalled();
|
|
117
|
+
expect(account.access).toBe("access-fresh");
|
|
118
|
+
expect(account.refreshToken).toBe("refresh-old");
|
|
119
|
+
expect(account.expires).toBe(Date.now() + 3600_000);
|
|
120
|
+
expect(client.auth.set).toHaveBeenCalledWith({
|
|
121
|
+
path: { id: "anthropic" },
|
|
122
|
+
body: {
|
|
123
|
+
type: "oauth",
|
|
124
|
+
refresh: "refresh-old",
|
|
125
|
+
access: "access-fresh",
|
|
126
|
+
expires: Date.now() + 3600_000,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
137
129
|
});
|
|
138
130
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
131
|
+
it("invokes the claude CLI for expired file-backed CC accounts before re-reading", async () => {
|
|
132
|
+
const account = makeAccount({
|
|
133
|
+
source: "cc-file",
|
|
134
|
+
refreshToken: "refresh-old",
|
|
135
|
+
access: "access-old",
|
|
136
|
+
expires: Date.now() - 1_000,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
mockReadCCCredentialsFromFile
|
|
140
|
+
.mockReturnValueOnce(
|
|
141
|
+
makeCredential({
|
|
142
|
+
source: "cc-file",
|
|
143
|
+
refreshToken: "refresh-old",
|
|
144
|
+
accessToken: "access-stale",
|
|
145
|
+
expiresAt: Date.now() - 5_000,
|
|
146
|
+
}),
|
|
147
|
+
)
|
|
148
|
+
.mockReturnValueOnce(
|
|
149
|
+
makeCredential({
|
|
150
|
+
source: "cc-file",
|
|
151
|
+
refreshToken: "refresh-new",
|
|
152
|
+
accessToken: "access-new",
|
|
153
|
+
expiresAt: Date.now() + 7_200_000,
|
|
154
|
+
}),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
mockExecSync.mockImplementation((command: string) => {
|
|
158
|
+
if (command === "which claude") return "/usr/local/bin/claude\n";
|
|
159
|
+
if (command === "/usr/local/bin/claude -p . --model haiku") return "";
|
|
160
|
+
throw new Error(`unexpected command: ${command}`);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await expect(refreshAccountToken(account, {})).resolves.toBe("access-new");
|
|
164
|
+
|
|
165
|
+
expect(mockReadCCCredentialsFromFile).toHaveBeenCalledTimes(2);
|
|
166
|
+
expect(mockReadCCCredentials).not.toHaveBeenCalled();
|
|
167
|
+
expect(mockRefreshToken).not.toHaveBeenCalled();
|
|
168
|
+
expect(mockExecSync).toHaveBeenNthCalledWith(1, "which claude", {
|
|
169
|
+
encoding: "utf-8",
|
|
170
|
+
timeout: 5000,
|
|
171
|
+
});
|
|
172
|
+
expect(mockExecSync).toHaveBeenNthCalledWith(2, "/usr/local/bin/claude -p . --model haiku", {
|
|
173
|
+
encoding: "utf-8",
|
|
174
|
+
timeout: 60000,
|
|
175
|
+
});
|
|
176
|
+
expect(account.access).toBe("access-new");
|
|
177
|
+
expect(account.refreshToken).toBe("refresh-new");
|
|
161
178
|
});
|
|
162
179
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
it("keeps OAuth-backed accounts on the existing HTTP refresh path", async () => {
|
|
181
|
+
const account = makeAccount({
|
|
182
|
+
source: "oauth",
|
|
183
|
+
refreshToken: "oauth-refresh",
|
|
184
|
+
access: "oauth-access",
|
|
185
|
+
expires: Date.now() - 1_000,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
mockRefreshToken.mockResolvedValue({
|
|
189
|
+
access_token: "oauth-access-new",
|
|
190
|
+
expires_in: 1800,
|
|
191
|
+
refresh_token: "oauth-refresh-new",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await expect(refreshAccountToken(account, {})).resolves.toBe("oauth-access-new");
|
|
195
|
+
|
|
196
|
+
expect(mockRefreshToken).toHaveBeenCalledWith("oauth-refresh", {
|
|
197
|
+
signal: expect.any(AbortSignal),
|
|
198
|
+
});
|
|
199
|
+
expect(mockReadCCCredentials).not.toHaveBeenCalled();
|
|
200
|
+
expect(mockReadCCCredentialsFromFile).not.toHaveBeenCalled();
|
|
201
|
+
expect(account.access).toBe("oauth-access-new");
|
|
202
|
+
expect(account.refreshToken).toBe("oauth-refresh-new");
|
|
186
203
|
});
|
|
187
204
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
205
|
+
it("fails cleanly when the claude binary is unavailable for CC refresh", async () => {
|
|
206
|
+
const account = makeAccount({
|
|
207
|
+
source: "cc-file",
|
|
208
|
+
refreshToken: "refresh-old",
|
|
209
|
+
expires: Date.now() - 1_000,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
mockReadCCCredentialsFromFile.mockReturnValue(
|
|
213
|
+
makeCredential({
|
|
214
|
+
source: "cc-file",
|
|
215
|
+
refreshToken: "refresh-old",
|
|
216
|
+
accessToken: "access-stale",
|
|
217
|
+
expiresAt: Date.now() - 5_000,
|
|
218
|
+
}),
|
|
219
|
+
);
|
|
220
|
+
mockExecSync.mockImplementation((command: string) => {
|
|
221
|
+
if (command === "which claude") {
|
|
222
|
+
throw new Error("not found");
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`unexpected command: ${command}`);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await expect(refreshAccountToken(account, {})).rejects.toThrow("CC credential refresh failed");
|
|
228
|
+
|
|
229
|
+
expect(mockRefreshToken).not.toHaveBeenCalled();
|
|
230
|
+
expect(mockExecSync).toHaveBeenCalledTimes(1);
|
|
231
|
+
expect(mockExecSync).toHaveBeenCalledWith("which claude", {
|
|
232
|
+
encoding: "utf-8",
|
|
233
|
+
timeout: 5000,
|
|
234
|
+
});
|
|
192
235
|
});
|
|
193
236
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
237
|
+
it("reuses the first foreground retry after an idle refresh rejection", async () => {
|
|
238
|
+
const idleRefresh = createDeferred<{
|
|
239
|
+
access_token: string;
|
|
240
|
+
expires_in: number;
|
|
241
|
+
refresh_token?: string;
|
|
242
|
+
}>();
|
|
243
|
+
const foregroundRefresh = createDeferred<{
|
|
244
|
+
access_token: string;
|
|
245
|
+
expires_in: number;
|
|
246
|
+
refresh_token?: string;
|
|
247
|
+
}>();
|
|
248
|
+
const idleFailure = new Error("idle refresh failed");
|
|
249
|
+
const foregroundFailure = new Error("foreground refresh failed");
|
|
250
|
+
mockRefreshToken
|
|
251
|
+
.mockImplementationOnce(() => idleRefresh.promise)
|
|
252
|
+
.mockImplementationOnce(() => foregroundRefresh.promise)
|
|
253
|
+
.mockRejectedValueOnce(new Error("duplicate foreground refresh"));
|
|
254
|
+
const accountManager = {
|
|
255
|
+
saveToDisk: vi.fn().mockResolvedValue(undefined),
|
|
256
|
+
requestSaveToDisk: vi.fn(),
|
|
257
|
+
getEnabledAccounts: vi.fn().mockReturnValue([]),
|
|
258
|
+
};
|
|
259
|
+
const account = makeAccount();
|
|
260
|
+
const helpers = createRefreshHelpers({
|
|
261
|
+
client: {},
|
|
262
|
+
config: {
|
|
263
|
+
idle_refresh: {
|
|
264
|
+
enabled: true,
|
|
265
|
+
window_minutes: 10,
|
|
266
|
+
min_interval_minutes: 1,
|
|
267
|
+
},
|
|
268
|
+
} as never,
|
|
269
|
+
getAccountManager: () => accountManager as never,
|
|
270
|
+
debugLog: vi.fn(),
|
|
271
|
+
});
|
|
272
|
+
const idleCall = helpers.refreshAccountTokenSingleFlight(account, "idle").catch((error) => error);
|
|
273
|
+
await nextTick();
|
|
274
|
+
await nextTick();
|
|
275
|
+
|
|
276
|
+
const foregroundCallA = helpers.refreshAccountTokenSingleFlight(account, "foreground").catch((error) => error);
|
|
277
|
+
const foregroundCallB = helpers.refreshAccountTokenSingleFlight(account, "foreground").catch((error) => error);
|
|
278
|
+
await nextTick();
|
|
279
|
+
|
|
280
|
+
idleRefresh.reject(idleFailure);
|
|
281
|
+
await expect(idleCall).resolves.toBe(idleFailure);
|
|
282
|
+
await nextTick();
|
|
283
|
+
|
|
284
|
+
expect(mockRefreshToken).toHaveBeenCalledTimes(2);
|
|
285
|
+
|
|
286
|
+
foregroundRefresh.reject(foregroundFailure);
|
|
287
|
+
|
|
288
|
+
await expect(foregroundCallA).resolves.toBe(foregroundFailure);
|
|
289
|
+
await expect(foregroundCallB).resolves.toBe(foregroundFailure);
|
|
210
290
|
});
|
|
211
291
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
292
|
+
it("does not adopt older expired-fallback disk auth when only access differs", () => {
|
|
293
|
+
const currentTime = Date.now();
|
|
294
|
+
const account = makeAccount({
|
|
295
|
+
refreshToken: "refresh-current",
|
|
296
|
+
access: "access-current",
|
|
297
|
+
expires: currentTime - 1_000,
|
|
298
|
+
tokenUpdatedAt: currentTime,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const adopted = applyDiskAuthIfFresher(
|
|
302
|
+
account,
|
|
303
|
+
{
|
|
304
|
+
refreshToken: "refresh-current",
|
|
305
|
+
access: "access-stale",
|
|
306
|
+
expires: currentTime + 60_000,
|
|
307
|
+
tokenUpdatedAt: currentTime - 60_000,
|
|
308
|
+
},
|
|
309
|
+
{ allowExpiredFallback: true },
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
expect(adopted).toBe(false);
|
|
313
|
+
expect(account.refreshToken).toBe("refresh-current");
|
|
314
|
+
expect(account.access).toBe("access-current");
|
|
315
|
+
expect(account.tokenUpdatedAt).toBe(currentTime);
|
|
234
316
|
});
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it("reuses the first foreground retry after an idle refresh rejection", async () => {
|
|
238
|
-
const idleRefresh = createDeferred<{
|
|
239
|
-
access_token: string;
|
|
240
|
-
expires_in: number;
|
|
241
|
-
refresh_token?: string;
|
|
242
|
-
}>();
|
|
243
|
-
const foregroundRefresh = createDeferred<{
|
|
244
|
-
access_token: string;
|
|
245
|
-
expires_in: number;
|
|
246
|
-
refresh_token?: string;
|
|
247
|
-
}>();
|
|
248
|
-
const idleFailure = new Error("idle refresh failed");
|
|
249
|
-
const foregroundFailure = new Error("foreground refresh failed");
|
|
250
|
-
mockRefreshToken
|
|
251
|
-
.mockImplementationOnce(() => idleRefresh.promise)
|
|
252
|
-
.mockImplementationOnce(() => foregroundRefresh.promise)
|
|
253
|
-
.mockRejectedValueOnce(new Error("duplicate foreground refresh"));
|
|
254
|
-
const accountManager = {
|
|
255
|
-
saveToDisk: vi.fn().mockResolvedValue(undefined),
|
|
256
|
-
requestSaveToDisk: vi.fn(),
|
|
257
|
-
getEnabledAccounts: vi.fn().mockReturnValue([]),
|
|
258
|
-
};
|
|
259
|
-
const account = makeAccount();
|
|
260
|
-
const helpers = createRefreshHelpers({
|
|
261
|
-
client: {},
|
|
262
|
-
config: {
|
|
263
|
-
idle_refresh: {
|
|
264
|
-
enabled: true,
|
|
265
|
-
window_minutes: 10,
|
|
266
|
-
min_interval_minutes: 1,
|
|
267
|
-
},
|
|
268
|
-
} as never,
|
|
269
|
-
getAccountManager: () => accountManager as never,
|
|
270
|
-
debugLog: vi.fn(),
|
|
271
|
-
});
|
|
272
|
-
const idleCall = helpers.refreshAccountTokenSingleFlight(account, "idle").catch((error) => error);
|
|
273
|
-
await nextTick();
|
|
274
|
-
await nextTick();
|
|
275
|
-
|
|
276
|
-
const foregroundCallA = helpers.refreshAccountTokenSingleFlight(account, "foreground").catch((error) => error);
|
|
277
|
-
const foregroundCallB = helpers.refreshAccountTokenSingleFlight(account, "foreground").catch((error) => error);
|
|
278
|
-
await nextTick();
|
|
279
|
-
|
|
280
|
-
idleRefresh.reject(idleFailure);
|
|
281
|
-
await expect(idleCall).resolves.toBe(idleFailure);
|
|
282
|
-
await nextTick();
|
|
283
|
-
|
|
284
|
-
expect(mockRefreshToken).toHaveBeenCalledTimes(2);
|
|
285
|
-
|
|
286
|
-
foregroundRefresh.reject(foregroundFailure);
|
|
287
|
-
|
|
288
|
-
await expect(foregroundCallA).resolves.toBe(foregroundFailure);
|
|
289
|
-
await expect(foregroundCallB).resolves.toBe(foregroundFailure);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it("does not adopt older expired-fallback disk auth when only access differs", () => {
|
|
293
|
-
const currentTime = Date.now();
|
|
294
|
-
const account = makeAccount({
|
|
295
|
-
refreshToken: "refresh-current",
|
|
296
|
-
access: "access-current",
|
|
297
|
-
expires: currentTime - 1_000,
|
|
298
|
-
tokenUpdatedAt: currentTime,
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
const adopted = applyDiskAuthIfFresher(
|
|
302
|
-
account,
|
|
303
|
-
{
|
|
304
|
-
refreshToken: "refresh-current",
|
|
305
|
-
access: "access-stale",
|
|
306
|
-
expires: currentTime + 60_000,
|
|
307
|
-
tokenUpdatedAt: currentTime - 60_000,
|
|
308
|
-
},
|
|
309
|
-
{ allowExpiredFallback: true },
|
|
310
|
-
);
|
|
311
|
-
|
|
312
|
-
expect(adopted).toBe(false);
|
|
313
|
-
expect(account.refreshToken).toBe("refresh-current");
|
|
314
|
-
expect(account.access).toBe("access-current");
|
|
315
|
-
expect(account.tokenUpdatedAt).toBe(currentTime);
|
|
316
|
-
});
|
|
317
317
|
});
|