codex-multi-auth 0.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 (327) hide show
  1. package/LICENSE +1 -0
  2. package/README.md +162 -0
  3. package/assets/opencode-logo-ornate-dark.svg +18 -0
  4. package/assets/readme-hero.svg +31 -0
  5. package/config/README.md +87 -0
  6. package/config/minimal-opencode.json +13 -0
  7. package/config/opencode-legacy.json +571 -0
  8. package/config/opencode-modern.json +239 -0
  9. package/dist/index.d.ts +45 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +3160 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/lib/accounts/rate-limits.d.ts +22 -0
  14. package/dist/lib/accounts/rate-limits.d.ts.map +1 -0
  15. package/dist/lib/accounts/rate-limits.js +63 -0
  16. package/dist/lib/accounts/rate-limits.js.map +1 -0
  17. package/dist/lib/accounts.d.ts +95 -0
  18. package/dist/lib/accounts.d.ts.map +1 -0
  19. package/dist/lib/accounts.js +668 -0
  20. package/dist/lib/accounts.js.map +1 -0
  21. package/dist/lib/audit.d.ts +45 -0
  22. package/dist/lib/audit.d.ts.map +1 -0
  23. package/dist/lib/audit.js +131 -0
  24. package/dist/lib/audit.js.map +1 -0
  25. package/dist/lib/auth/auth.d.ts +56 -0
  26. package/dist/lib/auth/auth.d.ts.map +1 -0
  27. package/dist/lib/auth/auth.js +214 -0
  28. package/dist/lib/auth/auth.js.map +1 -0
  29. package/dist/lib/auth/browser.d.ts +34 -0
  30. package/dist/lib/auth/browser.d.ts.map +1 -0
  31. package/dist/lib/auth/browser.js +185 -0
  32. package/dist/lib/auth/browser.js.map +1 -0
  33. package/dist/lib/auth/server.d.ts +24 -0
  34. package/dist/lib/auth/server.d.ts.map +1 -0
  35. package/dist/lib/auth/server.js +116 -0
  36. package/dist/lib/auth/server.js.map +1 -0
  37. package/dist/lib/auth/token-utils.d.ts +59 -0
  38. package/dist/lib/auth/token-utils.d.ts.map +1 -0
  39. package/dist/lib/auth/token-utils.js +331 -0
  40. package/dist/lib/auth/token-utils.js.map +1 -0
  41. package/dist/lib/auth-rate-limit.d.ts +20 -0
  42. package/dist/lib/auth-rate-limit.d.ts.map +1 -0
  43. package/dist/lib/auth-rate-limit.js +91 -0
  44. package/dist/lib/auth-rate-limit.js.map +1 -0
  45. package/dist/lib/auto-update-checker.d.ts +10 -0
  46. package/dist/lib/auto-update-checker.d.ts.map +1 -0
  47. package/dist/lib/auto-update-checker.js +216 -0
  48. package/dist/lib/auto-update-checker.js.map +1 -0
  49. package/dist/lib/capability-policy.d.ts +18 -0
  50. package/dist/lib/capability-policy.d.ts.map +1 -0
  51. package/dist/lib/capability-policy.js +150 -0
  52. package/dist/lib/capability-policy.js.map +1 -0
  53. package/dist/lib/circuit-breaker.d.ts +34 -0
  54. package/dist/lib/circuit-breaker.d.ts.map +1 -0
  55. package/dist/lib/circuit-breaker.js +124 -0
  56. package/dist/lib/circuit-breaker.js.map +1 -0
  57. package/dist/lib/cli.d.ts +64 -0
  58. package/dist/lib/cli.d.ts.map +1 -0
  59. package/dist/lib/cli.js +274 -0
  60. package/dist/lib/cli.js.map +1 -0
  61. package/dist/lib/codex-cli/observability.d.ts +22 -0
  62. package/dist/lib/codex-cli/observability.d.ts.map +1 -0
  63. package/dist/lib/codex-cli/observability.js +36 -0
  64. package/dist/lib/codex-cli/observability.js.map +1 -0
  65. package/dist/lib/codex-cli/state.d.ts +86 -0
  66. package/dist/lib/codex-cli/state.d.ts.map +1 -0
  67. package/dist/lib/codex-cli/state.js +470 -0
  68. package/dist/lib/codex-cli/state.js.map +1 -0
  69. package/dist/lib/codex-cli/sync.d.ts +27 -0
  70. package/dist/lib/codex-cli/sync.d.ts.map +1 -0
  71. package/dist/lib/codex-cli/sync.js +325 -0
  72. package/dist/lib/codex-cli/sync.js.map +1 -0
  73. package/dist/lib/codex-cli/writer.d.ts +12 -0
  74. package/dist/lib/codex-cli/writer.d.ts.map +1 -0
  75. package/dist/lib/codex-cli/writer.js +388 -0
  76. package/dist/lib/codex-cli/writer.js.map +1 -0
  77. package/dist/lib/codex-manager.d.ts +2 -0
  78. package/dist/lib/codex-manager.d.ts.map +1 -0
  79. package/dist/lib/codex-manager.js +4841 -0
  80. package/dist/lib/codex-manager.js.map +1 -0
  81. package/dist/lib/config.d.ts +269 -0
  82. package/dist/lib/config.d.ts.map +1 -0
  83. package/dist/lib/config.js +789 -0
  84. package/dist/lib/config.js.map +1 -0
  85. package/dist/lib/constants.d.ts +78 -0
  86. package/dist/lib/constants.d.ts.map +1 -0
  87. package/dist/lib/constants.js +78 -0
  88. package/dist/lib/constants.js.map +1 -0
  89. package/dist/lib/context-overflow.d.ts +27 -0
  90. package/dist/lib/context-overflow.d.ts.map +1 -0
  91. package/dist/lib/context-overflow.js +124 -0
  92. package/dist/lib/context-overflow.js.map +1 -0
  93. package/dist/lib/dashboard-settings.d.ts +90 -0
  94. package/dist/lib/dashboard-settings.d.ts.map +1 -0
  95. package/dist/lib/dashboard-settings.js +327 -0
  96. package/dist/lib/dashboard-settings.js.map +1 -0
  97. package/dist/lib/entitlement-cache.d.ts +41 -0
  98. package/dist/lib/entitlement-cache.d.ts.map +1 -0
  99. package/dist/lib/entitlement-cache.js +137 -0
  100. package/dist/lib/entitlement-cache.js.map +1 -0
  101. package/dist/lib/errors.d.ts +113 -0
  102. package/dist/lib/errors.d.ts.map +1 -0
  103. package/dist/lib/errors.js +103 -0
  104. package/dist/lib/errors.js.map +1 -0
  105. package/dist/lib/forecast.d.ts +42 -0
  106. package/dist/lib/forecast.d.ts.map +1 -0
  107. package/dist/lib/forecast.js +256 -0
  108. package/dist/lib/forecast.js.map +1 -0
  109. package/dist/lib/health.d.ts +33 -0
  110. package/dist/lib/health.d.ts.map +1 -0
  111. package/dist/lib/health.js +70 -0
  112. package/dist/lib/health.js.map +1 -0
  113. package/dist/lib/index.d.ts +32 -0
  114. package/dist/lib/index.d.ts.map +1 -0
  115. package/dist/lib/index.js +32 -0
  116. package/dist/lib/index.js.map +1 -0
  117. package/dist/lib/live-account-sync.d.ts +39 -0
  118. package/dist/lib/live-account-sync.d.ts.map +1 -0
  119. package/dist/lib/live-account-sync.js +196 -0
  120. package/dist/lib/live-account-sync.js.map +1 -0
  121. package/dist/lib/logger.d.ts +40 -0
  122. package/dist/lib/logger.d.ts.map +1 -0
  123. package/dist/lib/logger.js +364 -0
  124. package/dist/lib/logger.js.map +1 -0
  125. package/dist/lib/oauth-success.html +338 -0
  126. package/dist/lib/parallel-probe.d.ts +28 -0
  127. package/dist/lib/parallel-probe.d.ts.map +1 -0
  128. package/dist/lib/parallel-probe.js +97 -0
  129. package/dist/lib/parallel-probe.js.map +1 -0
  130. package/dist/lib/preemptive-quota-scheduler.d.ts +53 -0
  131. package/dist/lib/preemptive-quota-scheduler.d.ts.map +1 -0
  132. package/dist/lib/preemptive-quota-scheduler.js +220 -0
  133. package/dist/lib/preemptive-quota-scheduler.js.map +1 -0
  134. package/dist/lib/proactive-refresh.d.ts +66 -0
  135. package/dist/lib/proactive-refresh.d.ts.map +1 -0
  136. package/dist/lib/proactive-refresh.js +143 -0
  137. package/dist/lib/proactive-refresh.js.map +1 -0
  138. package/dist/lib/prompts/codex-opencode-bridge.d.ts +19 -0
  139. package/dist/lib/prompts/codex-opencode-bridge.d.ts.map +1 -0
  140. package/dist/lib/prompts/codex-opencode-bridge.js +169 -0
  141. package/dist/lib/prompts/codex-opencode-bridge.js.map +1 -0
  142. package/dist/lib/prompts/codex.d.ts +41 -0
  143. package/dist/lib/prompts/codex.d.ts.map +1 -0
  144. package/dist/lib/prompts/codex.js +383 -0
  145. package/dist/lib/prompts/codex.js.map +1 -0
  146. package/dist/lib/prompts/opencode-codex.d.ts +25 -0
  147. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -0
  148. package/dist/lib/prompts/opencode-codex.js +270 -0
  149. package/dist/lib/prompts/opencode-codex.js.map +1 -0
  150. package/dist/lib/quota-cache.d.ts +68 -0
  151. package/dist/lib/quota-cache.d.ts.map +1 -0
  152. package/dist/lib/quota-cache.js +224 -0
  153. package/dist/lib/quota-cache.js.map +1 -0
  154. package/dist/lib/quota-probe.d.ts +49 -0
  155. package/dist/lib/quota-probe.d.ts.map +1 -0
  156. package/dist/lib/quota-probe.js +368 -0
  157. package/dist/lib/quota-probe.js.map +1 -0
  158. package/dist/lib/recovery/constants.d.ts +12 -0
  159. package/dist/lib/recovery/constants.d.ts.map +1 -0
  160. package/dist/lib/recovery/constants.js +31 -0
  161. package/dist/lib/recovery/constants.js.map +1 -0
  162. package/dist/lib/recovery/index.d.ts +12 -0
  163. package/dist/lib/recovery/index.d.ts.map +1 -0
  164. package/dist/lib/recovery/index.js +12 -0
  165. package/dist/lib/recovery/index.js.map +1 -0
  166. package/dist/lib/recovery/storage.d.ts +24 -0
  167. package/dist/lib/recovery/storage.d.ts.map +1 -0
  168. package/dist/lib/recovery/storage.js +362 -0
  169. package/dist/lib/recovery/storage.js.map +1 -0
  170. package/dist/lib/recovery/types.d.ts +116 -0
  171. package/dist/lib/recovery/types.d.ts.map +1 -0
  172. package/dist/lib/recovery/types.js +7 -0
  173. package/dist/lib/recovery/types.js.map +1 -0
  174. package/dist/lib/recovery.d.ts +31 -0
  175. package/dist/lib/recovery.d.ts.map +1 -0
  176. package/dist/lib/recovery.js +313 -0
  177. package/dist/lib/recovery.js.map +1 -0
  178. package/dist/lib/refresh-guardian.d.ts +31 -0
  179. package/dist/lib/refresh-guardian.d.ts.map +1 -0
  180. package/dist/lib/refresh-guardian.js +151 -0
  181. package/dist/lib/refresh-guardian.js.map +1 -0
  182. package/dist/lib/refresh-lease.d.ts +37 -0
  183. package/dist/lib/refresh-lease.d.ts.map +1 -0
  184. package/dist/lib/refresh-lease.js +335 -0
  185. package/dist/lib/refresh-lease.js.map +1 -0
  186. package/dist/lib/refresh-queue.d.ts +117 -0
  187. package/dist/lib/refresh-queue.d.ts.map +1 -0
  188. package/dist/lib/refresh-queue.js +297 -0
  189. package/dist/lib/refresh-queue.js.map +1 -0
  190. package/dist/lib/request/failure-policy.d.ts +42 -0
  191. package/dist/lib/request/failure-policy.d.ts.map +1 -0
  192. package/dist/lib/request/failure-policy.js +133 -0
  193. package/dist/lib/request/failure-policy.js.map +1 -0
  194. package/dist/lib/request/fetch-helpers.d.ts +152 -0
  195. package/dist/lib/request/fetch-helpers.d.ts.map +1 -0
  196. package/dist/lib/request/fetch-helpers.js +704 -0
  197. package/dist/lib/request/fetch-helpers.js.map +1 -0
  198. package/dist/lib/request/helpers/input-utils.d.ts +7 -0
  199. package/dist/lib/request/helpers/input-utils.d.ts.map +1 -0
  200. package/dist/lib/request/helpers/input-utils.js +214 -0
  201. package/dist/lib/request/helpers/input-utils.js.map +1 -0
  202. package/dist/lib/request/helpers/model-map.d.ts +28 -0
  203. package/dist/lib/request/helpers/model-map.d.ts.map +1 -0
  204. package/dist/lib/request/helpers/model-map.js +133 -0
  205. package/dist/lib/request/helpers/model-map.js.map +1 -0
  206. package/dist/lib/request/helpers/tool-utils.d.ts +29 -0
  207. package/dist/lib/request/helpers/tool-utils.d.ts.map +1 -0
  208. package/dist/lib/request/helpers/tool-utils.js +117 -0
  209. package/dist/lib/request/helpers/tool-utils.js.map +1 -0
  210. package/dist/lib/request/rate-limit-backoff.d.ts +17 -0
  211. package/dist/lib/request/rate-limit-backoff.d.ts.map +1 -0
  212. package/dist/lib/request/rate-limit-backoff.js +83 -0
  213. package/dist/lib/request/rate-limit-backoff.js.map +1 -0
  214. package/dist/lib/request/request-transformer.d.ts +107 -0
  215. package/dist/lib/request/request-transformer.d.ts.map +1 -0
  216. package/dist/lib/request/request-transformer.js +814 -0
  217. package/dist/lib/request/request-transformer.js.map +1 -0
  218. package/dist/lib/request/response-handler.d.ts +23 -0
  219. package/dist/lib/request/response-handler.d.ts.map +1 -0
  220. package/dist/lib/request/response-handler.js +155 -0
  221. package/dist/lib/request/response-handler.js.map +1 -0
  222. package/dist/lib/request/stream-failover.d.ts +21 -0
  223. package/dist/lib/request/stream-failover.d.ts.map +1 -0
  224. package/dist/lib/request/stream-failover.js +204 -0
  225. package/dist/lib/request/stream-failover.js.map +1 -0
  226. package/dist/lib/rotation.d.ts +146 -0
  227. package/dist/lib/rotation.d.ts.map +1 -0
  228. package/dist/lib/rotation.js +321 -0
  229. package/dist/lib/rotation.js.map +1 -0
  230. package/dist/lib/runtime-paths.d.ts +58 -0
  231. package/dist/lib/runtime-paths.d.ts.map +1 -0
  232. package/dist/lib/runtime-paths.js +164 -0
  233. package/dist/lib/runtime-paths.js.map +1 -0
  234. package/dist/lib/schemas.d.ts +435 -0
  235. package/dist/lib/schemas.d.ts.map +1 -0
  236. package/dist/lib/schemas.js +268 -0
  237. package/dist/lib/schemas.js.map +1 -0
  238. package/dist/lib/session-affinity.d.ts +23 -0
  239. package/dist/lib/session-affinity.d.ts.map +1 -0
  240. package/dist/lib/session-affinity.js +127 -0
  241. package/dist/lib/session-affinity.js.map +1 -0
  242. package/dist/lib/shutdown.d.ts +7 -0
  243. package/dist/lib/shutdown.d.ts.map +1 -0
  244. package/dist/lib/shutdown.js +43 -0
  245. package/dist/lib/shutdown.js.map +1 -0
  246. package/dist/lib/storage/migrations.d.ts +59 -0
  247. package/dist/lib/storage/migrations.d.ts.map +1 -0
  248. package/dist/lib/storage/migrations.js +41 -0
  249. package/dist/lib/storage/migrations.js.map +1 -0
  250. package/dist/lib/storage/paths.d.ts +51 -0
  251. package/dist/lib/storage/paths.d.ts.map +1 -0
  252. package/dist/lib/storage/paths.js +152 -0
  253. package/dist/lib/storage/paths.js.map +1 -0
  254. package/dist/lib/storage.d.ts +106 -0
  255. package/dist/lib/storage.d.ts.map +1 -0
  256. package/dist/lib/storage.js +896 -0
  257. package/dist/lib/storage.js.map +1 -0
  258. package/dist/lib/table-formatter.d.ts +32 -0
  259. package/dist/lib/table-formatter.d.ts.map +1 -0
  260. package/dist/lib/table-formatter.js +44 -0
  261. package/dist/lib/table-formatter.js.map +1 -0
  262. package/dist/lib/tools/hashline-tools.d.ts +51 -0
  263. package/dist/lib/tools/hashline-tools.d.ts.map +1 -0
  264. package/dist/lib/tools/hashline-tools.js +456 -0
  265. package/dist/lib/tools/hashline-tools.js.map +1 -0
  266. package/dist/lib/types.d.ts +130 -0
  267. package/dist/lib/types.d.ts.map +1 -0
  268. package/dist/lib/types.js +2 -0
  269. package/dist/lib/types.js.map +1 -0
  270. package/dist/lib/ui/ansi.d.ts +40 -0
  271. package/dist/lib/ui/ansi.d.ts.map +1 -0
  272. package/dist/lib/ui/ansi.js +68 -0
  273. package/dist/lib/ui/ansi.js.map +1 -0
  274. package/dist/lib/ui/auth-menu.d.ts +76 -0
  275. package/dist/lib/ui/auth-menu.d.ts.map +1 -0
  276. package/dist/lib/ui/auth-menu.js +590 -0
  277. package/dist/lib/ui/auth-menu.js.map +1 -0
  278. package/dist/lib/ui/confirm.d.ts +11 -0
  279. package/dist/lib/ui/confirm.d.ts.map +1 -0
  280. package/dist/lib/ui/confirm.js +29 -0
  281. package/dist/lib/ui/confirm.js.map +1 -0
  282. package/dist/lib/ui/copy.d.ts +123 -0
  283. package/dist/lib/ui/copy.d.ts.map +1 -0
  284. package/dist/lib/ui/copy.js +127 -0
  285. package/dist/lib/ui/copy.js.map +1 -0
  286. package/dist/lib/ui/format.d.ts +62 -0
  287. package/dist/lib/ui/format.d.ts.map +1 -0
  288. package/dist/lib/ui/format.js +205 -0
  289. package/dist/lib/ui/format.js.map +1 -0
  290. package/dist/lib/ui/runtime.d.ts +43 -0
  291. package/dist/lib/ui/runtime.d.ts.map +1 -0
  292. package/dist/lib/ui/runtime.js +69 -0
  293. package/dist/lib/ui/runtime.js.map +1 -0
  294. package/dist/lib/ui/select.d.ts +60 -0
  295. package/dist/lib/ui/select.d.ts.map +1 -0
  296. package/dist/lib/ui/select.js +467 -0
  297. package/dist/lib/ui/select.js.map +1 -0
  298. package/dist/lib/ui/theme.d.ts +56 -0
  299. package/dist/lib/ui/theme.d.ts.map +1 -0
  300. package/dist/lib/ui/theme.js +186 -0
  301. package/dist/lib/ui/theme.js.map +1 -0
  302. package/dist/lib/unified-settings.d.ts +71 -0
  303. package/dist/lib/unified-settings.d.ts.map +1 -0
  304. package/dist/lib/unified-settings.js +299 -0
  305. package/dist/lib/unified-settings.js.map +1 -0
  306. package/dist/lib/utils.d.ts +29 -0
  307. package/dist/lib/utils.d.ts.map +1 -0
  308. package/dist/lib/utils.js +54 -0
  309. package/dist/lib/utils.js.map +1 -0
  310. package/package.json +115 -0
  311. package/scripts/audit-dev-allowlist.js +128 -0
  312. package/scripts/bench-format/hashline-v2.mjs +642 -0
  313. package/scripts/bench-format/models.mjs +105 -0
  314. package/scripts/bench-format/opencode.mjs +205 -0
  315. package/scripts/bench-format/render.mjs +496 -0
  316. package/scripts/bench-format/stats.mjs +54 -0
  317. package/scripts/bench-format/tasks.mjs +151 -0
  318. package/scripts/benchmark-edit-formats.mjs +1161 -0
  319. package/scripts/benchmark-render-dashboard.mjs +49 -0
  320. package/scripts/codex-multi-auth.js +6 -0
  321. package/scripts/codex-routing.js +34 -0
  322. package/scripts/codex.js +122 -0
  323. package/scripts/copy-oauth-success.js +37 -0
  324. package/scripts/install-opencode-codex-auth.js +193 -0
  325. package/scripts/test-all-models.sh +7 -0
  326. package/scripts/test-model-matrix.js +424 -0
  327. package/scripts/validate-model-map.sh +7 -0
