@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.
Files changed (107) hide show
  1. package/README.md +88 -88
  2. package/dist/opencode-anthropic-auth-cli.mjs +804 -507
  3. package/dist/opencode-anthropic-auth-plugin.js +4751 -4109
  4. package/package.json +67 -59
  5. package/src/__tests__/billing-edge-cases.test.ts +59 -59
  6. package/src/__tests__/bun-proxy.parallel.test.ts +388 -382
  7. package/src/__tests__/cc-comparison.test.ts +87 -87
  8. package/src/__tests__/cc-credentials.test.ts +254 -250
  9. package/src/__tests__/cch-drift-checker.test.ts +51 -51
  10. package/src/__tests__/cch-native-style.test.ts +56 -56
  11. package/src/__tests__/debug-gating.test.ts +42 -42
  12. package/src/__tests__/decomposition-smoke.test.ts +68 -68
  13. package/src/__tests__/fingerprint-regression.test.ts +575 -566
  14. package/src/__tests__/helpers/conversation-history.smoke.test.ts +271 -271
  15. package/src/__tests__/helpers/conversation-history.ts +119 -119
  16. package/src/__tests__/helpers/deferred.smoke.test.ts +103 -103
  17. package/src/__tests__/helpers/deferred.ts +69 -69
  18. package/src/__tests__/helpers/in-memory-storage.smoke.test.ts +155 -155
  19. package/src/__tests__/helpers/in-memory-storage.ts +88 -88
  20. package/src/__tests__/helpers/mock-bun-proxy.smoke.test.ts +68 -68
  21. package/src/__tests__/helpers/mock-bun-proxy.ts +189 -189
  22. package/src/__tests__/helpers/plugin-fetch-harness.smoke.test.ts +273 -273
  23. package/src/__tests__/helpers/plugin-fetch-harness.ts +288 -288
  24. package/src/__tests__/helpers/sse.smoke.test.ts +236 -236
  25. package/src/__tests__/helpers/sse.ts +209 -209
  26. package/src/__tests__/index.parallel.test.ts +605 -595
  27. package/src/__tests__/sanitization-regex.test.ts +112 -112
  28. package/src/__tests__/state-bounds.test.ts +90 -90
  29. package/src/account-identity.test.ts +197 -192
  30. package/src/account-identity.ts +69 -67
  31. package/src/account-state.test.ts +86 -86
  32. package/src/account-state.ts +25 -25
  33. package/src/accounts/matching.test.ts +335 -0
  34. package/src/accounts/matching.ts +167 -0
  35. package/src/accounts/persistence.test.ts +345 -0
  36. package/src/accounts/persistence.ts +432 -0
  37. package/src/accounts/repair.test.ts +276 -0
  38. package/src/accounts/repair.ts +407 -0
  39. package/src/accounts.dedup.test.ts +621 -621
  40. package/src/accounts.test.ts +933 -929
  41. package/src/accounts.ts +633 -989
  42. package/src/backoff.test.ts +345 -345
  43. package/src/backoff.ts +219 -219
  44. package/src/betas.ts +124 -124
  45. package/src/bun-fetch.test.ts +345 -342
  46. package/src/bun-fetch.ts +424 -424
  47. package/src/bun-proxy.test.ts +25 -25
  48. package/src/bun-proxy.ts +209 -209
  49. package/src/cc-credentials.ts +111 -111
  50. package/src/circuit-breaker.test.ts +184 -184
  51. package/src/circuit-breaker.ts +169 -169
  52. package/src/cli/commands/auth.ts +963 -0
  53. package/src/cli/commands/config.ts +547 -0
  54. package/src/cli/formatting.test.ts +406 -0
  55. package/src/cli/formatting.ts +219 -0
  56. package/src/cli.ts +255 -2022
  57. package/src/commands/handlers/betas.ts +100 -0
  58. package/src/commands/handlers/config.ts +99 -0
  59. package/src/commands/handlers/files.ts +375 -0
  60. package/src/commands/oauth-flow.ts +181 -166
  61. package/src/commands/prompts.ts +61 -61
  62. package/src/commands/router.test.ts +421 -0
  63. package/src/commands/router.ts +143 -635
  64. package/src/config.test.ts +482 -482
  65. package/src/config.ts +412 -404
  66. package/src/constants.ts +48 -48
  67. package/src/drift/cch-constants.ts +95 -95
  68. package/src/env.ts +111 -105
  69. package/src/headers/billing.ts +33 -33
  70. package/src/headers/builder.ts +130 -130
  71. package/src/headers/cch.ts +75 -75
  72. package/src/headers/stainless.ts +25 -25
  73. package/src/headers/user-agent.ts +23 -23
  74. package/src/index.ts +436 -828
  75. package/src/models.ts +27 -27
  76. package/src/oauth.test.ts +102 -102
  77. package/src/oauth.ts +178 -178
  78. package/src/parent-pid-watcher.test.ts +148 -148
  79. package/src/parent-pid-watcher.ts +69 -69
  80. package/src/plugin-helpers.ts +82 -82
  81. package/src/refresh-helpers.ts +145 -139
  82. package/src/refresh-lock.test.ts +94 -94
  83. package/src/refresh-lock.ts +93 -93
  84. package/src/request/body.history.test.ts +579 -571
  85. package/src/request/body.ts +255 -255
  86. package/src/request/metadata.ts +65 -65
  87. package/src/request/retry.test.ts +156 -156
  88. package/src/request/retry.ts +67 -67
  89. package/src/request/url.ts +21 -21
  90. package/src/request-orchestration-helpers.ts +648 -0
  91. package/src/response/index.ts +5 -5
  92. package/src/response/mcp.ts +58 -58
  93. package/src/response/streaming.test.ts +313 -311
  94. package/src/response/streaming.ts +412 -410
  95. package/src/rotation.test.ts +304 -301
  96. package/src/rotation.ts +205 -205
  97. package/src/storage.test.ts +547 -547
  98. package/src/storage.ts +315 -291
  99. package/src/system-prompt/builder.ts +38 -38
  100. package/src/system-prompt/index.ts +5 -5
  101. package/src/system-prompt/normalize.ts +60 -60
  102. package/src/system-prompt/sanitize.ts +30 -30
  103. package/src/thinking.ts +21 -20
  104. package/src/token-refresh.test.ts +265 -265
  105. package/src/token-refresh.ts +219 -214
  106. package/src/types.ts +30 -30
  107. package/dist/bun-proxy.mjs +0 -291
