@cortexkit/opencode-openai-auth 1.0.5 → 1.1.0

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 (102) hide show
  1. package/dist/bin.js +9 -8
  2. package/dist/{pool-lifecycle-C_6YRmsh.d.ts → codex-request-BXDXToyt.d.ts} +158 -27
  3. package/dist/index-0yh85bwb.js +69 -0
  4. package/dist/{index-826s1yxa.js → index-4c8yw54k.js} +2 -2
  5. package/dist/{index-y4gxyxqd.js → index-7c6sxtd6.js} +2 -2
  6. package/dist/index-7eb3dw37.js +90 -0
  7. package/dist/index-8re8rprk.js +2 -0
  8. package/dist/{index-66asrje2.js → index-csky2xpg.js} +1 -1
  9. package/dist/index-edbdscy7.js +65 -0
  10. package/dist/{index-pg305s90.js → index-f2fmb596.js} +1 -1
  11. package/dist/{index-fsxbncpw.js → index-j8v2xwq6.js} +1 -1
  12. package/dist/{index-fsky1rsh.js → index-k6cydkgt.js} +2 -2
  13. package/dist/{index-116q849r.js → index-m5qmvr6c.js} +2 -2
  14. package/dist/index-msx2m78k.js +8 -0
  15. package/dist/{index-nx2qh047.js → index-zpm95mt5.js} +1 -1
  16. package/dist/index.d.ts +15 -7
  17. package/dist/index.js +1 -1
  18. package/dist/{map-m6f5a_IA.d.ts → map-Buq-GW7l.d.ts} +1 -1
  19. package/dist/{protocol-DctqemRI.d.ts → model-YRg7Uowo.d.ts} +67 -25
  20. package/dist/{port-file-ZPlxXB4G.d.ts → port-file-CxEpgSHY.d.ts} +1 -1
  21. package/dist/protocol-CDV1Pc6h.d.ts +29 -0
  22. package/dist/rpc/port-file.d.ts +1 -1
  23. package/dist/rpc/port-file.js +1 -1
  24. package/dist/rpc/protocol.d.ts +1 -1
  25. package/dist/rpc/protocol.js +1 -1
  26. package/dist/rpc/rpc-client.d.ts +3 -3
  27. package/dist/rpc/rpc-client.js +1 -1
  28. package/dist/rpc/rpc-dir.js +1 -1
  29. package/dist/{sidebar-state-z_M7Trnd.d.ts → sidebar-state-DjTWUbmf.d.ts} +15 -9
  30. package/dist/sidebar-state.d.ts +1 -1
  31. package/dist/sidebar-state.js +1 -1
  32. package/dist/{token-bzhkgc29.js → token-rz4t3ay6.js} +1 -1
  33. package/dist/token-util-tpy2c7h6.js +1 -0
  34. package/dist/tui-preferences.js +1 -1
  35. package/dist/tui.d.ts +20 -2
  36. package/dist/v2/server.d.ts +73 -1
  37. package/dist/v2/server.js +3 -3
  38. package/package.json +2 -2
  39. package/src/tui-compiled/raw/shared/0332701351b3eeec-format.js +13 -8
  40. package/src/tui-compiled/raw/shared/0f77ebc3c914c9c6-custody.js +354 -0
  41. package/src/tui-compiled/raw/shared/134b008900698fe0-backoff.js +92 -0
  42. package/src/tui-compiled/raw/shared/169b2b4bf30909bc-consumer.js +393 -0
  43. package/src/tui-compiled/raw/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  44. package/src/tui-compiled/raw/shared/2dce6e9613993329-quota-manager.js +473 -0
  45. package/src/tui-compiled/raw/shared/30f5bc480f31f51c-roster.js +566 -0
  46. package/src/tui-compiled/raw/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  47. package/src/tui-compiled/raw/shared/4593a4d71edf69fd-tombstone.js +25 -0
  48. package/src/tui-compiled/raw/shared/4d05c2d7a531aece-oauth.js +500 -0
  49. package/src/tui-compiled/raw/shared/6371b24a0573d79e-any-signal.js +42 -0
  50. package/src/tui-compiled/raw/shared/7c140680fc81be92-interlock.js +29 -0
  51. package/src/tui-compiled/raw/shared/85512cc5263ba259-provider.js +87 -0
  52. package/src/tui-compiled/raw/shared/abd062d633e29fd2-pool-routing.js +192 -0
  53. package/src/tui-compiled/raw/shared/b9588ad4725c1d10-enrollment.js +806 -0
  54. package/src/tui-compiled/raw/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  55. package/src/tui-compiled/raw/shared/c852a15cfe489d12-pool.js +0 -1
  56. package/src/tui-compiled/raw/shared/ca0fccaba1d87af4-errors.js +8 -0
  57. package/src/tui-compiled/raw/shared/d0f0b6be200311af-insula.js +152 -0
  58. package/src/tui-compiled/raw/shared/d9d2892a4c371301-host-slot.js +72 -0
  59. package/src/tui-compiled/raw/shared/dbc61cd405941828-accounts.js +1470 -0
  60. package/src/tui-compiled/raw/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  61. package/src/tui-compiled/raw/shared/e8d3c8da776ef9cd-index.js +24 -0
  62. package/src/tui-compiled/raw/sidebar-state.js +41 -14
  63. package/src/tui-compiled/raw/tui/command-dialogs.tsx +3 -34
  64. package/src/tui-compiled/raw/tui/section-text.js +21 -0
  65. package/src/tui-compiled/raw/tui/v2-dialog.js +137 -0
  66. package/src/tui-compiled/raw/tui/v2.tsx +133 -0
  67. package/src/tui-compiled/raw/tui.tsx +146 -40
  68. package/src/tui-compiled/runtime/shared/0332701351b3eeec-format.js +13 -8
  69. package/src/tui-compiled/runtime/shared/0f77ebc3c914c9c6-custody.js +354 -0
  70. package/src/tui-compiled/runtime/shared/134b008900698fe0-backoff.js +92 -0
  71. package/src/tui-compiled/runtime/shared/169b2b4bf30909bc-consumer.js +393 -0
  72. package/src/tui-compiled/runtime/shared/2577ec69f9973ae9-quota-normalize.js +154 -0
  73. package/src/tui-compiled/runtime/shared/2dce6e9613993329-quota-manager.js +473 -0
  74. package/src/tui-compiled/runtime/shared/30f5bc480f31f51c-roster.js +566 -0
  75. package/src/tui-compiled/runtime/shared/43fc9e6b994519c7-refresh-lock.js +0 -1
  76. package/src/tui-compiled/runtime/shared/4593a4d71edf69fd-tombstone.js +25 -0
  77. package/src/tui-compiled/runtime/shared/4d05c2d7a531aece-oauth.js +500 -0
  78. package/src/tui-compiled/runtime/shared/6371b24a0573d79e-any-signal.js +42 -0
  79. package/src/tui-compiled/runtime/shared/7c140680fc81be92-interlock.js +29 -0
  80. package/src/tui-compiled/runtime/shared/85512cc5263ba259-provider.js +87 -0
  81. package/src/tui-compiled/runtime/shared/abd062d633e29fd2-pool-routing.js +192 -0
  82. package/src/tui-compiled/runtime/shared/b9588ad4725c1d10-enrollment.js +806 -0
  83. package/src/tui-compiled/runtime/shared/bfbdd0f9aae56b47-pool-quota.js +85 -0
  84. package/src/tui-compiled/runtime/shared/c852a15cfe489d12-pool.js +0 -1
  85. package/src/tui-compiled/runtime/shared/ca0fccaba1d87af4-errors.js +8 -0
  86. package/src/tui-compiled/runtime/shared/d0f0b6be200311af-insula.js +152 -0
  87. package/src/tui-compiled/runtime/shared/d9d2892a4c371301-host-slot.js +72 -0
  88. package/src/tui-compiled/runtime/shared/dbc61cd405941828-accounts.js +1470 -0
  89. package/src/tui-compiled/runtime/shared/e8cd4929507508d8-refresh-file-lock.js +26 -12
  90. package/src/tui-compiled/runtime/shared/e8d3c8da776ef9cd-index.js +24 -0
  91. package/src/tui-compiled/runtime/sidebar-state.js +41 -14
  92. package/src/tui-compiled/runtime/tui/command-dialogs.js +3 -15
  93. package/src/tui-compiled/runtime/tui/section-text.js +21 -0
  94. package/src/tui-compiled/runtime/tui/v2-dialog.js +137 -0
  95. package/src/tui-compiled/runtime/tui/v2.js +95 -0
  96. package/src/tui-compiled/runtime/tui.js +255 -163
  97. package/dist/index-4fnghj9e.js +0 -65
  98. package/dist/index-a69gr76b.js +0 -2
  99. package/dist/index-h263aftt.js +0 -157
  100. package/dist/index-ssj7m2rd.js +0 -8
  101. package/dist/index-ydz3hbhc.js +0 -2
  102. package/dist/token-util-4a2xxnpg.js +0 -1