@@ -0,0 +1,896 @@
1
+ import { promises as fs, existsSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { ACCOUNT_LIMITS } from "./constants.js";
5
+ import { createLogger } from "./logger.js";
6
+ import { MODEL_FAMILIES } from "./prompts/codex.js";
7
+ import { AnyAccountStorageSchema, getValidationErrors } from "./schemas.js";
8
+ import { getConfigDir, getProjectConfigDir, getProjectGlobalConfigDir, findProjectRoot, resolvePath } from "./storage/paths.js";
9
+ import { migrateV1ToV3, } from "./storage/migrations.js";
10
+ const log = createLogger("storage");
11
+ const ACCOUNTS_FILE_NAME = "openai-codex-accounts.json";
12
+ const FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-flagged-accounts.json";
13
+ const LEGACY_FLAGGED_ACCOUNTS_FILE_NAME = "openai-codex-blocked-accounts.json";
14
+ const ACCOUNTS_BACKUP_SUFFIX = ".bak";
15
+ const ACCOUNTS_WAL_SUFFIX = ".wal";
16
+ const BACKUP_COPY_MAX_ATTEMPTS = 5;
17
+ const BACKUP_COPY_BASE_DELAY_MS = 10;
18
+ let storageBackupEnabled = true;
19
+ let lastAccountsSaveTimestamp = 0;
20
+ /**
21
+ * Custom error class for storage operations with platform-aware hints.
22
+ */
23
+ export class StorageError extends Error {
24
+ code;
25
+ path;
26
+ hint;
27
+ constructor(message, code, path, hint, cause) {
28
+ super(message, { cause });
29
+ this.name = "StorageError";
30
+ this.code = code;
31
+ this.path = path;
32
+ this.hint = hint;
33
+ }
34
+ }
35
+ /**
36
+ * Generate platform-aware troubleshooting hint based on error code.
37
+ */
38
+ export function formatStorageErrorHint(error, path) {
39
+ const err = error;
40
+ const code = err?.code || "UNKNOWN";
41
+ const isWindows = process.platform === "win32";
42
+ switch (code) {
43
+ case "EACCES":
44
+ case "EPERM":
45
+ return isWindows
46
+ ? `Permission denied writing to ${path}. Check antivirus exclusions for this folder. Ensure you have write permissions.`
47
+ : `Permission denied writing to ${path}. Check folder permissions. Try: chmod 755 ~/.codex`;
48
+ case "EBUSY":
49
+ return `File is locked at ${path}. The file may be open in another program. Close any editors or processes accessing it.`;
50
+ case "ENOSPC":
51
+ return `Disk is full. Free up space and try again. Path: ${path}`;
52
+ case "EEMPTY":
53
+ return `File written but is empty. This may indicate a disk or filesystem issue. Path: ${path}`;
54
+ default:
55
+ return isWindows
56
+ ? `Failed to write to ${path}. Check folder permissions and ensure path contains no special characters.`
57
+ : `Failed to write to ${path}. Check folder permissions and disk space.`;
58
+ }
59
+ }
60
+ let storageMutex = Promise.resolve();
61
+ function withStorageLock(fn) {
62
+ const previousMutex = storageMutex;
63
+ let releaseLock;
64
+ storageMutex = new Promise((resolve) => {
65
+ releaseLock = resolve;
66
+ });
67
+ return previousMutex.then(fn).finally(() => releaseLock());
68
+ }
69
+ async function ensureGitignore(storagePath) {
70
+ if (!currentStoragePath)
71
+ return;
72
+ const configDir = dirname(storagePath);
73
+ const inferredProjectRoot = dirname(configDir);
74
+ const candidateRoots = [currentProjectRoot, inferredProjectRoot].filter((root) => typeof root === "string" && root.length > 0);
75
+ const projectRoot = candidateRoots.find((root) => existsSync(join(root, ".git")));
76
+ if (!projectRoot)
77
+ return;
78
+ const gitignorePath = join(projectRoot, ".gitignore");
79
+ try {
80
+ let content = "";
81
+ if (existsSync(gitignorePath)) {
82
+ content = await fs.readFile(gitignorePath, "utf-8");
83
+ const lines = content.split("\n").map((l) => l.trim());
84
+ if (lines.includes(".codex") || lines.includes(".codex/") || lines.includes("/.codex") || lines.includes("/.codex/")) {
85
+ return;
86
+ }
87
+ }
88
+ const newContent = content.endsWith("\n") || content === "" ? content : content + "\n";
89
+ await fs.writeFile(gitignorePath, newContent + ".codex/\n", "utf-8");
90
+ log.debug("Added .codex to .gitignore", { path: gitignorePath });
91
+ }
92
+ catch (error) {
93
+ log.warn("Failed to update .gitignore", { error: String(error) });
94
+ }
95
+ }
96
+ let currentStoragePath = null;
97
+ let currentLegacyProjectStoragePath = null;
98
+ let currentProjectRoot = null;
99
+ export function setStorageBackupEnabled(enabled) {
100
+ storageBackupEnabled = enabled;
101
+ }
102
+ function getAccountsBackupPath(path) {
103
+ return `${path}${ACCOUNTS_BACKUP_SUFFIX}`;
104
+ }
105
+ function getAccountsWalPath(path) {
106
+ return `${path}${ACCOUNTS_WAL_SUFFIX}`;
107
+ }
108
+ function computeSha256(value) {
109
+ return createHash("sha256").update(value).digest("hex");
110
+ }
111
+ export function getLastAccountsSaveTimestamp() {
112
+ return lastAccountsSaveTimestamp;
113
+ }
114
+ export function setStoragePath(projectPath) {
115
+ if (!projectPath) {
116
+ currentStoragePath = null;
117
+ currentLegacyProjectStoragePath = null;
118
+ currentProjectRoot = null;
119
+ return;
120
+ }
121
+ const projectRoot = findProjectRoot(projectPath);
122
+ if (projectRoot) {
123
+ currentProjectRoot = projectRoot;
124
+ currentStoragePath = join(getProjectGlobalConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
125
+ currentLegacyProjectStoragePath = join(getProjectConfigDir(projectRoot), ACCOUNTS_FILE_NAME);
126
+ }
127
+ else {
128
+ currentStoragePath = null;
129
+ currentLegacyProjectStoragePath = null;
130
+ currentProjectRoot = null;
131
+ }
132
+ }
133
+ export function setStoragePathDirect(path) {
134
+ currentStoragePath = path;
135
+ currentLegacyProjectStoragePath = null;
136
+ currentProjectRoot = null;
137
+ }
138
+ /**
139
+ * Returns the file path for the account storage JSON file.
140
+ * @returns Absolute path to the accounts.json file
141
+ */
142
+ export function getStoragePath() {
143
+ if (currentStoragePath) {
144
+ return currentStoragePath;
145
+ }
146
+ return join(getConfigDir(), ACCOUNTS_FILE_NAME);
147
+ }
148
+ export function getFlaggedAccountsPath() {
149
+ return join(dirname(getStoragePath()), FLAGGED_ACCOUNTS_FILE_NAME);
150
+ }
151
+ function getLegacyFlaggedAccountsPath() {
152
+ return join(dirname(getStoragePath()), LEGACY_FLAGGED_ACCOUNTS_FILE_NAME);
153
+ }
154
+ async function migrateLegacyProjectStorageIfNeeded(persist = saveAccounts) {
155
+ if (!currentStoragePath ||
156
+ !currentLegacyProjectStoragePath ||
157
+ currentLegacyProjectStoragePath === currentStoragePath ||
158
+ !existsSync(currentLegacyProjectStoragePath)) {
159
+ return null;
160
+ }
161
+ try {
162
+ const legacyContent = await fs.readFile(currentLegacyProjectStoragePath, "utf-8");
163
+ const legacyData = JSON.parse(legacyContent);
164
+ const normalized = normalizeAccountStorage(legacyData);
165
+ if (!normalized)
166
+ return null;
167
+ await persist(normalized);
168
+ try {
169
+ await fs.unlink(currentLegacyProjectStoragePath);
170
+ log.info("Removed legacy project account storage file after migration", {
171
+ path: currentLegacyProjectStoragePath,
172
+ });
173
+ }
174
+ catch (unlinkError) {
175
+ const code = unlinkError.code;
176
+ if (code !== "ENOENT") {
177
+ log.warn("Failed to remove legacy project account storage file after migration", {
178
+ path: currentLegacyProjectStoragePath,
179
+ error: String(unlinkError),
180
+ });
181
+ }
182
+ }
183
+ log.info("Migrated legacy project account storage", {
184
+ from: currentLegacyProjectStoragePath,
185
+ to: currentStoragePath,
186
+ accounts: normalized.accounts.length,
187
+ });
188
+ return normalized;
189
+ }
190
+ catch (error) {
191
+ log.warn("Failed to migrate legacy project account storage", {
192
+ from: currentLegacyProjectStoragePath,
193
+ to: currentStoragePath,
194
+ error: String(error),
195
+ });
196
+ return null;
197
+ }
198
+ }
199
+ function selectNewestAccount(current, candidate) {
200
+ if (!current)
201
+ return candidate;
202
+ const currentLastUsed = current.lastUsed || 0;
203
+ const candidateLastUsed = candidate.lastUsed || 0;
204
+ if (candidateLastUsed > currentLastUsed)
205
+ return candidate;
206
+ if (candidateLastUsed < currentLastUsed)
207
+ return current;
208
+ const currentAddedAt = current.addedAt || 0;
209
+ const candidateAddedAt = candidate.addedAt || 0;
210
+ return candidateAddedAt >= currentAddedAt ? candidate : current;
211
+ }
212
+ function deduplicateAccountsByKey(accounts) {
213
+ const keyToIndex = new Map();
214
+ const indicesToKeep = new Set();
215
+ for (let i = 0; i < accounts.length; i += 1) {
216
+ const account = accounts[i];
217
+ if (!account)
218
+ continue;
219
+ const key = account.accountId || account.refreshToken;
220
+ if (!key)
221
+ continue;
222
+ const existingIndex = keyToIndex.get(key);
223
+ if (existingIndex === undefined) {
224
+ keyToIndex.set(key, i);
225
+ continue;
226
+ }
227
+ const existing = accounts[existingIndex];
228
+ const newest = selectNewestAccount(existing, account);
229
+ keyToIndex.set(key, newest === account ? i : existingIndex);
230
+ }
231
+ for (const idx of keyToIndex.values()) {
232
+ indicesToKeep.add(idx);
233
+ }
234
+ const result = [];
235
+ for (let i = 0; i < accounts.length; i += 1) {
236
+ if (indicesToKeep.has(i)) {
237
+ const account = accounts[i];
238
+ if (account)
239
+ result.push(account);
240
+ }
241
+ }
242
+ return result;
243
+ }
244
+ /**
245
+ * Removes duplicate accounts, keeping the most recently used entry for each unique key.
246
+ * Deduplication is based on accountId or refreshToken.
247
+ * @param accounts - Array of accounts to deduplicate
248
+ * @returns New array with duplicates removed
249
+ */
250
+ export function deduplicateAccounts(accounts) {
251
+ return deduplicateAccountsByKey(accounts);
252
+ }
253
+ /**
254
+ * Removes duplicate accounts by email, keeping the most recently used entry.
255
+ * Accounts without email are always preserved.
256
+ * @param accounts - Array of accounts to deduplicate
257
+ * @returns New array with email duplicates removed
258
+ */
259
+ export function deduplicateAccountsByEmail(accounts) {
260
+ const emailToNewestIndex = new Map();
261
+ const indicesToKeep = new Set();
262
+ for (let i = 0; i < accounts.length; i += 1) {
263
+ const account = accounts[i];
264
+ if (!account)
265
+ continue;
266
+ const email = account.email?.trim();
267
+ if (!email) {
268
+ indicesToKeep.add(i);
269
+ continue;
270
+ }
271
+ const existingIndex = emailToNewestIndex.get(email);
272
+ if (existingIndex === undefined) {
273
+ emailToNewestIndex.set(email, i);
274
+ continue;
275
+ }
276
+ const existing = accounts[existingIndex];
277
+ // istanbul ignore next -- defensive code: existingIndex always refers to valid account
278
+ if (!existing) {
279
+ emailToNewestIndex.set(email, i);
280
+ continue;
281
+ }
282
+ const existingLastUsed = existing.lastUsed || 0;
283
+ const candidateLastUsed = account.lastUsed || 0;
284
+ const existingAddedAt = existing.addedAt || 0;
285
+ const candidateAddedAt = account.addedAt || 0;
286
+ const isNewer = candidateLastUsed > existingLastUsed ||
287
+ (candidateLastUsed === existingLastUsed && candidateAddedAt > existingAddedAt);
288
+ if (isNewer) {
289
+ emailToNewestIndex.set(email, i);
290
+ }
291
+ }
292
+ for (const idx of emailToNewestIndex.values()) {
293
+ indicesToKeep.add(idx);
294
+ }
295
+ const result = [];
296
+ for (let i = 0; i < accounts.length; i += 1) {
297
+ if (indicesToKeep.has(i)) {
298
+ const account = accounts[i];
299
+ if (account)
300
+ result.push(account);
301
+ }
302
+ }
303
+ return result;
304
+ }
305
+ function isRecord(value) {
306
+ return !!value && typeof value === "object" && !Array.isArray(value);
307
+ }
308
+ function clampIndex(index, length) {
309
+ if (length <= 0)
310
+ return 0;
311
+ return Math.max(0, Math.min(index, length - 1));
312
+ }
313
+ function toAccountKey(account) {
314
+ return account.accountId || account.refreshToken;
315
+ }
316
+ function extractActiveKey(accounts, activeIndex) {
317
+ const candidate = accounts[activeIndex];
318
+ if (!isRecord(candidate))
319
+ return undefined;
320
+ const accountId = typeof candidate.accountId === "string" && candidate.accountId.trim()
321
+ ? candidate.accountId
322
+ : undefined;
323
+ const refreshToken = typeof candidate.refreshToken === "string" && candidate.refreshToken.trim()
324
+ ? candidate.refreshToken
325
+ : undefined;
326
+ return accountId || refreshToken;
327
+ }
328
+ /**
329
+ * Normalizes and validates account storage data, migrating from v1 to v3 if needed.
330
+ * Handles deduplication, index clamping, and per-family active index mapping.
331
+ * @param data - Raw storage data (unknown format)
332
+ * @returns Normalized AccountStorageV3 or null if invalid
333
+ */
334
+ export function normalizeAccountStorage(data) {
335
+ if (!isRecord(data)) {
336
+ log.warn("Invalid storage format, ignoring");
337
+ return null;
338
+ }
339
+ if (data.version !== 1 && data.version !== 3) {
340
+ log.warn("Unknown storage version, ignoring", {
341
+ version: data.version,
342
+ });
343
+ return null;
344
+ }
345
+ const rawAccounts = data.accounts;
346
+ if (!Array.isArray(rawAccounts)) {
347
+ log.warn("Invalid storage format, ignoring");
348
+ return null;
349
+ }
350
+ const activeIndexValue = typeof data.activeIndex === "number" && Number.isFinite(data.activeIndex)
351
+ ? data.activeIndex
352
+ : 0;
353
+ const rawActiveIndex = clampIndex(activeIndexValue, rawAccounts.length);
354
+ const activeKey = extractActiveKey(rawAccounts, rawActiveIndex);
355
+ const fromVersion = data.version;
356
+ const baseStorage = fromVersion === 1
357
+ ? migrateV1ToV3(data)
358
+ : data;
359
+ const validAccounts = rawAccounts.filter((account) => isRecord(account) && typeof account.refreshToken === "string" && !!account.refreshToken.trim());
360
+ const deduplicatedAccounts = deduplicateAccountsByEmail(deduplicateAccountsByKey(validAccounts));
361
+ const activeIndex = (() => {
362
+ if (deduplicatedAccounts.length === 0)
363
+ return 0;
364
+ if (activeKey) {
365
+ const mappedIndex = deduplicatedAccounts.findIndex((account) => toAccountKey(account) === activeKey);
366
+ if (mappedIndex >= 0)
367
+ return mappedIndex;
368
+ }
369
+ return clampIndex(rawActiveIndex, deduplicatedAccounts.length);
370
+ })();
371
+ const activeIndexByFamily = {};
372
+ const rawFamilyIndices = isRecord(baseStorage.activeIndexByFamily)
373
+ ? baseStorage.activeIndexByFamily
374
+ : {};
375
+ for (const family of MODEL_FAMILIES) {
376
+ const rawIndexValue = rawFamilyIndices[family];
377
+ const rawIndex = typeof rawIndexValue === "number" && Number.isFinite(rawIndexValue)
378
+ ? rawIndexValue
379
+ : rawActiveIndex;
380
+ const clampedRawIndex = clampIndex(rawIndex, rawAccounts.length);
381
+ const familyKey = extractActiveKey(rawAccounts, clampedRawIndex);
382
+ let mappedIndex = clampIndex(rawIndex, deduplicatedAccounts.length);
383
+ if (familyKey && deduplicatedAccounts.length > 0) {
384
+ const idx = deduplicatedAccounts.findIndex((account) => toAccountKey(account) === familyKey);
385
+ if (idx >= 0) {
386
+ mappedIndex = idx;
387
+ }
388
+ }
389
+ activeIndexByFamily[family] = mappedIndex;
390
+ }
391
+ return {
392
+ version: 3,
393
+ accounts: deduplicatedAccounts,
394
+ activeIndex,
395
+ activeIndexByFamily,
396
+ };
397
+ }
398
+ /**
399
+ * Loads OAuth accounts from disk storage.
400
+ * Automatically migrates v1 storage to v3 format if needed.
401
+ * @returns AccountStorageV3 if file exists and is valid, null otherwise
402
+ */
403
+ export async function loadAccounts() {
404
+ return loadAccountsInternal(saveAccounts);
405
+ }
406
+ function parseAndNormalizeStorage(data) {
407
+ const schemaErrors = getValidationErrors(AnyAccountStorageSchema, data);
408
+ const normalized = normalizeAccountStorage(data);
409
+ const storedVersion = isRecord(data) ? data.version : undefined;
410
+ return { normalized, storedVersion, schemaErrors };
411
+ }
412
+ async function loadAccountsFromPath(path) {
413
+ const content = await fs.readFile(path, "utf-8");
414
+ const data = JSON.parse(content);
415
+ return parseAndNormalizeStorage(data);
416
+ }
417
+ async function loadAccountsFromJournal(path) {
418
+ const walPath = getAccountsWalPath(path);
419
+ try {
420
+ const raw = await fs.readFile(walPath, "utf-8");
421
+ const parsed = JSON.parse(raw);
422
+ if (!isRecord(parsed))
423
+ return null;
424
+ const entry = parsed;
425
+ if (entry.version !== 1)
426
+ return null;
427
+ if (typeof entry.content !== "string" || typeof entry.checksum !== "string")
428
+ return null;
429
+ const computed = computeSha256(entry.content);
430
+ if (computed !== entry.checksum) {
431
+ log.warn("Account journal checksum mismatch", { path: walPath });
432
+ return null;
433
+ }
434
+ const data = JSON.parse(entry.content);
435
+ const { normalized } = parseAndNormalizeStorage(data);
436
+ if (!normalized)
437
+ return null;
438
+ log.warn("Recovered account storage from WAL journal", { path, walPath });
439
+ return normalized;
440
+ }
441
+ catch (error) {
442
+ const code = error.code;
443
+ if (code !== "ENOENT") {
444
+ log.warn("Failed to load account WAL journal", { path: walPath, error: String(error) });
445
+ }
446
+ return null;
447
+ }
448
+ }
449
+ async function loadAccountsInternal(persistMigration) {
450
+ try {
451
+ const path = getStoragePath();
452
+ const { normalized, storedVersion, schemaErrors } = await loadAccountsFromPath(path);
453
+ if (schemaErrors.length > 0) {
454
+ log.warn("Account storage schema validation warnings", { errors: schemaErrors.slice(0, 5) });
455
+ }
456
+ if (normalized && storedVersion !== normalized.version) {
457
+ log.info("Migrating account storage to v3", { from: storedVersion, to: normalized.version });
458
+ if (persistMigration) {
459
+ try {
460
+ await persistMigration(normalized);
461
+ }
462
+ catch (saveError) {
463
+ log.warn("Failed to persist migrated storage", { error: String(saveError) });
464
+ }
465
+ }
466
+ }
467
+ return normalized;
468
+ }
469
+ catch (error) {
470
+ const code = error.code;
471
+ const path = getStoragePath();
472
+ if (code === "ENOENT" && persistMigration) {
473
+ const migrated = await migrateLegacyProjectStorageIfNeeded(persistMigration);
474
+ if (migrated)
475
+ return migrated;
476
+ }
477
+ const recoveredFromWal = await loadAccountsFromJournal(path);
478
+ if (recoveredFromWal) {
479
+ if (persistMigration) {
480
+ try {
481
+ await persistMigration(recoveredFromWal);
482
+ }
483
+ catch (persistError) {
484
+ log.warn("Failed to persist WAL-recovered storage", {
485
+ path,
486
+ error: String(persistError),
487
+ });
488
+ }
489
+ }
490
+ return recoveredFromWal;
491
+ }
492
+ if (storageBackupEnabled) {
493
+ const backupPath = getAccountsBackupPath(path);
494
+ try {
495
+ const backup = await loadAccountsFromPath(backupPath);
496
+ if (backup.schemaErrors.length > 0) {
497
+ log.warn("Backup account storage schema validation warnings", {
498
+ path: backupPath,
499
+ errors: backup.schemaErrors.slice(0, 5),
500
+ });
501
+ }
502
+ if (backup.normalized) {
503
+ log.warn("Recovered account storage from backup file", { path, backupPath });
504
+ if (persistMigration) {
505
+ try {
506
+ await persistMigration(backup.normalized);
507
+ }
508
+ catch (persistError) {
509
+ log.warn("Failed to persist recovered backup storage", {
510
+ path,
511
+ error: String(persistError),
512
+ });
513
+ }
514
+ }
515
+ return backup.normalized;
516
+ }
517
+ }
518
+ catch (backupError) {
519
+ const backupCode = backupError.code;
520
+ if (backupCode !== "ENOENT") {
521
+ log.warn("Failed to load backup account storage", {
522
+ path: backupPath,
523
+ error: String(backupError),
524
+ });
525
+ }
526
+ }
527
+ }
528
+ if (code !== "ENOENT") {
529
+ log.error("Failed to load account storage", { error: String(error) });
530
+ }
531
+ return null;
532
+ }
533
+ }
534
+ async function saveAccountsUnlocked(storage) {
535
+ const path = getStoragePath();
536
+ const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
537
+ const tempPath = `${path}.${uniqueSuffix}.tmp`;
538
+ const walPath = getAccountsWalPath(path);
539
+ try {
540
+ await fs.mkdir(dirname(path), { recursive: true });
541
+ await ensureGitignore(path);
542
+ if (storageBackupEnabled && existsSync(path)) {
543
+ const backupPath = getAccountsBackupPath(path);
544
+ try {
545
+ for (let attempt = 0; attempt < BACKUP_COPY_MAX_ATTEMPTS; attempt += 1) {
546
+ try {
547
+ await fs.copyFile(path, backupPath);
548
+ break;
549
+ }
550
+ catch (backupError) {
551
+ const code = backupError.code;
552
+ const canRetry = (code === "EPERM" || code === "EBUSY") &&
553
+ attempt + 1 < BACKUP_COPY_MAX_ATTEMPTS;
554
+ if (canRetry) {
555
+ await new Promise((resolve) => setTimeout(resolve, BACKUP_COPY_BASE_DELAY_MS * 2 ** attempt));
556
+ continue;
557
+ }
558
+ throw backupError;
559
+ }
560
+ }
561
+ }
562
+ catch (backupError) {
563
+ log.warn("Failed to create account storage backup", {
564
+ path,
565
+ backupPath,
566
+ error: String(backupError),
567
+ });
568
+ }
569
+ }
570
+ const content = JSON.stringify(storage, null, 2);
571
+ const journalEntry = {
572
+ version: 1,
573
+ createdAt: Date.now(),
574
+ path,
575
+ checksum: computeSha256(content),
576
+ content,
577
+ };
578
+ await fs.writeFile(walPath, JSON.stringify(journalEntry), {
579
+ encoding: "utf-8",
580
+ mode: 0o600,
581
+ });
582
+ await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
583
+ const stats = await fs.stat(tempPath);
584
+ if (stats.size === 0) {
585
+ const emptyError = Object.assign(new Error("File written but size is 0"), { code: "EEMPTY" });
586
+ throw emptyError;
587
+ }
588
+ // Retry rename with exponential backoff for Windows EPERM/EBUSY
589
+ let lastError = null;
590
+ for (let attempt = 0; attempt < 5; attempt++) {
591
+ try {
592
+ await fs.rename(tempPath, path);
593
+ lastAccountsSaveTimestamp = Date.now();
594
+ try {
595
+ await fs.unlink(walPath);
596
+ }
597
+ catch {
598
+ // Best effort cleanup.
599
+ }
600
+ return;
601
+ }
602
+ catch (renameError) {
603
+ const code = renameError.code;
604
+ if (code === "EPERM" || code === "EBUSY") {
605
+ lastError = renameError;
606
+ await new Promise(r => setTimeout(r, 10 * Math.pow(2, attempt)));
607
+ continue;
608
+ }
609
+ throw renameError;
610
+ }
611
+ }
612
+ if (lastError)
613
+ throw lastError;
614
+ }
615
+ catch (error) {
616
+ try {
617
+ await fs.unlink(tempPath);
618
+ }
619
+ catch {
620
+ // Ignore cleanup failure.
621
+ }
622
+ const err = error;
623
+ const code = err?.code || "UNKNOWN";
624
+ const hint = formatStorageErrorHint(error, path);
625
+ log.error("Failed to save accounts", {
626
+ path,
627
+ code,
628
+ message: err?.message,
629
+ hint,
630
+ });
631
+ throw new StorageError(`Failed to save accounts: ${err?.message || "Unknown error"}`, code, path, hint, err instanceof Error ? err : undefined);
632
+ }
633
+ }
634
+ export async function withAccountStorageTransaction(handler) {
635
+ return withStorageLock(async () => {
636
+ const current = await loadAccountsInternal(saveAccountsUnlocked);
637
+ return handler(current, saveAccountsUnlocked);
638
+ });
639
+ }
640
+ /**
641
+ * Persists account storage to disk using atomic write (temp file + rename).
642
+ * Creates the Codex multi-auth storage directory if it doesn't exist.
643
+ * Verifies file was written correctly and provides detailed error messages.
644
+ * @param storage - Account storage data to save
645
+ * @throws StorageError with platform-aware hints on failure
646
+ */
647
+ export async function saveAccounts(storage) {
648
+ return withStorageLock(async () => {
649
+ await saveAccountsUnlocked(storage);
650
+ });
651
+ }
652
+ /**
653
+ * Deletes the account storage file from disk.
654
+ * Silently ignores if file doesn't exist.
655
+ */
656
+ export async function clearAccounts() {
657
+ return withStorageLock(async () => {
658
+ const path = getStoragePath();
659
+ const walPath = getAccountsWalPath(path);
660
+ const backupPath = getAccountsBackupPath(path);
661
+ const clearPath = async (targetPath) => {
662
+ try {
663
+ await fs.unlink(targetPath);
664
+ }
665
+ catch (error) {
666
+ const code = error.code;
667
+ if (code !== "ENOENT") {
668
+ log.error("Failed to clear account storage artifact", {
669
+ path: targetPath,
670
+ error: String(error),
671
+ });
672
+ }
673
+ }
674
+ };
675
+ try {
676
+ await Promise.all([clearPath(path), clearPath(walPath), clearPath(backupPath)]);
677
+ }
678
+ catch {
679
+ // Individual path cleanup is already best-effort with per-artifact logging.
680
+ }
681
+ });
682
+ }
683
+ function normalizeFlaggedStorage(data) {
684
+ if (!isRecord(data) || data.version !== 1 || !Array.isArray(data.accounts)) {
685
+ return { version: 1, accounts: [] };
686
+ }
687
+ const byRefreshToken = new Map();
688
+ for (const rawAccount of data.accounts) {
689
+ if (!isRecord(rawAccount))
690
+ continue;
691
+ const refreshToken = typeof rawAccount.refreshToken === "string" ? rawAccount.refreshToken.trim() : "";
692
+ if (!refreshToken)
693
+ continue;
694
+ const flaggedAt = typeof rawAccount.flaggedAt === "number" ? rawAccount.flaggedAt : Date.now();
695
+ const isAccountIdSource = (value) => value === "token" || value === "id_token" || value === "org" || value === "manual";
696
+ const isSwitchReason = (value) => value === "rate-limit" || value === "initial" || value === "rotation";
697
+ const isCooldownReason = (value) => value === "auth-failure" || value === "network-error" || value === "rate-limit";
698
+ let rateLimitResetTimes;
699
+ if (isRecord(rawAccount.rateLimitResetTimes)) {
700
+ const normalizedRateLimits = {};
701
+ for (const [key, value] of Object.entries(rawAccount.rateLimitResetTimes)) {
702
+ if (typeof value === "number") {
703
+ normalizedRateLimits[key] = value;
704
+ }
705
+ }
706
+ if (Object.keys(normalizedRateLimits).length > 0) {
707
+ rateLimitResetTimes = normalizedRateLimits;
708
+ }
709
+ }
710
+ const accountIdSource = isAccountIdSource(rawAccount.accountIdSource)
711
+ ? rawAccount.accountIdSource
712
+ : undefined;
713
+ const lastSwitchReason = isSwitchReason(rawAccount.lastSwitchReason)
714
+ ? rawAccount.lastSwitchReason
715
+ : undefined;
716
+ const cooldownReason = isCooldownReason(rawAccount.cooldownReason)
717
+ ? rawAccount.cooldownReason
718
+ : undefined;
719
+ const normalized = {
720
+ refreshToken,
721
+ addedAt: typeof rawAccount.addedAt === "number" ? rawAccount.addedAt : flaggedAt,
722
+ lastUsed: typeof rawAccount.lastUsed === "number" ? rawAccount.lastUsed : flaggedAt,
723
+ accountId: typeof rawAccount.accountId === "string" ? rawAccount.accountId : undefined,
724
+ accountIdSource,
725
+ accountLabel: typeof rawAccount.accountLabel === "string" ? rawAccount.accountLabel : undefined,
726
+ email: typeof rawAccount.email === "string" ? rawAccount.email : undefined,
727
+ enabled: typeof rawAccount.enabled === "boolean" ? rawAccount.enabled : undefined,
728
+ lastSwitchReason,
729
+ rateLimitResetTimes,
730
+ coolingDownUntil: typeof rawAccount.coolingDownUntil === "number" ? rawAccount.coolingDownUntil : undefined,
731
+ cooldownReason,
732
+ flaggedAt,
733
+ flaggedReason: typeof rawAccount.flaggedReason === "string" ? rawAccount.flaggedReason : undefined,
734
+ lastError: typeof rawAccount.lastError === "string" ? rawAccount.lastError : undefined,
735
+ };
736
+ byRefreshToken.set(refreshToken, normalized);
737
+ }
738
+ return {
739
+ version: 1,
740
+ accounts: Array.from(byRefreshToken.values()),
741
+ };
742
+ }
743
+ export async function loadFlaggedAccounts() {
744
+ const path = getFlaggedAccountsPath();
745
+ const empty = { version: 1, accounts: [] };
746
+ try {
747
+ const content = await fs.readFile(path, "utf-8");
748
+ const data = JSON.parse(content);
749
+ return normalizeFlaggedStorage(data);
750
+ }
751
+ catch (error) {
752
+ const code = error.code;
753
+ if (code !== "ENOENT") {
754
+ log.error("Failed to load flagged account storage", { path, error: String(error) });
755
+ return empty;
756
+ }
757
+ }
758
+ const legacyPath = getLegacyFlaggedAccountsPath();
759
+ if (!existsSync(legacyPath)) {
760
+ return empty;
761
+ }
762
+ try {
763
+ const legacyContent = await fs.readFile(legacyPath, "utf-8");
764
+ const legacyData = JSON.parse(legacyContent);
765
+ const migrated = normalizeFlaggedStorage(legacyData);
766
+ if (migrated.accounts.length > 0) {
767
+ await saveFlaggedAccounts(migrated);
768
+ }
769
+ try {
770
+ await fs.unlink(legacyPath);
771
+ }
772
+ catch {
773
+ // Best effort cleanup.
774
+ }
775
+ log.info("Migrated legacy flagged account storage", {
776
+ from: legacyPath,
777
+ to: path,
778
+ accounts: migrated.accounts.length,
779
+ });
780
+ return migrated;
781
+ }
782
+ catch (error) {
783
+ log.error("Failed to migrate legacy flagged account storage", {
784
+ from: legacyPath,
785
+ to: path,
786
+ error: String(error),
787
+ });
788
+ return empty;
789
+ }
790
+ }
791
+ export async function saveFlaggedAccounts(storage) {
792
+ return withStorageLock(async () => {
793
+ const path = getFlaggedAccountsPath();
794
+ const uniqueSuffix = `${Date.now()}.${Math.random().toString(36).slice(2, 8)}`;
795
+ const tempPath = `${path}.${uniqueSuffix}.tmp`;
796
+ try {
797
+ await fs.mkdir(dirname(path), { recursive: true });
798
+ const content = JSON.stringify(normalizeFlaggedStorage(storage), null, 2);
799
+ await fs.writeFile(tempPath, content, { encoding: "utf-8", mode: 0o600 });
800
+ await fs.rename(tempPath, path);
801
+ }
802
+ catch (error) {
803
+ try {
804
+ await fs.unlink(tempPath);
805
+ }
806
+ catch {
807
+ // Ignore cleanup failures.
808
+ }
809
+ log.error("Failed to save flagged account storage", { path, error: String(error) });
810
+ throw error;
811
+ }
812
+ });
813
+ }
814
+ export async function clearFlaggedAccounts() {
815
+ return withStorageLock(async () => {
816
+ try {
817
+ await fs.unlink(getFlaggedAccountsPath());
818
+ }
819
+ catch (error) {
820
+ const code = error.code;
821
+ if (code !== "ENOENT") {
822
+ log.error("Failed to clear flagged account storage", { error: String(error) });
823
+ }
824
+ }
825
+ });
826
+ }
827
+ /**
828
+ * Exports current accounts to a JSON file for backup/migration.
829
+ * @param filePath - Destination file path
830
+ * @param force - If true, overwrite existing file (default: true)
831
+ * @throws Error if file exists and force is false, or if no accounts to export
832
+ */
833
+ export async function exportAccounts(filePath, force = true) {
834
+ const resolvedPath = resolvePath(filePath);
835
+ if (!force && existsSync(resolvedPath)) {
836
+ throw new Error(`File already exists: ${resolvedPath}`);
837
+ }
838
+ const storage = await withAccountStorageTransaction((current) => Promise.resolve(current));
839
+ if (!storage || storage.accounts.length === 0) {
840
+ throw new Error("No accounts to export");
841
+ }
842
+ await fs.mkdir(dirname(resolvedPath), { recursive: true });
843
+ const content = JSON.stringify(storage, null, 2);
844
+ await fs.writeFile(resolvedPath, content, { encoding: "utf-8", mode: 0o600 });
845
+ log.info("Exported accounts", { path: resolvedPath, count: storage.accounts.length });
846
+ }
847
+ /**
848
+ * Imports accounts from a JSON file, merging with existing accounts.
849
+ * Deduplicates by accountId/email, preserving most recently used entries.
850
+ * @param filePath - Source file path
851
+ * @throws Error if file is invalid or would exceed MAX_ACCOUNTS
852
+ */
853
+ export async function importAccounts(filePath) {
854
+ const resolvedPath = resolvePath(filePath);
855
+ // Check file exists with friendly error
856
+ if (!existsSync(resolvedPath)) {
857
+ throw new Error(`Import file not found: ${resolvedPath}`);
858
+ }
859
+ const content = await fs.readFile(resolvedPath, "utf-8");
860
+ let imported;
861
+ try {
862
+ imported = JSON.parse(content);
863
+ }
864
+ catch {
865
+ throw new Error(`Invalid JSON in import file: ${resolvedPath}`);
866
+ }
867
+ const normalized = normalizeAccountStorage(imported);
868
+ if (!normalized) {
869
+ throw new Error("Invalid account storage format");
870
+ }
871
+ const { imported: importedCount, total, skipped: skippedCount } = await withAccountStorageTransaction(async (existing, persist) => {
872
+ const existingAccounts = existing?.accounts ?? [];
873
+ const existingActiveIndex = existing?.activeIndex ?? 0;
874
+ const merged = [...existingAccounts, ...normalized.accounts];
875
+ if (merged.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
876
+ const deduped = deduplicateAccountsByEmail(deduplicateAccounts(merged));
877
+ if (deduped.length > ACCOUNT_LIMITS.MAX_ACCOUNTS) {
878
+ throw new Error(`Import would exceed maximum of ${ACCOUNT_LIMITS.MAX_ACCOUNTS} accounts (would have ${deduped.length})`);
879
+ }
880
+ }
881
+ const deduplicatedAccounts = deduplicateAccountsByEmail(deduplicateAccounts(merged));
882
+ const newStorage = {
883
+ version: 3,
884
+ accounts: deduplicatedAccounts,
885
+ activeIndex: existingActiveIndex,
886
+ activeIndexByFamily: existing?.activeIndexByFamily,
887
+ };
888
+ await persist(newStorage);
889
+ const imported = deduplicatedAccounts.length - existingAccounts.length;
890
+ const skipped = normalized.accounts.length - imported;
891
+ return { imported, total: deduplicatedAccounts.length, skipped };
892
+ });
893
+ log.info("Imported accounts", { path: resolvedPath, imported: importedCount, skipped: skippedCount, total });
894
+ return { imported: importedCount, total, skipped: skippedCount };
895
+ }
896
+ //# sourceMappingURL=storage.js.map