@@ -12,101 +12,101 @@ import type { OpenCodeClient } from "./token-refresh.js";
12
12
  const DEBOUNCE_TOAST_MAP_MAX_SIZE = 50;
13
13
 
14
14
  export interface PluginHelperDeps {
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin client API boundary; accepts arbitrary extension methods
16
- client: OpenCodeClient & Record<string, any>;
17
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin config accepts forward-compatible arbitrary keys
18
- config: AnthropicAuthConfig & Record<string, any>;
19
- debugLog: (...args: unknown[]) => void;
20
- getAccountManager: () => AccountManager | null;
21
- setAccountManager: (accountManager: AccountManager | null) => void;
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- OpenCode plugin client API boundary; accepts arbitrary extension methods
16
+ client: OpenCodeClient & Record<string, any>;
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin config accepts forward-compatible arbitrary keys
18
+ config: AnthropicAuthConfig & Record<string, any>;
19
+ debugLog: (...args: unknown[]) => void;
20
+ getAccountManager: () => AccountManager | null;
21
+ setAccountManager: (accountManager: AccountManager | null) => void;
22
22
  }
23
23
 
24
24
  export function createPluginHelpers({
25
- client,
26
- config,
27
- debugLog,
28
- getAccountManager,
29
- setAccountManager,
25
+ client,
26
+ config,
27
+ debugLog,
28
+ getAccountManager,
29
+ setAccountManager,
30
30
  }: PluginHelperDeps) {
31
- const debouncedToastTimestamps = new Map<string, number>();
31
+ const debouncedToastTimestamps = new Map<string, number>();
32
32
 
33
- async function sendCommandMessage(sessionID: string, text: string) {
34
- await client.session?.prompt({
35
- path: { id: sessionID },
36
- body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
37
- });
38
- }
39
-
40
- async function reloadAccountManagerFromDisk() {
41
- if (!getAccountManager()) return;
42
- setAccountManager(await AccountManager.load(config, null));
43
- }
33
+ async function sendCommandMessage(sessionID: string, text: string) {
34
+ await client.session?.prompt({
35
+ path: { id: sessionID },
36
+ body: { noReply: true, parts: [{ type: "text", text, ignored: true }] },
37
+ });
38
+ }
44
39
 
45
- async function persistOpenCodeAuth(refresh: string, access: string | undefined, expires: number | undefined) {
46
- await client.auth?.set({
47
- path: { id: "anthropic" },
48
- body: { type: "oauth", refresh, access, expires },
49
- });
50
- }
40
+ async function reloadAccountManagerFromDisk() {
41
+ if (!getAccountManager()) return;
42
+ setAccountManager(await AccountManager.load(config, null));
43
+ }
51
44
 
52
- async function runCliCommand(argv: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
53
- const logs: string[] = [];
54
- const errors: string[] = [];
55
- let code = 1;
56
- try {
57
- const { main: cliMain } = await import("./cli.js");
58
- code = await cliMain(argv, {
59
- io: {
60
- log: (...args: unknown[]) => logs.push(args.map(String).join(" ")),
61
- error: (...args: unknown[]) => errors.push(args.map(String).join(" ")),
62
- },
63
- });
64
- } catch (err) {
65
- errors.push(err instanceof Error ? err.message : String(err));
45
+ async function persistOpenCodeAuth(refresh: string, access: string | undefined, expires: number | undefined) {
46
+ await client.auth?.set({
47
+ path: { id: "anthropic" },
48
+ body: { type: "oauth", refresh, access, expires },
49
+ });
66
50
  }
67
- return {
68
- code,
69
- stdout: stripAnsi(logs.join("\n")).trim(),
70
- stderr: stripAnsi(errors.join("\n")).trim(),
71
- };
72
- }
73
51
 
74
- async function toast(
75
- message: string,
76
- variant: "info" | "success" | "warning" | "error" = "info",
77
- options: { debounceKey?: string } = {},
78
- ) {
79
- if (config.toasts.quiet && variant !== "error") return;
80
- if (variant !== "error" && options.debounceKey) {
81
- const minGapMs = Math.max(0, config.toasts.debounce_seconds) * 1000;
82
- if (minGapMs > 0) {
83
- const now = Date.now();
84
- const lastAt = debouncedToastTimestamps.get(options.debounceKey) ?? 0;
85
- if (now - lastAt < minGapMs) return;
86
- if (
87
- !debouncedToastTimestamps.has(options.debounceKey) &&
88
- debouncedToastTimestamps.size >= DEBOUNCE_TOAST_MAP_MAX_SIZE
89
- ) {
90
- const oldestKey = debouncedToastTimestamps.keys().next().value;
91
- if (oldestKey !== undefined) debouncedToastTimestamps.delete(oldestKey);
52
+ async function runCliCommand(argv: string[]): Promise<{ code: number; stdout: string; stderr: string }> {
53
+ const logs: string[] = [];
54
+ const errors: string[] = [];
55
+ let code = 1;
56
+ try {
57
+ const { main: cliMain } = await import("./cli.js");
58
+ code = await cliMain(argv, {
59
+ io: {
60
+ log: (...args: unknown[]) => logs.push(args.map(String).join(" ")),
61
+ error: (...args: unknown[]) => errors.push(args.map(String).join(" ")),
62
+ },
63
+ });
64
+ } catch (err) {
65
+ errors.push(err instanceof Error ? err.message : String(err));
92
66
  }
93
- debouncedToastTimestamps.set(options.debounceKey, now);
94
- }
67
+ return {
68
+ code,
69
+ stdout: stripAnsi(logs.join("\n")).trim(),
70
+ stderr: stripAnsi(errors.join("\n")).trim(),
71
+ };
95
72
  }
96
- try {
97
- await client.tui?.showToast({ body: { message, variant } });
98
- } catch (err) {
99
- if (!(err instanceof TypeError)) debugLog("toast failed:", err);
73
+
74
+ async function toast(
75
+ message: string,
76
+ variant: "info" | "success" | "warning" | "error" = "info",
77
+ options: { debounceKey?: string } = {},
78
+ ) {
79
+ if (config.toasts.quiet && variant !== "error") return;
80
+ if (variant !== "error" && options.debounceKey) {
81
+ const minGapMs = Math.max(0, config.toasts.debounce_seconds) * 1000;
82
+ if (minGapMs > 0) {
83
+ const now = Date.now();
84
+ const lastAt = debouncedToastTimestamps.get(options.debounceKey) ?? 0;
85
+ if (now - lastAt < minGapMs) return;
86
+ if (
87
+ !debouncedToastTimestamps.has(options.debounceKey) &&
88
+ debouncedToastTimestamps.size >= DEBOUNCE_TOAST_MAP_MAX_SIZE
89
+ ) {
90
+ const oldestKey = debouncedToastTimestamps.keys().next().value;
91
+ if (oldestKey !== undefined) debouncedToastTimestamps.delete(oldestKey);
92
+ }
93
+ debouncedToastTimestamps.set(options.debounceKey, now);
94
+ }
95
+ }
96
+ try {
97
+ await client.tui?.showToast({ body: { message, variant } });
98
+ } catch (err) {
99
+ if (!(err instanceof TypeError)) debugLog("toast failed:", err);
100
+ }
100
101
  }
101
- }
102
102
 
103
- return {
104
- toast,
105
- sendCommandMessage,
106
- runCliCommand,
107
- reloadAccountManagerFromDisk,
108
- persistOpenCodeAuth,
109
- };
103
+ return {
104
+ toast,
105
+ sendCommandMessage,
106
+ runCliCommand,
107
+ reloadAccountManagerFromDisk,
108
+ persistOpenCodeAuth,
109
+ };
110
110
  }
111
111
 
112
112
  export type PluginHelpers = ReturnType<typeof createPluginHelpers>;
@@ -6,164 +6,170 @@ import { markTokenStateUpdated, readDiskAccountAuth, refreshAccountToken } from
6
6
  type RefreshSource = "foreground" | "idle";
7
7
 
8
8
  type RefreshInFlightEntry = {
9
- promise: Promise<string>;
10
- source: RefreshSource;
9
+ promise: Promise<string>;
10
+ source: RefreshSource;
11
11
  };
12
12
 
13
13
  export interface RefreshDeps {
14
- client: OpenCodeClient;
15
- // eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin config accepts forward-compatible arbitrary keys
16
- config: AnthropicAuthConfig & Record<string, any>;
17
- getAccountManager: () => AccountManager | null;
18
- debugLog: (...args: unknown[]) => void;
14
+ client: OpenCodeClient;
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- plugin config accepts forward-compatible arbitrary keys
16
+ config: AnthropicAuthConfig & Record<string, any>;
17
+ getAccountManager: () => AccountManager | null;
18
+ debugLog: (...args: unknown[]) => void;
19
19
  }
20
20
 
21
21
  export function createRefreshHelpers({ client, config, getAccountManager, debugLog }: RefreshDeps) {
22
- const refreshInFlight = new Map<string, RefreshInFlightEntry>();
23
- const idleRefreshLastAttempt = new Map<string, number>();
24
- const idleRefreshInFlight = new Set<string>();
22
+ const refreshInFlight = new Map<string, RefreshInFlightEntry>();
23
+ const idleRefreshLastAttempt = new Map<string, number>();
24
+ const idleRefreshInFlight = new Set<string>();
25
25
 
26
- const IDLE_REFRESH_ENABLED = config.idle_refresh.enabled;
27
- const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1000;
28
- const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1000;
26
+ const IDLE_REFRESH_ENABLED = config.idle_refresh.enabled;
27
+ const IDLE_REFRESH_WINDOW_MS = config.idle_refresh.window_minutes * 60 * 1000;
28
+ const IDLE_REFRESH_MIN_INTERVAL_MS = config.idle_refresh.min_interval_minutes * 60 * 1000;
29
29
 
30
- function parseRefreshFailure(refreshError: unknown) {
31
- const message = refreshError instanceof Error ? refreshError.message : String(refreshError);
32
- const status =
33
- typeof refreshError === "object" && refreshError && "status" in refreshError
34
- ? Number((refreshError as Record<string, unknown>).status)
35
- : NaN;
36
- const errorCode =
37
- typeof refreshError === "object" && refreshError && ("errorCode" in refreshError || "code" in refreshError)
38
- ? String(
39
- (refreshError as Record<string, unknown>).errorCode || (refreshError as Record<string, unknown>).code || "",
40
- )
41
- : "";
42
- const msgLower = message.toLowerCase();
43
- const isInvalidGrant =
44
- errorCode === "invalid_grant" || errorCode === "invalid_request" || msgLower.includes("invalid_grant");
45
- const isTerminalStatus = status === 400 || status === 401 || status === 403;
46
- return { message, status, errorCode, isInvalidGrant, isTerminalStatus };
47
- }
30
+ function parseRefreshFailure(refreshError: unknown) {
31
+ const message = refreshError instanceof Error ? refreshError.message : String(refreshError);
32
+ const status =
33
+ typeof refreshError === "object" && refreshError && "status" in refreshError
34
+ ? Number((refreshError as Record<string, unknown>).status)
35
+ : NaN;
36
+ const errorCode =
37
+ typeof refreshError === "object" && refreshError && ("errorCode" in refreshError || "code" in refreshError)
38
+ ? String(
39
+ (refreshError as Record<string, unknown>).errorCode ||
40
+ (refreshError as Record<string, unknown>).code ||
41
+ "",
42
+ )
43
+ : "";
44
+ const msgLower = message.toLowerCase();
45
+ const isInvalidGrant =
46
+ errorCode === "invalid_grant" || errorCode === "invalid_request" || msgLower.includes("invalid_grant");
47
+ const isTerminalStatus = status === 400 || status === 401 || status === 403;
48
+ return { message, status, errorCode, isInvalidGrant, isTerminalStatus };
49
+ }
48
50
 
49
- async function refreshAccountTokenSingleFlight(
50
- account: ManagedAccount,
51
- source: RefreshSource = "foreground",
52
- ): Promise<string> {
53
- const key = account.id;
54
- const existing = refreshInFlight.get(key);
55
- if (existing) {
56
- if (source === "foreground" && existing.source === "idle") {
57
- try {
58
- await existing.promise;
59
- } catch (err) {
60
- void err;
61
- }
62
- if (account.access && account.expires && account.expires > Date.now()) return account.access;
63
- const retried = refreshInFlight.get(key);
64
- if (retried && retried !== existing) {
65
- return retried.promise;
51
+ async function refreshAccountTokenSingleFlight(
52
+ account: ManagedAccount,
53
+ source: RefreshSource = "foreground",
54
+ ): Promise<string> {
55
+ const key = account.id;
56
+ const existing = refreshInFlight.get(key);
57
+ if (existing) {
58
+ if (source === "foreground" && existing.source === "idle") {
59
+ try {
60
+ await existing.promise;
61
+ } catch (err) {
62
+ void err;
63
+ }
64
+ if (account.access && account.expires && account.expires > Date.now()) return account.access;
65
+ const retried = refreshInFlight.get(key);
66
+ if (retried && retried !== existing) {
67
+ return retried.promise;
68
+ }
69
+ } else {
70
+ return existing.promise;
71
+ }
66
72
  }
67
- } else {
68
- return existing.promise;
69
- }
70
- }
71
73
 
72
- const entry: RefreshInFlightEntry = {
73
- source,
74
- promise: Promise.resolve(""),
75
- };
76
- const p = (async () => {
77
- try {
78
- return await refreshAccountToken(account, client, source, {
79
- onTokensUpdated: async () => {
74
+ const entry: RefreshInFlightEntry = {
75
+ source,
76
+ promise: Promise.resolve(""),
77
+ };
78
+ const p = (async () => {
80
79
  try {
81
- await getAccountManager()!.saveToDisk();
82
- } catch {
83
- getAccountManager()!.requestSaveToDisk();
84
- throw new Error("save failed, debounced retry scheduled");
80
+ return await refreshAccountToken(account, client, source, {
81
+ onTokensUpdated: async () => {
82
+ try {
83
+ await getAccountManager()!.saveToDisk();
84
+ } catch {
85
+ getAccountManager()!.requestSaveToDisk();
86
+ throw new Error("save failed, debounced retry scheduled");
87
+ }
88
+ },
89
+ debugLog,
90
+ });
91
+ } finally {
92
+ if (refreshInFlight.get(key) === entry) refreshInFlight.delete(key);
85
93
  }
86
- },
87
- debugLog,
88
- });
89
- } finally {
90
- if (refreshInFlight.get(key) === entry) refreshInFlight.delete(key);
91
- }
92
- })();
93
- entry.promise = p;
94
- refreshInFlight.set(key, entry);
95
- return p;
96
- }
94
+ })();
95
+ entry.promise = p;
96
+ refreshInFlight.set(key, entry);
97
+ return p;
98
+ }
97
99
 
98
- async function refreshIdleAccount(account: ManagedAccount) {
99
- if (!getAccountManager()) return;
100
- if (idleRefreshInFlight.has(account.id)) return;
101
- idleRefreshInFlight.add(account.id);
102
- const attemptedRefreshToken = account.refreshToken;
103
- try {
104
- try {
105
- await refreshAccountTokenSingleFlight(account, "idle");
106
- return;
107
- } catch (err) {
108
- let details = parseRefreshFailure(err);
109
- if (!(details.isInvalidGrant || details.isTerminalStatus)) {
110
- debugLog("idle refresh skipped after transient failure", {
111
- accountIndex: account.index,
112
- status: details.status,
113
- errorCode: details.errorCode,
114
- message: details.message,
115
- });
116
- return;
117
- }
118
- const diskAuth = await readDiskAccountAuth(account.id);
119
- const retryToken = diskAuth?.refreshToken;
120
- if (retryToken && retryToken !== attemptedRefreshToken && account.refreshToken === attemptedRefreshToken) {
121
- account.refreshToken = retryToken;
122
- if (diskAuth?.tokenUpdatedAt) account.tokenUpdatedAt = diskAuth.tokenUpdatedAt;
123
- else markTokenStateUpdated(account);
124
- }
100
+ async function refreshIdleAccount(account: ManagedAccount) {
101
+ if (!getAccountManager()) return;
102
+ if (idleRefreshInFlight.has(account.id)) return;
103
+ idleRefreshInFlight.add(account.id);
104
+ const attemptedRefreshToken = account.refreshToken;
125
105
  try {
126
- await refreshAccountTokenSingleFlight(account, "idle");
127
- } catch (retryErr) {
128
- details = parseRefreshFailure(retryErr);
129
- debugLog("idle refresh retry failed", {
130
- accountIndex: account.index,
131
- status: details.status,
132
- errorCode: details.errorCode,
133
- message: details.message,
134
- });
106
+ try {
107
+ await refreshAccountTokenSingleFlight(account, "idle");
108
+ return;
109
+ } catch (err) {
110
+ let details = parseRefreshFailure(err);
111
+ if (!(details.isInvalidGrant || details.isTerminalStatus)) {
112
+ debugLog("idle refresh skipped after transient failure", {
113
+ accountIndex: account.index,
114
+ status: details.status,
115
+ errorCode: details.errorCode,
116
+ message: details.message,
117
+ });
118
+ return;
119
+ }
120
+ const diskAuth = await readDiskAccountAuth(account.id);
121
+ const retryToken = diskAuth?.refreshToken;
122
+ if (
123
+ retryToken &&
124
+ retryToken !== attemptedRefreshToken &&
125
+ account.refreshToken === attemptedRefreshToken
126
+ ) {
127
+ account.refreshToken = retryToken;
128
+ if (diskAuth?.tokenUpdatedAt) account.tokenUpdatedAt = diskAuth.tokenUpdatedAt;
129
+ else markTokenStateUpdated(account);
130
+ }
131
+ try {
132
+ await refreshAccountTokenSingleFlight(account, "idle");
133
+ } catch (retryErr) {
134
+ details = parseRefreshFailure(retryErr);
135
+ debugLog("idle refresh retry failed", {
136
+ accountIndex: account.index,
137
+ status: details.status,
138
+ errorCode: details.errorCode,
139
+ message: details.message,
140
+ });
141
+ }
142
+ }
143
+ } finally {
144
+ idleRefreshInFlight.delete(account.id);
135
145
  }
136
- }
137
- } finally {
138
- idleRefreshInFlight.delete(account.id);
139
146
  }
140
- }
141
147
 
142
- function maybeRefreshIdleAccounts(activeAccount: ManagedAccount) {
143
- const accountManager = getAccountManager();
144
- if (!IDLE_REFRESH_ENABLED || !accountManager) return;
145
- const now = Date.now();
146
- const excluded = new Set([activeAccount.index]);
147
- const candidates = accountManager
148
- .getEnabledAccounts(excluded)
149
- .filter((acc) => !acc.expires || acc.expires <= now + IDLE_REFRESH_WINDOW_MS)
150
- .filter((acc) => {
151
- const last = idleRefreshLastAttempt.get(acc.id) ?? 0;
152
- return now - last >= IDLE_REFRESH_MIN_INTERVAL_MS;
153
- })
154
- .sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
155
- const target = candidates[0];
156
- if (!target) return;
157
- idleRefreshLastAttempt.set(target.id, now);
158
- void refreshIdleAccount(target);
159
- }
148
+ function maybeRefreshIdleAccounts(activeAccount: ManagedAccount) {
149
+ const accountManager = getAccountManager();
150
+ if (!IDLE_REFRESH_ENABLED || !accountManager) return;
151
+ const now = Date.now();
152
+ const excluded = new Set([activeAccount.index]);
153
+ const candidates = accountManager
154
+ .getEnabledAccounts(excluded)
155
+ .filter((acc) => !acc.expires || acc.expires <= now + IDLE_REFRESH_WINDOW_MS)
156
+ .filter((acc) => {
157
+ const last = idleRefreshLastAttempt.get(acc.id) ?? 0;
158
+ return now - last >= IDLE_REFRESH_MIN_INTERVAL_MS;
159
+ })
160
+ .sort((a, b) => (a.expires ?? 0) - (b.expires ?? 0));
161
+ const target = candidates[0];
162
+ if (!target) return;
163
+ idleRefreshLastAttempt.set(target.id, now);
164
+ void refreshIdleAccount(target);
165
+ }
160
166
 
161
- return {
162
- parseRefreshFailure,
163
- refreshAccountTokenSingleFlight,
164
- refreshIdleAccount,
165
- maybeRefreshIdleAccounts,
166
- };
167
+ return {
168
+ parseRefreshFailure,
169
+ refreshAccountTokenSingleFlight,
170
+ refreshIdleAccount,
171
+ maybeRefreshIdleAccounts,
172
+ };
167
173
  }
168
174
 
169
175
  export type RefreshHelpers = ReturnType<typeof createRefreshHelpers>;