@@ -0,0 +1,1470 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ import {
4
+ acquireRefreshFileLock,
5
+ writeJsonAtomic
6
+ } from "./ec971142548df90e-index.js";
7
+ import {
8
+ buildQuotaOperationError,
9
+ buildRefreshOperationError,
10
+ formatRefreshBackoffMessage,
11
+ isTransientQuotaError,
12
+ quotaBackoffActive,
13
+ refreshBackoffActive
14
+ } from "./134b008900698fe0-backoff.js";
15
+ import { createLogger } from "./d669d72c40e9e116-logger.js";
16
+ import { extractAccountId } from "./4d05c2d7a531aece-oauth.js";
17
+ import {
18
+ ACCOUNT_FILE_NAME,
19
+ ACCOUNT_STATE_FILE_NAME,
20
+ deriveStatePath
21
+ } from "./b85c5301fddcb98a-paths.js";
22
+ import { PRIMARY, SECONDARY } from "./85512cc5263ba259-provider.js";
23
+ import { quotaWindowResetIsPast } from "./2dce6e9613993329-quota-manager.js";
24
+ import {
25
+ assertNotTombstoneRefresh,
26
+ HOST_SLOT_TOMBSTONE_REFRESH,
27
+ isTombstoned,
28
+ TombstoneRefreshError
29
+ } from "./4593a4d71edf69fd-tombstone.js";
30
+ const logR = createLogger("refresh"), logA = createLogger("accounts"), SAVE_ACCOUNTS_LOCK_TTL_MS = 1e4, SAVE_ACCOUNTS_LOCK_WAIT_MS = 15000, SAVE_ACCOUNTS_LOCK_RETRY_MS = 50;
31
+
32
+ export { ACCOUNT_FILE_NAME, ACCOUNT_STATE_FILE_NAME, deriveStatePath };
33
+ export { PRIMARY, SECONDARY };
34
+ export function isOAuthAccount(account) {
35
+ return account.type === "oauth" && account.corrupt !== !0;
36
+ }
37
+ export function isApiKeyAccount(account) {
38
+ return account.type === "api";
39
+ }
40
+ export function isValidApiBaseURL(value) {
41
+ const raw = value?.trim();
42
+ if (!raw)
43
+ return !1;
44
+ try {
45
+ const url = new URL(raw);
46
+ return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
47
+ } catch {
48
+ return !1;
49
+ }
50
+ }
51
+ export const KILLSWITCH_FLOORS_SCHEMA = "floors-v1";
52
+ export function isCostZeroingEnabled(storage) {
53
+ return storage.costZeroing?.enabled !== !1;
54
+ }
55
+
56
+ export class AccountRemovedDuringRefreshError extends Error {
57
+ code = "ACCOUNT_REMOVED_DURING_REFRESH";
58
+ constructor(accountId) {
59
+ super(`Fallback account ${accountId} was removed before refresh`);
60
+ this.name = "AccountRemovedDuringRefreshError";
61
+ }
62
+ }
63
+ const DEFAULT_FALLBACK_ON = [401, 403, 429], DEFAULT_MINIMUM_REMAINING = {
64
+ primary: 0,
65
+ secondary: 0
66
+ }, DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA = !1;
67
+ export const DEFAULT_KILLSWITCH_THRESHOLDS = {
68
+ primary: 5,
69
+ secondary: 10
70
+ };
71
+ function isRecord(value) {
72
+ return value != null && typeof value === "object" && !Array.isArray(value);
73
+ }
74
+ const UNSAFE_RESET_ACCOUNT_KEYS = new Set([
75
+ "__proto__",
76
+ "constructor",
77
+ "prototype"
78
+ ]);
79
+ export function isSafeResetAccountKey(accountKey) {
80
+ return accountKey.length > 0 && !UNSAFE_RESET_ACCOUNT_KEYS.has(accountKey);
81
+ }
82
+ function isAccountRemovedDuringRefreshError(error) {
83
+ return error instanceof AccountRemovedDuringRefreshError || isRecord(error) && error.name === "AccountRemovedDuringRefreshError" && error.code === "ACCOUNT_REMOVED_DURING_REFRESH";
84
+ }
85
+ function normalizeAccountBase(value) {
86
+ return {
87
+ id: typeof value.id === "string" && value.id.trim() ? value.id.trim() : randomUUID(),
88
+ label: typeof value.label === "string" ? value.label : void 0,
89
+ enabled: typeof value.enabled === "boolean" ? value.enabled : void 0,
90
+ addedAt: typeof value.addedAt === "number" ? value.addedAt : void 0,
91
+ lastUsed: typeof value.lastUsed === "number" ? value.lastUsed : void 0,
92
+ accountId: typeof value.accountId === "string" ? value.accountId : void 0
93
+ };
94
+ }
95
+ function normalizeOperationError(value) {
96
+ if (!isRecord(value))
97
+ return;
98
+ if (typeof value.message !== "string")
99
+ return;
100
+ const checkedAt = Number(value.checkedAt);
101
+ if (!Number.isFinite(checkedAt))
102
+ return;
103
+ const nextRetryAt = Number(value.nextRetryAt), retryCount = Number(value.retryCount);
104
+ return {
105
+ message: value.message,
106
+ checkedAt,
107
+ nextRetryAt: Number.isFinite(nextRetryAt) ? nextRetryAt : void 0,
108
+ retryCount: Number.isFinite(retryCount) ? retryCount : void 0,
109
+ tokenHash: typeof value.tokenHash === "string" ? value.tokenHash : void 0
110
+ };
111
+ }
112
+ function normalizeQuota(value) {
113
+ if (!isRecord(value))
114
+ return;
115
+ const quota = {};
116
+ for (const key of ["primary", "secondary"]) {
117
+ const window = value[key];
118
+ if (!isRecord(window))
119
+ continue;
120
+ const usedPercent = Number(window.usedPercent), remainingPercent = Number(window.remainingPercent), checkedAt = Number(window.checkedAt);
121
+ if (!Number.isFinite(usedPercent) || !Number.isFinite(remainingPercent) || !Number.isFinite(checkedAt))
122
+ continue;
123
+ const windowMinutes = typeof window.windowMinutes === "number" ? window.windowMinutes : Number.NaN;
124
+ quota[key] = {
125
+ usedPercent,
126
+ remainingPercent,
127
+ checkedAt,
128
+ resetsAt: typeof window.resetsAt === "string" ? window.resetsAt : void 0,
129
+ ...Number.isFinite(windowMinutes) && windowMinutes > 0 ? { windowMinutes } : {}
130
+ };
131
+ }
132
+ for (const key of [
133
+ "resetCreditsAvailable",
134
+ "resetCreditsApplicable"
135
+ ]) {
136
+ const credits = typeof value[key] === "number" ? value[key] : Number.NaN;
137
+ if (Number.isFinite(credits) && credits >= 0)
138
+ quota[key] = credits;
139
+ }
140
+ const spendControl = value.spendControl;
141
+ if (isRecord(spendControl)) {
142
+ const limit = Number(spendControl.limit), used = Number(spendControl.used), remaining = Number(spendControl.remaining), usedPercent = Number(spendControl.usedPercent), remainingPercent = Number(spendControl.remainingPercent);
143
+ if (Number.isFinite(limit) && Number.isFinite(used) && Number.isFinite(remaining) && Number.isFinite(usedPercent) && Number.isFinite(remainingPercent) && typeof spendControl.reached === "boolean")
144
+ quota.spendControl = {
145
+ limit,
146
+ used,
147
+ remaining,
148
+ usedPercent,
149
+ remainingPercent,
150
+ resetsAt: typeof spendControl.resetsAt === "string" ? spendControl.resetsAt : void 0,
151
+ unit: typeof spendControl.unit === "string" ? spendControl.unit : void 0,
152
+ source: typeof spendControl.source === "string" ? spendControl.source : void 0,
153
+ reached: spendControl.reached
154
+ };
155
+ }
156
+ const credits = value.credits;
157
+ if (isRecord(credits) && typeof credits.hasCredits === "boolean" && typeof credits.unlimited === "boolean" && typeof credits.overageLimitReached === "boolean") {
158
+ const balance = Number(credits.balance);
159
+ quota.credits = {
160
+ hasCredits: credits.hasCredits,
161
+ unlimited: credits.unlimited,
162
+ overageLimitReached: credits.overageLimitReached,
163
+ ...Number.isFinite(balance) ? { balance } : {}
164
+ };
165
+ }
166
+ return Object.keys(quota).length ? quota : void 0;
167
+ }
168
+ export function normalizeAccount(value) {
169
+ if (!isRecord(value))
170
+ return null;
171
+ if (value.type === "api") {
172
+ const baseURL = typeof value.baseURL === "string" ? value.baseURL.trim() : "", apiKey = typeof value.apiKey === "string" ? value.apiKey.trim() : "";
173
+ if (!isValidApiBaseURL(baseURL))
174
+ return null;
175
+ const authHeader = value.authHeader === "x-api-key" ? "x-api-key" : "authorization-bearer";
176
+ return {
177
+ ...normalizeAccountBase(value),
178
+ type: "api",
179
+ apiKey: apiKey || void 0,
180
+ baseURL,
181
+ authHeader
182
+ };
183
+ }
184
+ if (value.type !== "oauth")
185
+ return null;
186
+ if (value.corrupt === !0) {
187
+ if (typeof value.id !== "string" || !value.id.trim())
188
+ return null;
189
+ return {
190
+ ...normalizeAccountBase(value),
191
+ type: "oauth",
192
+ corrupt: !0
193
+ };
194
+ }
195
+ if (typeof value.refresh !== "string" || !value.refresh.trim()) {
196
+ if (!Object.hasOwn(value, "refresh") || typeof value.id !== "string" || !value.id.trim())
197
+ return null;
198
+ return {
199
+ ...normalizeAccountBase(value),
200
+ type: "oauth",
201
+ corrupt: !0
202
+ };
203
+ }
204
+ return {
205
+ ...normalizeAccountBase(value),
206
+ type: "oauth",
207
+ access: typeof value.access === "string" ? value.access : void 0,
208
+ refresh: value.refresh,
209
+ expires: typeof value.expires === "number" ? value.expires : void 0,
210
+ lastRefreshedAt: typeof value.lastRefreshedAt === "number" ? value.lastRefreshedAt : void 0,
211
+ lastRefreshError: normalizeOperationError(value.lastRefreshError),
212
+ lastQuotaRefreshError: normalizeOperationError(value.lastQuotaRefreshError),
213
+ quota: normalizeQuota(value.quota)
214
+ };
215
+ }
216
+ function normalizeResetState(value) {
217
+ if (!isRecord(value))
218
+ return;
219
+ const normalized = Object.create(null);
220
+ for (const [accountId, candidate] of Object.entries(value)) {
221
+ if (!isSafeResetAccountKey(accountId) || !isRecord(candidate))
222
+ continue;
223
+ const state = {};
224
+ if (isRecord(candidate.inFlight))
225
+ state.inFlight = { ...candidate.inFlight };
226
+ if (isRecord(candidate.lastOutcome) && typeof candidate.lastOutcome.code === "string" && candidate.lastOutcome.code.length > 0 && typeof candidate.lastOutcome.at === "number" && Number.isFinite(candidate.lastOutcome.at))
227
+ state.lastOutcome = {
228
+ code: candidate.lastOutcome.code,
229
+ at: candidate.lastOutcome.at,
230
+ ...isRecord(candidate.lastOutcome.previousOutcome) && typeof candidate.lastOutcome.previousOutcome.code === "string" && candidate.lastOutcome.previousOutcome.code.length > 0 && typeof candidate.lastOutcome.previousOutcome.at === "number" && Number.isFinite(candidate.lastOutcome.previousOutcome.at) ? {
231
+ previousOutcome: {
232
+ code: candidate.lastOutcome.previousOutcome.code,
233
+ at: candidate.lastOutcome.previousOutcome.at
234
+ }
235
+ } : {}
236
+ };
237
+ if (typeof candidate.cooldownUntil === "number" && Number.isFinite(candidate.cooldownUntil))
238
+ state.cooldownUntil = candidate.cooldownUntil;
239
+ if (Object.keys(state).length > 0)
240
+ normalized[accountId] = state;
241
+ }
242
+ return Object.keys(normalized).length > 0 ? normalized : void 0;
243
+ }
244
+ const warnedRosterDrops = new Set;
245
+ function emitRosterDropWarning(droppedIds) {
246
+ if (droppedIds.length === 0)
247
+ return;
248
+ const key = JSON.stringify([...droppedIds].sort());
249
+ if (warnedRosterDrops.has(key))
250
+ return;
251
+ warnedRosterDrops.add(key);
252
+ logA.warn("account load-dropped, preserved on disk", {
253
+ droppedIds
254
+ });
255
+ }
256
+ function normalizeStorage(value) {
257
+ if (!isRecord(value) || !Array.isArray(value.accounts))
258
+ return null;
259
+ const inputAccounts = value.accounts, normalizedAccounts = inputAccounts.map(normalizeAccount).filter((account) => account != null);
260
+ if (normalizedAccounts.length < inputAccounts.length) {
261
+ const inputIds = new Set;
262
+ for (const candidate of inputAccounts)
263
+ if (isRecord(candidate) && typeof candidate.id === "string" && candidate.id.trim())
264
+ inputIds.add(candidate.id.trim());
265
+ const loadedIds = new Set(normalizedAccounts.map((account) => account.id)), dropped = [];
266
+ for (const id of inputIds)
267
+ if (!loadedIds.has(id))
268
+ dropped.push(id);
269
+ emitRosterDropWarning(dropped);
270
+ }
271
+ return {
272
+ version: 1,
273
+ main: { type: "opencode", provider: "openai" },
274
+ routing: isRecord(value.routing) ? value.routing : void 0,
275
+ fallbackOn: Array.isArray(value.fallbackOn) ? value.fallbackOn.filter((status) => Number.isInteger(status)) : void 0,
276
+ refresh: isRecord(value.refresh) ? value.refresh : void 0,
277
+ quota: isRecord(value.quota) ? value.quota : void 0,
278
+ reset: normalizeResetState(value.reset),
279
+ dump: isRecord(value.dump) ? value.dump : void 0,
280
+ costZeroing: isRecord(value.costZeroing) ? value.costZeroing : void 0,
281
+ killswitch: isRecord(value.killswitch) ? value.killswitch : void 0,
282
+ logging: isRecord(value.logging) ? value.logging : void 0,
283
+ cachekeep: isRecord(value.cachekeep) ? value.cachekeep : void 0,
284
+ cacheKeep: isRecord(value.cacheKeep) ? value.cacheKeep : void 0,
285
+ mainAccountId: typeof value.mainAccountId === "string" ? value.mainAccountId : void 0,
286
+ accounts: normalizedAccounts
287
+ };
288
+ }
289
+ function withoutRetiredSettings(config) {
290
+ if (!("claustrum" in config))
291
+ return config;
292
+ const { claustrum: _retired, ...rest } = config;
293
+ return rest;
294
+ }
295
+ import { readFile } from "node:fs/promises";
296
+ async function readJsonIfPresent(path) {
297
+ try {
298
+ return { exists: !0, value: JSON.parse(await readFile(path, "utf8")) };
299
+ } catch (error) {
300
+ if (error.code === "ENOENT")
301
+ return { exists: !1, value: null };
302
+ throw error;
303
+ }
304
+ }
305
+ function objectWithDefinedEntries(value) {
306
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
307
+ }
308
+ export async function readConfigRosterIds(path) {
309
+ let value;
310
+ try {
311
+ value = (await readJsonIfPresent(path)).value;
312
+ } catch {
313
+ return null;
314
+ }
315
+ return collectConfigRosterIds(value);
316
+ }
317
+ function mergeConfigAndState(configValue, stateValue) {
318
+ if (!isRecord(configValue))
319
+ return configValue;
320
+ const state = isRecord(stateValue) ? stateValue : {}, mainState = isRecord(state.main) ? state.main : void 0, stateAccounts = isRecord(state.accounts) ? state.accounts : {}, quotaConfig = isRecord(configValue.quota) ? configValue.quota : {}, refreshConfig = isRecord(configValue.refresh) ? configValue.refresh : {}, mainQuotaSource = mainState ?? quotaConfig, mainRefreshSource = mainState ?? refreshConfig, mainLease = refreshConfig.mainRefreshLeaseId !== void 0 ? {
321
+ id: refreshConfig.mainRefreshLeaseId,
322
+ until: refreshConfig.mainRefreshLeaseUntil,
323
+ tokenHash: refreshConfig.mainRefreshLeaseTokenHash
324
+ } : {
325
+ id: mainRefreshSource.refreshLeaseId,
326
+ until: mainRefreshSource.refreshLeaseUntil,
327
+ tokenHash: mainRefreshSource.refreshLeaseTokenHash
328
+ }, accounts = Array.isArray(configValue.accounts) ? configValue.accounts.map((account) => {
329
+ if (!isRecord(account))
330
+ return account;
331
+ const stateAccount = typeof account.id === "string" && isRecord(stateAccounts[account.id]) ? stateAccounts[account.id] : {};
332
+ return { ...account, ...stateAccount };
333
+ }) : void 0;
334
+ return omitUndefinedTopLevel({
335
+ ...configValue,
336
+ refresh: objectWithDefinedEntries({
337
+ ...refreshConfig,
338
+ mainLastRefreshError: refreshConfig.mainLastRefreshError ?? mainRefreshSource.lastRefreshError,
339
+ mainRefreshLeaseId: mainLease.id,
340
+ mainRefreshLeaseUntil: mainLease.until,
341
+ mainRefreshLeaseTokenHash: mainLease.tokenHash
342
+ }),
343
+ quota: objectWithDefinedEntries({
344
+ ...quotaConfig,
345
+ mainQuota: mainQuotaSource.quota,
346
+ mainQuotaCheckedAt: mainQuotaSource.quotaCheckedAt,
347
+ mainQuotaToken: mainQuotaSource.quotaToken,
348
+ mainLastQuotaApiError: mainQuotaSource.lastQuotaApiError
349
+ }),
350
+ accounts
351
+ });
352
+ }
353
+ export async function loadAccounts(paths) {
354
+ const config = await readJsonIfPresent(paths.configPath);
355
+ if (!config.exists)
356
+ return null;
357
+ const state = await readJsonIfPresent(paths.statePath);
358
+ return normalizeStorage(mergeConfigAndState(config.value, state.value));
359
+ }
360
+ function omitUndefinedTopLevel(value) {
361
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
362
+ }
363
+ function accountConfig(account) {
364
+ return objectWithDefinedEntries({
365
+ id: account.id,
366
+ label: account.label,
367
+ type: account.type,
368
+ corrupt: account.type === "oauth" && account.corrupt === !0 ? !0 : void 0,
369
+ enabled: account.enabled,
370
+ addedAt: account.addedAt,
371
+ accountId: account.accountId,
372
+ baseURL: account.type === "api" ? account.baseURL : void 0,
373
+ authHeader: account.type === "api" ? account.authHeader : void 0
374
+ });
375
+ }
376
+ function accountRuntimeState(account) {
377
+ if (account.type === "api")
378
+ return objectWithDefinedEntries({
379
+ apiKey: account.apiKey,
380
+ lastUsed: account.lastUsed
381
+ });
382
+ if (account.corrupt)
383
+ return {};
384
+ return objectWithDefinedEntries({
385
+ access: account.access,
386
+ refresh: account.refresh,
387
+ expires: account.expires,
388
+ lastUsed: account.lastUsed,
389
+ lastRefreshedAt: account.lastRefreshedAt,
390
+ lastRefreshError: account.lastRefreshError,
391
+ lastQuotaRefreshError: account.lastQuotaRefreshError,
392
+ quota: account.quota
393
+ });
394
+ }
395
+ function quotaSnapshotCheckedAt(quota) {
396
+ return Math.max(quota?.primary?.checkedAt ?? 0, quota?.secondary?.checkedAt ?? 0);
397
+ }
398
+ function copyRuntimeField(target, source, key) {
399
+ if (key in source)
400
+ target[key] = source[key];
401
+ else
402
+ delete target[key];
403
+ }
404
+ function tokenFieldsMatch(existing, incoming) {
405
+ return existing.access === incoming.access && existing.refresh === incoming.refresh && existing.expires === incoming.expires && existing.lastRefreshedAt === incoming.lastRefreshedAt;
406
+ }
407
+ function selectSameTokenState(existing, incoming) {
408
+ if (!tokenFieldsMatch(existing, incoming))
409
+ return incoming;
410
+ if (!("lastRefreshError" in incoming))
411
+ return existing;
412
+ if (!("lastRefreshError" in existing))
413
+ return incoming;
414
+ return (incoming.lastRefreshError?.checkedAt ?? 0) > (existing.lastRefreshError?.checkedAt ?? 0) ? incoming : existing;
415
+ }
416
+ const FUTURE_REFRESH_STAMP_TOLERANCE_MS = 300000;
417
+ function trustedRefreshStamp(stamp, now) {
418
+ if (stamp === void 0)
419
+ return 0;
420
+ return stamp > now + FUTURE_REFRESH_STAMP_TOLERANCE_MS ? 0 : stamp;
421
+ }
422
+ function applyNewerTokenState(merged, existing, incoming) {
423
+ const now = Date.now(), existingRefreshAt = trustedRefreshStamp(existing.lastRefreshedAt, now), incomingRefreshAt = trustedRefreshStamp(incoming.lastRefreshedAt, now), existingExpires = existing.expires ?? 0, incomingExpires = incoming.expires ?? 0, tokenSource = incomingRefreshAt > existingRefreshAt ? incoming : existingRefreshAt > incomingRefreshAt ? existing : incomingExpires > existingExpires ? incoming : existingExpires > incomingExpires ? existing : selectSameTokenState(existing, incoming);
424
+ copyRuntimeField(merged, tokenSource, "access");
425
+ copyRuntimeField(merged, tokenSource, "refresh");
426
+ copyRuntimeField(merged, tokenSource, "expires");
427
+ copyRuntimeField(merged, tokenSource, "lastRefreshedAt");
428
+ copyRuntimeField(merged, tokenSource, "lastRefreshError");
429
+ }
430
+ function mergeAccountRuntimeState(existing, incoming) {
431
+ if (!isRecord(existing))
432
+ return incoming;
433
+ const existingEntry = existing, existingQuotaCheckedAt = quotaSnapshotCheckedAt(existingEntry.quota), incomingQuotaCheckedAt = quotaSnapshotCheckedAt(incoming.quota), existingQuotaIsNewer = existingQuotaCheckedAt > incomingQuotaCheckedAt, merged = existingQuotaIsNewer ? {
434
+ ...existingEntry,
435
+ ...incoming,
436
+ quota: existingEntry.quota,
437
+ lastQuotaRefreshError: existingEntry.lastQuotaRefreshError
438
+ } : { ...existingEntry, ...incoming };
439
+ if (!existingQuotaIsNewer && !("lastQuotaRefreshError" in incoming))
440
+ delete merged.lastQuotaRefreshError;
441
+ if (!("lastRefreshError" in incoming))
442
+ delete merged.lastRefreshError;
443
+ if (typeof existingEntry.lastUsed === "number" && (typeof incoming.lastUsed !== "number" || existingEntry.lastUsed > incoming.lastUsed))
444
+ merged.lastUsed = existingEntry.lastUsed;
445
+ applyNewerTokenState(merged, existingEntry, incoming);
446
+ return merged;
447
+ }
448
+ function configFromStorage(storage) {
449
+ const refresh = storage.refresh ? objectWithDefinedEntries({
450
+ enabled: storage.refresh.enabled,
451
+ refreshBeforeExpiryMinutes: storage.refresh.refreshBeforeExpiryMinutes
452
+ }) : void 0, quota = storage.quota ? objectWithDefinedEntries({
453
+ enabled: storage.quota.enabled,
454
+ checkIntervalMinutes: storage.quota.checkIntervalMinutes,
455
+ refreshEveryNRequests: storage.quota.refreshEveryNRequests,
456
+ minimumRemaining: storage.quota.minimumRemaining,
457
+ failClosedOnUnknownQuota: storage.quota.failClosedOnUnknownQuota
458
+ }) : void 0;
459
+ return omitUndefinedTopLevel({
460
+ version: 1,
461
+ main: storage.main,
462
+ routing: storage.routing,
463
+ fallbackOn: storage.fallbackOn,
464
+ refresh,
465
+ quota,
466
+ reset: storage.reset,
467
+ dump: storage.dump,
468
+ costZeroing: storage.costZeroing,
469
+ killswitch: storage.killswitch,
470
+ logging: storage.logging,
471
+ cachekeep: storage.cachekeep,
472
+ cacheKeep: storage.cacheKeep,
473
+ mainAccountId: storage.mainAccountId,
474
+ accounts: storage.accounts.map(accountConfig)
475
+ });
476
+ }
477
+ const CREDENTIAL_FIELDS = [
478
+ "access",
479
+ "refresh",
480
+ "expires",
481
+ "lastRefreshedAt",
482
+ "lastRefreshError"
483
+ ];
484
+ function keepNewerCredential(onDisk, incoming) {
485
+ if (!isOAuthAccount(onDisk) || !isOAuthAccount(incoming))
486
+ return incoming;
487
+ const incomingEntry = accountRuntimeState(incoming), chosen = { ...incomingEntry };
488
+ applyNewerTokenState(chosen, accountRuntimeState(onDisk), incomingEntry);
489
+ const result = { ...incoming };
490
+ for (const field of CREDENTIAL_FIELDS)
491
+ if (field in chosen)
492
+ result[field] = chosen[field];
493
+ else
494
+ delete result[field];
495
+ return result;
496
+ }
497
+ function mergeStorageForSave(latest, incoming) {
498
+ if (!latest)
499
+ return incoming;
500
+ const accounts = new Map;
501
+ for (const account of latest.accounts)
502
+ accounts.set(account.id, account);
503
+ for (const account of incoming.accounts) {
504
+ const onDisk = accounts.get(account.id);
505
+ accounts.set(account.id, onDisk ? keepNewerCredential(onDisk, account) : account);
506
+ }
507
+ return {
508
+ ...latest,
509
+ ...incoming,
510
+ accounts: [...accounts.values()]
511
+ };
512
+ }
513
+ async function acquireSaveAccountsLock(path, renew = !1) {
514
+ const startedAt = Date.now(), deadline = startedAt + SAVE_ACCOUNTS_LOCK_WAIT_MS;
515
+ let attempts = 0;
516
+ while (Date.now() <= deadline) {
517
+ attempts++;
518
+ const lock = await acquireRefreshFileLock({
519
+ name: "save",
520
+ ttlMs: SAVE_ACCOUNTS_LOCK_TTL_MS,
521
+ path,
522
+ renew
523
+ });
524
+ if (lock)
525
+ return lock;
526
+ const remainingMs = deadline - Date.now();
527
+ if (remainingMs <= 0)
528
+ break;
529
+ await sleep(Math.min(SAVE_ACCOUNTS_LOCK_RETRY_MS + jitterMs(SAVE_ACCOUNTS_LOCK_RETRY_MS), remainingMs));
530
+ }
531
+ const elapsedMs = Date.now() - startedAt, averageGapMs = Math.round(elapsedMs / Math.max(1, attempts));
532
+ throw Error(`Timed out after ${elapsedMs}ms waiting for the account store lock on ${path} (${attempts} attempts, ~${averageGapMs}ms apart; retry interval is ${SAVE_ACCOUNTS_LOCK_RETRY_MS}ms). A gap near the retry interval means lock contention; a much larger one means this process's event loop was saturated and the wait expired without getting scheduled.`);
533
+ }
534
+ function stateFromStorage(storage) {
535
+ const accounts = Object.fromEntries(storage.accounts.map((account) => [
536
+ account.id,
537
+ accountRuntimeState(account)
538
+ ]));
539
+ return {
540
+ version: 1,
541
+ main: objectWithDefinedEntries({
542
+ quota: storage.quota?.mainQuota,
543
+ quotaCheckedAt: storage.quota?.mainQuotaCheckedAt,
544
+ quotaToken: storage.quota?.mainQuotaToken,
545
+ lastQuotaApiError: storage.quota?.mainLastQuotaApiError,
546
+ lastRefreshError: storage.refresh?.mainLastRefreshError,
547
+ refreshLeaseId: storage.refresh?.mainRefreshLeaseId,
548
+ refreshLeaseUntil: storage.refresh?.mainRefreshLeaseUntil,
549
+ refreshLeaseTokenHash: storage.refresh?.mainRefreshLeaseTokenHash
550
+ }),
551
+ accounts
552
+ };
553
+ }
554
+ export async function saveAccounts(storage, paths, canWrite = void 0) {
555
+ const { configPath: path, statePath } = paths, lock = await acquireSaveAccountsLock(path);
556
+ try {
557
+ const stateLock = await acquireSaveAccountsLock(statePath);
558
+ try {
559
+ const configJson = await readJsonIfPresent(path), stateJson = await readJsonIfPresent(statePath), latest = configJson.exists ? normalizeStorage(mergeConfigAndState(configJson.value, stateJson.value)) : null, merged = mergeStorageForSave(latest, storage), existing = isRecord(configJson.value) ? configJson.value : {}, latestAccountIds = latest ? new Set(latest.accounts.map((a) => a.id)) : new Set, preserved = buildPreservedAdditions(configJson.value, latestAccountIds, new Set), baseConfig = configFromStorage(merged), writtenIds = new Set((Array.isArray(baseConfig.accounts) ? baseConfig.accounts : []).map((e) => isRecord(e) && typeof e.id === "string" ? e.id.trim() : "").filter(Boolean)), additions = preserved.filter((raw) => {
560
+ if (!isRecord(raw))
561
+ return !1;
562
+ if (typeof raw.id !== "string")
563
+ return !1;
564
+ return !writtenIds.has(raw.id.trim());
565
+ }), nextConfig = {
566
+ ...withoutRetiredSettings(existing),
567
+ ...baseConfig,
568
+ accounts: [
569
+ ...Array.isArray(baseConfig.accounts) ? baseConfig.accounts : [],
570
+ ...additions
571
+ ]
572
+ };
573
+ if (canWrite && !canWrite())
574
+ return;
575
+ await writeJsonAtomic(path, nextConfig);
576
+ if (canWrite && !canWrite())
577
+ return;
578
+ await writeJsonAtomic(statePath, stateFromStorage(merged));
579
+ } finally {
580
+ await stateLock.release();
581
+ }
582
+ } finally {
583
+ await lock.release();
584
+ }
585
+ }
586
+ function collectConfigRosterIds(value) {
587
+ if (!isRecord(value) || !Array.isArray(value.accounts))
588
+ return null;
589
+ const ids = new Set;
590
+ for (const account of value.accounts) {
591
+ if (!isRecord(account))
592
+ continue;
593
+ if (typeof account.id !== "string")
594
+ continue;
595
+ const trimmed = account.id.trim();
596
+ if (!trimmed)
597
+ continue;
598
+ ids.add(trimmed);
599
+ }
600
+ return ids;
601
+ }
602
+ function pickRawRosterEntriesForPreservation(rawValue, rawIds, currentAccountIds, allowDrop) {
603
+ if (!isRecord(rawValue) || !Array.isArray(rawValue.accounts))
604
+ return { preservedRawEntries: [], preservedIds: [] };
605
+ const preservedRawEntries = [], preservedIds = [];
606
+ for (const id of rawIds) {
607
+ if (currentAccountIds.has(id))
608
+ continue;
609
+ if (allowDrop.has(id))
610
+ continue;
611
+ const rawEntry = rawValue.accounts.find((acc) => isRecord(acc) && typeof acc.id === "string" && acc.id.trim() === id);
612
+ if (!rawEntry)
613
+ continue;
614
+ preservedRawEntries.push(rawEntry);
615
+ preservedIds.push(id);
616
+ }
617
+ return { preservedRawEntries, preservedIds };
618
+ }
619
+ function collectStringIds(entries) {
620
+ if (!Array.isArray(entries))
621
+ return [];
622
+ const ids = [];
623
+ for (const entry of entries) {
624
+ if (!isRecord(entry))
625
+ continue;
626
+ if (typeof entry.id !== "string")
627
+ continue;
628
+ ids.push(entry.id);
629
+ }
630
+ return ids;
631
+ }
632
+ function buildPreservedAdditions(rawConfigValue, loadedIds, allowDrop) {
633
+ const rawIds = collectConfigRosterIds(rawConfigValue);
634
+ if (rawIds === null)
635
+ return [];
636
+ const { preservedRawEntries, preservedIds } = pickRawRosterEntriesForPreservation(rawConfigValue, rawIds, loadedIds, allowDrop);
637
+ emitRosterDropWarning(preservedIds);
638
+ return preservedRawEntries;
639
+ }
640
+ export async function mutateAccounts(mutate, paths, options = {}) {
641
+ const { configPath: path, statePath } = paths, lock = await acquireSaveAccountsLock(path);
642
+ try {
643
+ const stateLock = await acquireSaveAccountsLock(statePath);
644
+ try {
645
+ const configJson = await readJsonIfPresent(path), stateJson = await readJsonIfPresent(statePath), current = (configJson.exists ? normalizeStorage(mergeConfigAndState(configJson.value, stateJson.value)) : null) ?? emptyAccountStorage(), currentAccountIds = new Set(current.accounts.map((a) => a.id)), rawRosterIds = collectConfigRosterIds(configJson.value) ?? new Set, next = mutate(current, {
646
+ rawRosterIds: [...rawRosterIds],
647
+ loadedRosterIds: [...currentAccountIds]
648
+ }) ?? current, allowDrop = new Set(options.allowDrop ?? []), preserved = buildPreservedAdditions(configJson.value, currentAccountIds, allowDrop), baseConfig = configFromStorage(next), writtenIds = new Set((Array.isArray(baseConfig.accounts) ? baseConfig.accounts : []).map((e) => isRecord(e) && typeof e.id === "string" ? e.id.trim() : "").filter(Boolean)), additions = preserved.filter((raw) => {
649
+ if (!isRecord(raw))
650
+ return !1;
651
+ if (typeof raw.id !== "string")
652
+ return !1;
653
+ return !writtenIds.has(raw.id.trim());
654
+ }), existing = isRecord(configJson.value) ? configJson.value : {}, nextConfig = {
655
+ ...withoutRetiredSettings(existing),
656
+ ...baseConfig,
657
+ accounts: [
658
+ ...Array.isArray(baseConfig.accounts) ? baseConfig.accounts : [],
659
+ ...additions
660
+ ]
661
+ };
662
+ if (options.canWrite && !options.canWrite())
663
+ return current;
664
+ await writeJsonAtomic(path, nextConfig);
665
+ if (options.canWrite && !options.canWrite())
666
+ return current;
667
+ await writeJsonAtomic(statePath, stateFromStorage(next));
668
+ logA.debug("account config written", {
669
+ accountCount: nextConfig.accounts.length,
670
+ accountIds: collectStringIds(nextConfig.accounts)
671
+ });
672
+ return next;
673
+ } finally {
674
+ await stateLock.release();
675
+ }
676
+ } finally {
677
+ await lock.release();
678
+ }
679
+ }
680
+ function emptyAccountStorage() {
681
+ return {
682
+ version: 1,
683
+ main: { type: "opencode", provider: "openai" },
684
+ accounts: []
685
+ };
686
+ }
687
+ function applyMainQuotaStatePatch(state, storage) {
688
+ state.main = state.main ?? {};
689
+ const existingCheckedAt = typeof state.main.quotaCheckedAt === "number" ? state.main.quotaCheckedAt : quotaSnapshotCheckedAt(state.main.quota), incomingCheckedAt = typeof storage.quota?.mainQuotaCheckedAt === "number" ? storage.quota.mainQuotaCheckedAt : quotaSnapshotCheckedAt(storage.quota?.mainQuota);
690
+ if (existingCheckedAt > incomingCheckedAt)
691
+ return;
692
+ state.main.quota = storage.quota?.mainQuota;
693
+ state.main.quotaCheckedAt = storage.quota?.mainQuotaCheckedAt;
694
+ state.main.quotaToken = storage.quota?.mainQuotaToken;
695
+ state.main.lastQuotaApiError = storage.quota?.mainLastQuotaApiError;
696
+ }
697
+ function applyMainRefreshStatePatch(state, storage) {
698
+ state.main = state.main ?? {};
699
+ state.main.lastRefreshError = storage.refresh?.mainLastRefreshError;
700
+ state.main.refreshLeaseId = storage.refresh?.mainRefreshLeaseId;
701
+ state.main.refreshLeaseUntil = storage.refresh?.mainRefreshLeaseUntil;
702
+ state.main.refreshLeaseTokenHash = storage.refresh?.mainRefreshLeaseTokenHash;
703
+ }
704
+ function pruneUndefined(value) {
705
+ if (Array.isArray(value))
706
+ return value.map(pruneUndefined);
707
+ if (!isRecord(value))
708
+ return value;
709
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0).map(([key, entry]) => [key, pruneUndefined(entry)]));
710
+ }
711
+ export async function saveAccountState(storage, paths, scope = {
712
+ mainQuota: !0,
713
+ mainRefresh: !0,
714
+ accounts: !0
715
+ }, canWrite) {
716
+ const statePath = paths.statePath, lock = await acquireSaveAccountsLock(statePath);
717
+ try {
718
+ const existing = (await readJsonIfPresent(statePath)).value, next = isRecord(existing) ? { ...existing, version: 1 } : { version: 1 };
719
+ if (scope.mainQuota)
720
+ applyMainQuotaStatePatch(next, storage);
721
+ if (scope.mainRefresh)
722
+ applyMainRefreshStatePatch(next, storage);
723
+ if (scope.accounts) {
724
+ const ids = scope.accounts === !0 ? null : new Set(scope.accounts), roster = await readConfigRosterIds(paths.configPath);
725
+ next.accounts = { ...isRecord(next.accounts) ? next.accounts : {} };
726
+ for (const account of storage.accounts) {
727
+ if (ids && !ids.has(account.id))
728
+ continue;
729
+ if (roster && !roster.has(account.id))
730
+ continue;
731
+ next.accounts[account.id] = mergeAccountRuntimeState(next.accounts[account.id], accountRuntimeState(account));
732
+ }
733
+ if (ids) {
734
+ for (const id of ids)
735
+ if (!storage.accounts.some((account) => account.id === id))
736
+ delete next.accounts[id];
737
+ }
738
+ if (roster) {
739
+ for (const id of Object.keys(next.accounts))
740
+ if (!roster.has(id))
741
+ delete next.accounts[id];
742
+ }
743
+ }
744
+ if (canWrite && !canWrite())
745
+ return;
746
+ await writeJsonAtomic(statePath, pruneUndefined(next));
747
+ } finally {
748
+ await lock.release();
749
+ }
750
+ }
751
+ export function getFallbackStatuses(storage) {
752
+ return storage?.fallbackOn?.length ? storage.fallbackOn : DEFAULT_FALLBACK_ON;
753
+ }
754
+ export function shouldFallbackStatus(status, storage) {
755
+ return getFallbackStatuses(storage).includes(status);
756
+ }
757
+ function normalizeThresholds(storage) {
758
+ const configured = storage?.quota?.minimumRemaining || {};
759
+ return {
760
+ primary: configured.primary ?? configured["5h"] ?? DEFAULT_MINIMUM_REMAINING.primary,
761
+ secondary: configured.secondary ?? configured["1w"] ?? DEFAULT_MINIMUM_REMAINING.secondary
762
+ };
763
+ }
764
+ function quotaEnabled(storage) {
765
+ return storage?.quota?.enabled !== !1;
766
+ }
767
+ function failClosedOnUnknownQuota(storage) {
768
+ return storage?.quota?.failClosedOnUnknownQuota ?? DEFAULT_FAIL_CLOSED_ON_UNKNOWN_QUOTA;
769
+ }
770
+ export function quotaSnapshotPassesPolicy(quota, storage, now = Date.now()) {
771
+ if (!quotaEnabled(storage))
772
+ return !0;
773
+ if (!quota)
774
+ return !failClosedOnUnknownQuota(storage);
775
+ const thresholds = normalizeThresholds(storage), failClosed = failClosedOnUnknownQuota(storage);
776
+ for (const key of ["primary", "secondary"]) {
777
+ const window = quota[key];
778
+ if (!window)
779
+ continue;
780
+ if (quotaWindowResetIsPast(window, now)) {
781
+ if (failClosed)
782
+ return !1;
783
+ continue;
784
+ }
785
+ if (window.remainingPercent < thresholds[key])
786
+ return !1;
787
+ }
788
+ return !0;
789
+ }
790
+ export function isKillswitchEnabled(storage) {
791
+ return storage?.killswitch?.enabled === !0;
792
+ }
793
+ function normalizeKillswitchThresholds(thresholds) {
794
+ return {
795
+ primary: thresholds?.primary ?? thresholds?.["5h"] ?? DEFAULT_KILLSWITCH_THRESHOLDS.primary,
796
+ secondary: thresholds?.secondary ?? thresholds?.["1w"] ?? DEFAULT_KILLSWITCH_THRESHOLDS.secondary
797
+ };
798
+ }
799
+ export function cacheKeepSettings(storage) {
800
+ return storage?.cacheKeep ?? storage?.cachekeep;
801
+ }
802
+ function floorOf(value) {
803
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
804
+ }
805
+ export function getKillswitchThresholdsForAccount(storage, accountId) {
806
+ if (!storage?.killswitch)
807
+ return DEFAULT_KILLSWITCH_THRESHOLDS;
808
+ if (storage.killswitch.schema === KILLSWITCH_FLOORS_SCHEMA) {
809
+ const { accounts, defaults } = storage.killswitch, id = accountId ?? "main", own = isRecord(accounts) && Object.hasOwn(accounts, id) && isRecord(accounts[id]) ? accounts[id] : isRecord(defaults) ? defaults : void 0;
810
+ return {
811
+ primary: floorOf(own?.primary),
812
+ secondary: floorOf(own?.secondary)
813
+ };
814
+ }
815
+ if (accountId && storage.killswitch.accounts?.[accountId])
816
+ return normalizeKillswitchThresholds(storage.killswitch.accounts[accountId]);
817
+ return normalizeKillswitchThresholds(storage.killswitch.main);
818
+ }
819
+ export function killswitchPassesPolicy(quota, storage, accountId, now = Date.now()) {
820
+ if (!isKillswitchEnabled(storage))
821
+ return !0;
822
+ if (!quota)
823
+ return !failClosedOnUnknownQuota(storage);
824
+ const thresholds = getKillswitchThresholdsForAccount(storage, accountId), failClosed = failClosedOnUnknownQuota(storage);
825
+ for (const key of ["primary", "secondary"]) {
826
+ const window = quota[key];
827
+ if (!window)
828
+ continue;
829
+ if (quotaWindowResetIsPast(window, now)) {
830
+ if (failClosed)
831
+ return !1;
832
+ continue;
833
+ }
834
+ if (window.remainingPercent < thresholds[key])
835
+ return !1;
836
+ }
837
+ return !0;
838
+ }
839
+ export function killswitchRetryAfterSeconds(mainQuota, fallbackAccounts, now, storage) {
840
+ if (!isKillswitchEnabled(storage))
841
+ return 300;
842
+ const accountResetTimes = [], accounts = [
843
+ { accountId: void 0, quota: mainQuota },
844
+ ...fallbackAccounts
845
+ ];
846
+ for (const { accountId, quota } of accounts) {
847
+ if (!quota)
848
+ continue;
849
+ const thresholds = getKillswitchThresholdsForAccount(storage, accountId), violatingResetTimes = [];
850
+ let resetUnknown = !1;
851
+ for (const key of ["primary", "secondary"]) {
852
+ const window = quota[key];
853
+ if (!window || quotaWindowResetIsPast(window, now))
854
+ continue;
855
+ if (window.remainingPercent >= thresholds[key])
856
+ continue;
857
+ const resetTime = window.resetsAt ? Date.parse(window.resetsAt) : NaN;
858
+ if (!Number.isFinite(resetTime) || resetTime <= now) {
859
+ resetUnknown = !0;
860
+ break;
861
+ }
862
+ violatingResetTimes.push(resetTime);
863
+ }
864
+ if (resetUnknown || violatingResetTimes.length === 0)
865
+ continue;
866
+ accountResetTimes.push(Math.max(...violatingResetTimes));
867
+ }
868
+ if (accountResetTimes.length === 0)
869
+ return 300;
870
+ const earliest = Math.min(...accountResetTimes);
871
+ return Math.max(1, Math.ceil((earliest - now) / 1000));
872
+ }
873
+ function isAccountStore(value) {
874
+ return typeof value.version === "number" && Array.isArray(value.accounts);
875
+ }
876
+ export async function migrateIfNeeded(existingToken, paths) {
877
+ const { configPath: path, statePath } = paths;
878
+ if (!existingToken)
879
+ return;
880
+ try {
881
+ const current = await readJsonIfPresent(path);
882
+ if (current.exists && isRecord(current.value) && isAccountStore(current.value))
883
+ return;
884
+ } catch {}
885
+ const lock = await acquireSaveAccountsLock(path);
886
+ try {
887
+ const stateLock = await acquireSaveAccountsLock(statePath);
888
+ try {
889
+ const existing = await readJsonIfPresent(path);
890
+ if (existing.exists && isRecord(existing.value)) {
891
+ if (isAccountStore(existing.value))
892
+ return;
893
+ }
894
+ if (!existingToken)
895
+ return;
896
+ const storage = {
897
+ version: 1,
898
+ main: { type: "opencode", provider: "openai" },
899
+ accounts: []
900
+ };
901
+ if (existingToken.access) {
902
+ const accountId = extractAccountId({
903
+ id_token: "",
904
+ access_token: existingToken.access,
905
+ refresh_token: existingToken.refresh
906
+ });
907
+ if (accountId)
908
+ storage.mainAccountId = accountId;
909
+ }
910
+ const existingFields = existing.exists && isRecord(existing.value) ? existing.value : {}, nextConfig = {
911
+ ...withoutRetiredSettings(existingFields),
912
+ ...configFromStorage(storage)
913
+ };
914
+ await writeJsonAtomic(path, nextConfig);
915
+ await writeJsonAtomic(statePath, stateFromStorage(storage));
916
+ } finally {
917
+ await stateLock.release();
918
+ }
919
+ } finally {
920
+ await lock.release();
921
+ }
922
+ }
923
+ function refreshEnabled(storage) {
924
+ return storage?.refresh?.enabled !== !1;
925
+ }
926
+ export function refreshBeforeExpiryMs(storage) {
927
+ return (storage?.refresh?.refreshBeforeExpiryMinutes ?? 240) * 60000;
928
+ }
929
+ function jitterMs(maxMs) {
930
+ return Math.floor(Math.random() * (maxMs + 1));
931
+ }
932
+ function tokenNeedsRefresh(account, storage, now) {
933
+ return !account.access || !account.expires || account.expires - now <= refreshBeforeExpiryMs(storage);
934
+ }
935
+ function hasUnexpiredAccessToken(account, now) {
936
+ return Boolean(account.access && typeof account.expires === "number" && account.expires > now);
937
+ }
938
+ export function isShieldedMainRow(storage, account) {
939
+ return Boolean(storage?.mainAccountId && account.accountId && account.accountId === storage.mainAccountId);
940
+ }
941
+
942
+ export class ShieldedMainRowRefreshError extends Error {
943
+ constructor(accountId) {
944
+ super(`Refusing to refresh account ${accountId}: it holds the main account's credential while the main slot is live`);
945
+ this.name = "ShieldedMainRowRefreshError";
946
+ }
947
+ }
948
+ export const POOL_MAIN_PLACEHOLDER_REFRESH = "common-auth-placeholder:v1:openai", POOL_MAIN_ROW_ID = "main";
949
+ export function isPoolMainPlaceholder(auth) {
950
+ return isRecord(auth) && auth.type === "oauth" && (auth.refresh === POOL_MAIN_PLACEHOLDER_REFRESH || auth.refresh === HOST_SLOT_TOMBSTONE_REFRESH);
951
+ }
952
+ export function findPoolMainRow(storage) {
953
+ return storage?.accounts.find((account) => account.id === POOL_MAIN_ROW_ID && account.enabled !== !1 && isOAuthAccount(account));
954
+ }
955
+ export function withoutPoolMainRow(storage) {
956
+ if (!storage)
957
+ return storage;
958
+ return {
959
+ ...storage,
960
+ accounts: storage.accounts.filter((account) => account.id !== POOL_MAIN_ROW_ID)
961
+ };
962
+ }
963
+ function updateStoredAccount(storage, account) {
964
+ const idx = storage.accounts.findIndex((candidate) => candidate.id === account.id);
965
+ if (idx !== -1)
966
+ storage.accounts[idx] = account;
967
+ }
968
+ function formatErrorMessage(error) {
969
+ return error instanceof Error ? error.message : String(error);
970
+ }
971
+ function recordRefreshError(account, error, now) {
972
+ if (error instanceof TombstoneRefreshError)
973
+ return;
974
+ account.lastRefreshError = buildRefreshOperationError({
975
+ error,
976
+ now,
977
+ refreshToken: account.refresh,
978
+ previous: account.lastRefreshError
979
+ });
980
+ }
981
+ function recordQuotaRefreshError(account, error, now) {
982
+ if (error instanceof TombstoneRefreshError)
983
+ return;
984
+ account.lastQuotaRefreshError = buildQuotaOperationError({
985
+ error,
986
+ now,
987
+ previous: account.lastQuotaRefreshError
988
+ });
989
+ if (error?.isRefreshError === !0)
990
+ recordRefreshError(account, error, now);
991
+ }
992
+ function cachedQuotaWindowStillRelevant(window, now) {
993
+ if (!window)
994
+ return !1;
995
+ if (!window.resetsAt)
996
+ return !0;
997
+ const resetTime = Date.parse(window.resetsAt);
998
+ return Number.isFinite(resetTime) ? resetTime > now : !0;
999
+ }
1000
+ function cachedQuotaSnapshotStillRelevant(quota, now) {
1001
+ if (!quota)
1002
+ return !1;
1003
+ return cachedQuotaWindowStillRelevant(quota.primary, now) || cachedQuotaWindowStillRelevant(quota.secondary, now);
1004
+ }
1005
+ function quotaSnapshotIsFresh(quota, storage, now) {
1006
+ if (!quota)
1007
+ return !1;
1008
+ const intervalMinutes = storage?.quota?.checkIntervalMinutes ?? 5, staleAfterMs = Math.max(1, intervalMinutes) * 60000, checkedAt = Math.max(quota.primary?.checkedAt ?? 0, quota.secondary?.checkedAt ?? 0);
1009
+ return now - checkedAt < staleAfterMs;
1010
+ }
1011
+ function quotaIsStale(account, storage, now) {
1012
+ return !quotaSnapshotIsFresh(account.quota, storage, now);
1013
+ }
1014
+ function canUseCachedQuotaAfterRefreshError(account, storage, error, now) {
1015
+ return isTransientQuotaError(error) && quotaSnapshotPassesPolicy(account.quota, storage, now) && cachedQuotaSnapshotStillRelevant(account.quota, now);
1016
+ }
1017
+ export function getQuotaCheckIntervalMs(storage) {
1018
+ const minutes = storage?.quota?.checkIntervalMinutes ?? 5;
1019
+ return Math.max(1, minutes) * 60000;
1020
+ }
1021
+ const BACKGROUND_TICK_MS = 60000, BACKGROUND_TICK_JITTER_MS = 60000;
1022
+ export const FALLBACK_REFRESH_LOCK_TTL_MS = 600000;
1023
+ export function fallbackRefreshLockName(accountId) {
1024
+ return `fallback-oauth-refresh-${createHash("sha256").update(accountId).digest("base64url").slice(0, 16)}`;
1025
+ }
1026
+ const FALLBACK_REFRESH_JOIN_WAIT_MS = 1e4, FALLBACK_REFRESH_JOIN_POLL_MS = 100;
1027
+
1028
+ export class FallbackAccountManager {
1029
+ now;
1030
+ fetchImpl;
1031
+ paths;
1032
+ refreshPromises = new Map;
1033
+ refreshTimer = null;
1034
+ quotaTimer = null;
1035
+ quotaManager;
1036
+ onFallbackStorageChanged;
1037
+ options;
1038
+ constructor(options) {
1039
+ this.options = options;
1040
+ this.now = options.now ?? Date.now;
1041
+ this.fetchImpl = options.fetchImpl ?? fetch;
1042
+ this.paths = options.paths;
1043
+ this.quotaManager = options.quotaManager ?? null;
1044
+ this.onFallbackStorageChanged = options.onFallbackStorageChanged;
1045
+ }
1046
+ assertNotTombstoned(account) {
1047
+ if (account)
1048
+ assertNotTombstoneRefresh(account.refresh);
1049
+ }
1050
+ seedFallbackQuota(account, storage) {
1051
+ if (!this.quotaManager)
1052
+ return;
1053
+ if (!account.quota)
1054
+ return;
1055
+ const checkedAt = Math.max(account.quota.primary?.checkedAt ?? 0, account.quota.secondary?.checkedAt ?? 0);
1056
+ if (checkedAt <= 0)
1057
+ return;
1058
+ const existing = this.quotaManager.getFallback(account.id, account.access);
1059
+ if (existing && existing.checkedAt >= checkedAt)
1060
+ return;
1061
+ this.quotaManager.setFallback(account.id, {
1062
+ quota: account.quota,
1063
+ refreshAfter: checkedAt + getQuotaCheckIntervalMs(storage),
1064
+ checkedAt
1065
+ }, account.access, !1, account.accountId);
1066
+ }
1067
+ async load() {
1068
+ return loadAccounts(this.paths);
1069
+ }
1070
+ selectionBookkeeping = Promise.resolve();
1071
+ selectionBookkeepingSettled() {
1072
+ return this.selectionBookkeeping;
1073
+ }
1074
+ async save(storage, accountIds) {
1075
+ await saveAccountState(storage, this.paths, {
1076
+ accounts: accountIds ?? !0
1077
+ }, this.options.canWrite);
1078
+ }
1079
+ startBackgroundRefresh() {
1080
+ const run = async () => {
1081
+ if (await this.options.backgroundRefreshPaused?.())
1082
+ return;
1083
+ await this.refreshDueAccounts();
1084
+ if (this.options.fetchQuotaFn)
1085
+ await this.refreshQuotaForDueAccounts();
1086
+ };
1087
+ run().catch(() => {});
1088
+ if (!this.refreshTimer) {
1089
+ this.refreshTimer = setInterval(() => {
1090
+ run().catch(() => {});
1091
+ }, BACKGROUND_TICK_MS + jitterMs(BACKGROUND_TICK_JITTER_MS));
1092
+ if ("unref" in this.refreshTimer)
1093
+ this.refreshTimer.unref();
1094
+ }
1095
+ }
1096
+ stopBackgroundRefresh() {
1097
+ if (this.refreshTimer)
1098
+ clearInterval(this.refreshTimer);
1099
+ if (this.quotaTimer)
1100
+ clearInterval(this.quotaTimer);
1101
+ this.refreshTimer = null;
1102
+ this.quotaTimer = null;
1103
+ }
1104
+ async getUsableFallbackAccounts(existingStorage) {
1105
+ const storage = existingStorage !== void 0 ? existingStorage : await this.load();
1106
+ if (!storage)
1107
+ return [];
1108
+ const usable = [];
1109
+ let changed = !1;
1110
+ for (const account of storage.accounts) {
1111
+ if (account.enabled === !1 || !isOAuthAccount(account))
1112
+ continue;
1113
+ if (isShieldedMainRow(storage, account))
1114
+ continue;
1115
+ if (isTombstoned(account))
1116
+ continue;
1117
+ let refreshFailed = !1, candidate = account;
1118
+ try {
1119
+ if (tokenNeedsRefresh(candidate, storage, this.now())) {
1120
+ const refreshError = candidate.lastRefreshError;
1121
+ if (refreshError && refreshBackoffActive(refreshError, candidate.refresh, this.now())) {
1122
+ refreshFailed = !0;
1123
+ throw Error(formatRefreshBackoffMessage(refreshError, this.now()));
1124
+ }
1125
+ try {
1126
+ candidate = await this.refreshAccount(candidate, storage);
1127
+ changed = !0;
1128
+ } catch (error) {
1129
+ if (isAccountRemovedDuringRefreshError(error))
1130
+ continue;
1131
+ refreshFailed = !0;
1132
+ const stored = storage.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
1133
+ if (stored && !refreshBackoffActive(stored.lastRefreshError, stored.refresh, this.now())) {
1134
+ recordRefreshError(stored, error, this.now());
1135
+ updateStoredAccount(storage, stored);
1136
+ changed = !0;
1137
+ }
1138
+ throw error;
1139
+ }
1140
+ }
1141
+ this.seedFallbackQuota(candidate, storage);
1142
+ if (this.accountPassesQuotaPolicy(this.quotaPolicyAccount(candidate), storage))
1143
+ usable.push(candidate);
1144
+ } catch (error) {
1145
+ const hasUsableCandidateToken = hasUnexpiredAccessToken(candidate, this.now());
1146
+ if (refreshFailed) {
1147
+ if (!hasUsableCandidateToken)
1148
+ continue;
1149
+ } else if (!hasUsableCandidateToken)
1150
+ continue;
1151
+ if (canUseCachedQuotaAfterRefreshError(candidate, storage, error, this.now())) {
1152
+ logR.debug("fallback quota using cached quota after refresh error", {
1153
+ pid: process.pid,
1154
+ accountId: candidate.id,
1155
+ error: formatErrorMessage(error)
1156
+ });
1157
+ usable.push(candidate);
1158
+ } else if (!failClosedOnUnknownQuota(storage))
1159
+ usable.push(candidate);
1160
+ }
1161
+ }
1162
+ if (changed) {
1163
+ const snapshot = structuredClone(storage);
1164
+ this.selectionBookkeeping = this.selectionBookkeeping.then(() => this.save(snapshot)).catch((error) => {
1165
+ logA.warn("fallback selection bookkeeping not persisted", {
1166
+ pid: process.pid,
1167
+ error: formatErrorMessage(error)
1168
+ });
1169
+ });
1170
+ }
1171
+ return usable;
1172
+ }
1173
+ async markUsed(account) {
1174
+ try {
1175
+ const storage = await this.load();
1176
+ if (!storage)
1177
+ return;
1178
+ const stored = storage.accounts.find((candidate) => candidate.id === account.id);
1179
+ if (!stored)
1180
+ return;
1181
+ stored.lastUsed = this.now();
1182
+ await this.save(storage, [stored.id]);
1183
+ } catch (error) {
1184
+ logA.warn("lastUsed stamp not persisted", {
1185
+ pid: process.pid,
1186
+ accountId: account.id,
1187
+ error: formatErrorMessage(error)
1188
+ });
1189
+ }
1190
+ }
1191
+ accountPassesQuotaPolicy(account, storage) {
1192
+ return quotaSnapshotPassesPolicy(account.quota, storage, this.now());
1193
+ }
1194
+ quotaPolicyAccount(account) {
1195
+ if (!this.quotaManager)
1196
+ return account;
1197
+ const cached = this.quotaManager.getFallback(account.id, account.access)?.quota;
1198
+ return cached ? { ...account, quota: cached } : account;
1199
+ }
1200
+ async refreshDueAccounts() {
1201
+ const storage = await this.load();
1202
+ if (!storage || !refreshEnabled(storage))
1203
+ return;
1204
+ let changed = !1;
1205
+ for (const account of storage.accounts) {
1206
+ if (account.enabled === !1 || !isOAuthAccount(account))
1207
+ continue;
1208
+ if (isShieldedMainRow(storage, account))
1209
+ continue;
1210
+ if (isTombstoned(account))
1211
+ continue;
1212
+ if (!tokenNeedsRefresh(account, storage, this.now()))
1213
+ continue;
1214
+ if (refreshBackoffActive(account.lastRefreshError, account.refresh, this.now()))
1215
+ continue;
1216
+ try {
1217
+ logR.debug("fallback oauth background due", {
1218
+ pid: process.pid,
1219
+ accountId: account.id,
1220
+ expiresInMs: account.expires ? account.expires - this.now() : void 0
1221
+ });
1222
+ await this.refreshAccount(account, storage);
1223
+ changed = !0;
1224
+ } catch (error) {
1225
+ logR.warn("fallback oauth background failed", {
1226
+ pid: process.pid,
1227
+ accountId: account.id,
1228
+ error: formatErrorMessage(error)
1229
+ });
1230
+ recordRefreshError(account, error, this.now());
1231
+ updateStoredAccount(storage, account);
1232
+ changed = !0;
1233
+ }
1234
+ }
1235
+ if (changed)
1236
+ await this.save(storage);
1237
+ }
1238
+ async refreshQuotaForDueAccounts() {
1239
+ const storage = await this.load();
1240
+ if (!storage || !quotaEnabled(storage))
1241
+ return;
1242
+ if (!this.options.fetchQuotaFn)
1243
+ return;
1244
+ let changed = !1;
1245
+ for (const account of storage.accounts) {
1246
+ if (account.enabled === !1 || !isOAuthAccount(account))
1247
+ continue;
1248
+ if (isShieldedMainRow(storage, account))
1249
+ continue;
1250
+ if (isTombstoned(account))
1251
+ continue;
1252
+ let next = account;
1253
+ try {
1254
+ if (tokenNeedsRefresh(next, storage, this.now())) {
1255
+ if (refreshBackoffActive(next.lastRefreshError, next.refresh, this.now()))
1256
+ continue;
1257
+ next = await this.refreshAccount(next, storage);
1258
+ changed = !0;
1259
+ }
1260
+ if (quotaBackoffActive(next.lastQuotaRefreshError, this.now()))
1261
+ continue;
1262
+ this.seedFallbackQuota(next, storage);
1263
+ if (!(this.quotaManager ? this.quotaManager.isFallbackStale(next.id, next.access) : quotaIsStale(next, storage, this.now())))
1264
+ continue;
1265
+ await this.refreshAccountQuota(next, storage);
1266
+ changed = !0;
1267
+ } catch (error) {
1268
+ recordQuotaRefreshError(account, error, this.now());
1269
+ updateStoredAccount(storage, account);
1270
+ changed = !0;
1271
+ }
1272
+ }
1273
+ if (changed) {
1274
+ await this.save(storage);
1275
+ this.onFallbackStorageChanged?.();
1276
+ }
1277
+ }
1278
+ async refreshQuotaForAllAccounts(options = {}) {
1279
+ const storage = await this.load(), errors = [];
1280
+ if (!storage || !quotaEnabled(storage))
1281
+ return { storage, errors };
1282
+ if (!this.options.fetchQuotaFn)
1283
+ return { storage, errors };
1284
+ const force = options.force ?? !1;
1285
+ let changed = !1;
1286
+ for (const account of storage.accounts) {
1287
+ if (account.enabled === !1 || !isOAuthAccount(account))
1288
+ continue;
1289
+ if (isShieldedMainRow(storage, account))
1290
+ continue;
1291
+ if (isTombstoned(account))
1292
+ continue;
1293
+ let next = account;
1294
+ try {
1295
+ if (tokenNeedsRefresh(next, storage, this.now())) {
1296
+ const refreshError = next.lastRefreshError;
1297
+ if (refreshError && refreshBackoffActive(refreshError, next.refresh, this.now()))
1298
+ throw Error(formatRefreshBackoffMessage(refreshError, this.now()));
1299
+ next = await this.refreshAccount(next, storage);
1300
+ changed = !0;
1301
+ }
1302
+ if (!force && !quotaIsStale(next, storage, this.now())) {
1303
+ if (next.lastQuotaRefreshError) {
1304
+ next.lastQuotaRefreshError = void 0;
1305
+ updateStoredAccount(storage, next);
1306
+ changed = !0;
1307
+ }
1308
+ continue;
1309
+ }
1310
+ await this.refreshAccountQuota(next, storage);
1311
+ changed = !0;
1312
+ } catch (error) {
1313
+ recordQuotaRefreshError(account, error, this.now());
1314
+ updateStoredAccount(storage, account);
1315
+ changed = !0;
1316
+ errors.push({
1317
+ accountId: account.id,
1318
+ message: formatErrorMessage(error)
1319
+ });
1320
+ }
1321
+ }
1322
+ if (changed)
1323
+ await this.save(storage);
1324
+ return { storage, errors };
1325
+ }
1326
+ async refreshAccount(account, storage, options = {}) {
1327
+ const existing = this.refreshPromises.get(account.id);
1328
+ if (existing) {
1329
+ const refreshed = await existing;
1330
+ updateStoredAccount(storage, refreshed);
1331
+ return refreshed;
1332
+ }
1333
+ const promise = this.refreshAccountNow(account, storage, options).finally(() => {
1334
+ this.refreshPromises.delete(account.id);
1335
+ });
1336
+ this.refreshPromises.set(account.id, promise);
1337
+ const refreshed = await promise;
1338
+ updateStoredAccount(storage, refreshed);
1339
+ return refreshed;
1340
+ }
1341
+ async waitForConcurrentFallbackRefresh(account, storage, previous, options) {
1342
+ const deadline = Date.now() + FALLBACK_REFRESH_JOIN_WAIT_MS;
1343
+ while (Date.now() < deadline) {
1344
+ await new Promise((resolve) => setTimeout(resolve, FALLBACK_REFRESH_JOIN_POLL_MS));
1345
+ const latestStorage = await this.load(), latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
1346
+ if (!latestAccount)
1347
+ continue;
1348
+ this.assertNotTombstoned(latestAccount);
1349
+ if ((latestAccount.access !== previous.access || latestAccount.refresh !== previous.refresh || (latestAccount.expires ?? 0) > (previous.expires ?? 0) + 60000) && (options.force || !tokenNeedsRefresh(latestAccount, latestStorage, this.now()))) {
1350
+ updateStoredAccount(storage, latestAccount);
1351
+ logR.debug("fallback oauth joined concurrent refresh", {
1352
+ pid: process.pid,
1353
+ accountId: latestAccount.id,
1354
+ expiresInMs: latestAccount.expires ? latestAccount.expires - this.now() : void 0
1355
+ });
1356
+ return latestAccount;
1357
+ }
1358
+ const refreshError = latestAccount.lastRefreshError;
1359
+ if (refreshError && refreshBackoffActive(refreshError, latestAccount.refresh, this.now())) {
1360
+ updateStoredAccount(storage, latestAccount);
1361
+ throw Error(formatRefreshBackoffMessage(refreshError, this.now()));
1362
+ }
1363
+ }
1364
+ return null;
1365
+ }
1366
+ assertNotShieldedMainRow(storage, account, options) {
1367
+ if (options.asPoolMain || !account)
1368
+ return;
1369
+ if (isShieldedMainRow(storage, account))
1370
+ throw new ShieldedMainRowRefreshError(account.id);
1371
+ }
1372
+ async refreshAccountNow(account, storage, options) {
1373
+ let latestStorage = await this.load(), latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
1374
+ this.assertNotTombstoned(latestAccount);
1375
+ this.assertNotShieldedMainRow(latestStorage, latestAccount, options);
1376
+ if (latestAccount && !options.force && !tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
1377
+ updateStoredAccount(storage, latestAccount);
1378
+ return latestAccount;
1379
+ }
1380
+ let sourceAccount = latestAccount ?? account;
1381
+ const fileLock = await acquireRefreshFileLock({
1382
+ name: fallbackRefreshLockName(sourceAccount.id),
1383
+ ttlMs: FALLBACK_REFRESH_LOCK_TTL_MS,
1384
+ path: this.paths.configPath,
1385
+ renew: !0
1386
+ });
1387
+ if (!fileLock) {
1388
+ logR.debug("fallback oauth refresh skipped file lock", {
1389
+ pid: process.pid,
1390
+ accountId: sourceAccount.id
1391
+ });
1392
+ const concurrent = await this.waitForConcurrentFallbackRefresh(account, storage, sourceAccount, options);
1393
+ if (concurrent)
1394
+ return concurrent;
1395
+ throw Error("Fallback OAuth refresh is already in progress");
1396
+ }
1397
+ try {
1398
+ latestStorage = await this.load();
1399
+ latestAccount = latestStorage?.accounts.find((candidate) => candidate.id === account.id && isOAuthAccount(candidate));
1400
+ this.assertNotTombstoned(latestAccount);
1401
+ this.assertNotShieldedMainRow(latestStorage, latestAccount, options);
1402
+ if (latestAccount && !options.force && !tokenNeedsRefresh(latestAccount, latestStorage, this.now())) {
1403
+ updateStoredAccount(storage, latestAccount);
1404
+ return latestAccount;
1405
+ }
1406
+ if (!latestAccount)
1407
+ throw new AccountRemovedDuringRefreshError(account.id);
1408
+ sourceAccount = latestAccount;
1409
+ const providerRefreshFn = this.options.refreshFn ?? (async () => {
1410
+ throw Error("No refreshFn injected into FallbackAccountManager");
1411
+ });
1412
+ logR.debug("fallback oauth refresh request start", {
1413
+ pid: process.pid,
1414
+ accountId: sourceAccount.id,
1415
+ force: options.force === !0,
1416
+ expiresInMs: sourceAccount.expires ? sourceAccount.expires - this.now() : void 0
1417
+ });
1418
+ const refreshed = await providerRefreshFn({
1419
+ refreshToken: sourceAccount.refresh,
1420
+ fetchImpl: this.fetchImpl,
1421
+ now: this.now
1422
+ });
1423
+ sourceAccount.access = refreshed.access;
1424
+ sourceAccount.refresh = refreshed.refresh;
1425
+ sourceAccount.expires = refreshed.expires;
1426
+ sourceAccount.lastRefreshedAt = refreshed.expires - refreshed.expiresIn * 1000;
1427
+ sourceAccount.lastRefreshError = void 0;
1428
+ updateStoredAccount(storage, sourceAccount);
1429
+ await this.save(storage);
1430
+ if (!(await this.load())?.accounts.some((candidate) => candidate.id === account.id))
1431
+ throw new AccountRemovedDuringRefreshError(account.id);
1432
+ logR.debug("fallback oauth refresh succeeded", {
1433
+ pid: process.pid,
1434
+ accountId: sourceAccount.id,
1435
+ expiresInMs: sourceAccount.expires ? sourceAccount.expires - this.now() : void 0
1436
+ });
1437
+ return sourceAccount;
1438
+ } finally {
1439
+ await fileLock.release();
1440
+ }
1441
+ }
1442
+ async refreshAccountQuota(account, storage) {
1443
+ const target = account;
1444
+ this.assertNotTombstoned(target);
1445
+ if (!target.access)
1446
+ throw Error(`Fallback account ${account.id} has no access token`);
1447
+ if (!this.options.fetchQuotaFn)
1448
+ throw Error("No fetchQuotaFn injected \u2014 wham/usage supplement is disabled");
1449
+ const snapshotFn = this.options.fetchQuotaFn ?? (async () => {
1450
+ throw Error("No fetchQuotaFn injected \u2014 wham/usage supplement is disabled");
1451
+ }), fetchSnapshot = (accessToken) => this.quotaManager ? this.quotaManager.refreshFallback(target.id, accessToken) : snapshotFn({
1452
+ accessToken,
1453
+ fetchImpl: this.fetchImpl,
1454
+ now: this.now
1455
+ });
1456
+ try {
1457
+ if (!target.access)
1458
+ throw Error(`Fallback account ${account.id} has no access token`);
1459
+ const quota = await fetchSnapshot(target.access);
1460
+ target.quota = quota;
1461
+ target.lastQuotaRefreshError = void 0;
1462
+ updateStoredAccount(storage, target);
1463
+ await this.save(storage);
1464
+ } catch (error) {
1465
+ recordQuotaRefreshError(account, error, this.now());
1466
+ updateStoredAccount(storage, account);
1467
+ throw error;
1468
+ }
1469
+ }
1470
+ }