oc-codex-multi-auth 6.0.0 → 6.1.1

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 (284) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +56 -18
  3. package/assets/readme-hero.svg +10 -10
  4. package/config/README.md +1 -1
  5. package/config/minimal-opencode.json +1 -1
  6. package/config/opencode-legacy.json +1 -1
  7. package/config/opencode-modern.json +1 -1
  8. package/dist/index.d.ts +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +112 -2525
  11. package/dist/index.js.map +1 -1
  12. package/dist/lib/accounts/persistence.d.ts +34 -0
  13. package/dist/lib/accounts/persistence.d.ts.map +1 -0
  14. package/dist/lib/accounts/persistence.js +153 -0
  15. package/dist/lib/accounts/persistence.js.map +1 -0
  16. package/dist/lib/accounts/recovery.d.ts +59 -0
  17. package/dist/lib/accounts/recovery.d.ts.map +1 -0
  18. package/dist/lib/accounts/recovery.js +229 -0
  19. package/dist/lib/accounts/recovery.js.map +1 -0
  20. package/dist/lib/accounts/rotation.d.ts +40 -0
  21. package/dist/lib/accounts/rotation.d.ts.map +1 -0
  22. package/dist/lib/accounts/rotation.js +204 -0
  23. package/dist/lib/accounts/rotation.js.map +1 -0
  24. package/dist/lib/accounts/state.d.ts +92 -0
  25. package/dist/lib/accounts/state.d.ts.map +1 -0
  26. package/dist/lib/accounts/state.js +366 -0
  27. package/dist/lib/accounts/state.js.map +1 -0
  28. package/dist/lib/accounts.d.ts +48 -91
  29. package/dist/lib/accounts.d.ts.map +1 -1
  30. package/dist/lib/accounts.js +121 -699
  31. package/dist/lib/accounts.js.map +1 -1
  32. package/dist/lib/auth/auth.d.ts +1 -1
  33. package/dist/lib/auth/auth.d.ts.map +1 -1
  34. package/dist/lib/auth/auth.js +5 -2
  35. package/dist/lib/auth/auth.js.map +1 -1
  36. package/dist/lib/auth/login-runner.d.ts +32 -0
  37. package/dist/lib/auth/login-runner.d.ts.map +1 -1
  38. package/dist/lib/auth/login-runner.js +79 -64
  39. package/dist/lib/auth/login-runner.js.map +1 -1
  40. package/dist/lib/auth/server.d.ts.map +1 -1
  41. package/dist/lib/auth/server.js +3 -8
  42. package/dist/lib/auth/server.js.map +1 -1
  43. package/dist/lib/circuit-breaker.d.ts +53 -2
  44. package/dist/lib/circuit-breaker.d.ts.map +1 -1
  45. package/dist/lib/circuit-breaker.js +65 -6
  46. package/dist/lib/circuit-breaker.js.map +1 -1
  47. package/dist/lib/config.d.ts.map +1 -1
  48. package/dist/lib/config.js +77 -32
  49. package/dist/lib/config.js.map +1 -1
  50. package/dist/lib/error-sentinels.d.ts +20 -0
  51. package/dist/lib/error-sentinels.d.ts.map +1 -0
  52. package/dist/lib/error-sentinels.js +28 -0
  53. package/dist/lib/error-sentinels.js.map +1 -0
  54. package/dist/lib/errors.d.ts +100 -1
  55. package/dist/lib/errors.d.ts.map +1 -1
  56. package/dist/lib/errors.js +116 -1
  57. package/dist/lib/errors.js.map +1 -1
  58. package/dist/lib/logger.d.ts +14 -1
  59. package/dist/lib/logger.d.ts.map +1 -1
  60. package/dist/lib/logger.js +65 -5
  61. package/dist/lib/logger.js.map +1 -1
  62. package/dist/lib/oauth-constants.d.ts +18 -0
  63. package/dist/lib/oauth-constants.d.ts.map +1 -0
  64. package/dist/lib/oauth-constants.js +18 -0
  65. package/dist/lib/oauth-constants.js.map +1 -0
  66. package/dist/lib/oauth-success.d.ts +2 -0
  67. package/dist/lib/oauth-success.d.ts.map +1 -0
  68. package/dist/lib/oauth-success.html +711 -711
  69. package/dist/lib/oauth-success.js +714 -0
  70. package/dist/lib/oauth-success.js.map +1 -0
  71. package/dist/lib/prompts/codex.d.ts.map +1 -1
  72. package/dist/lib/prompts/codex.js +12 -3
  73. package/dist/lib/prompts/codex.js.map +1 -1
  74. package/dist/lib/prompts/opencode-codex.d.ts.map +1 -1
  75. package/dist/lib/prompts/opencode-codex.js +3 -2
  76. package/dist/lib/prompts/opencode-codex.js.map +1 -1
  77. package/dist/lib/recovery/hook.d.ts +43 -0
  78. package/dist/lib/recovery/hook.d.ts.map +1 -0
  79. package/dist/lib/recovery/hook.js +325 -0
  80. package/dist/lib/recovery/hook.js.map +1 -0
  81. package/dist/lib/recovery/index.d.ts +1 -0
  82. package/dist/lib/recovery/index.d.ts.map +1 -1
  83. package/dist/lib/recovery/index.js +1 -0
  84. package/dist/lib/recovery/index.js.map +1 -1
  85. package/dist/lib/recovery/storage.d.ts.map +1 -1
  86. package/dist/lib/recovery/storage.js +5 -1
  87. package/dist/lib/recovery/storage.js.map +1 -1
  88. package/dist/lib/recovery.d.ts +15 -30
  89. package/dist/lib/recovery.d.ts.map +1 -1
  90. package/dist/lib/recovery.js +14 -312
  91. package/dist/lib/recovery.js.map +1 -1
  92. package/dist/lib/request/fetch-helpers.d.ts +1 -0
  93. package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
  94. package/dist/lib/request/fetch-helpers.js +58 -10
  95. package/dist/lib/request/fetch-helpers.js.map +1 -1
  96. package/dist/lib/request/response-handler.d.ts.map +1 -1
  97. package/dist/lib/request/response-handler.js +8 -2
  98. package/dist/lib/request/response-handler.js.map +1 -1
  99. package/dist/lib/runtime.d.ts +101 -0
  100. package/dist/lib/runtime.d.ts.map +1 -0
  101. package/dist/lib/runtime.js +77 -0
  102. package/dist/lib/runtime.js.map +1 -0
  103. package/dist/lib/schemas.d.ts +164 -1
  104. package/dist/lib/schemas.d.ts.map +1 -1
  105. package/dist/lib/schemas.js +251 -0
  106. package/dist/lib/schemas.js.map +1 -1
  107. package/dist/lib/storage/atomic-write.d.ts +31 -0
  108. package/dist/lib/storage/atomic-write.d.ts.map +1 -0
  109. package/dist/lib/storage/atomic-write.js +75 -0
  110. package/dist/lib/storage/atomic-write.js.map +1 -0
  111. package/dist/lib/storage/backup.d.ts +13 -0
  112. package/dist/lib/storage/backup.d.ts.map +1 -0
  113. package/dist/lib/storage/backup.js +59 -0
  114. package/dist/lib/storage/backup.js.map +1 -0
  115. package/dist/lib/storage/errors.d.ts +17 -0
  116. package/dist/lib/storage/errors.d.ts.map +1 -0
  117. package/dist/lib/storage/errors.js +38 -0
  118. package/dist/lib/storage/errors.js.map +1 -0
  119. package/dist/lib/storage/export-import.d.ts +84 -0
  120. package/dist/lib/storage/export-import.d.ts.map +1 -0
  121. package/dist/lib/storage/export-import.js +222 -0
  122. package/dist/lib/storage/export-import.js.map +1 -0
  123. package/dist/lib/storage/flagged.d.ts +29 -0
  124. package/dist/lib/storage/flagged.d.ts.map +1 -0
  125. package/dist/lib/storage/flagged.js +207 -0
  126. package/dist/lib/storage/flagged.js.map +1 -0
  127. package/dist/lib/storage/identity.d.ts +72 -0
  128. package/dist/lib/storage/identity.d.ts.map +1 -0
  129. package/dist/lib/storage/identity.js +230 -0
  130. package/dist/lib/storage/identity.js.map +1 -0
  131. package/dist/lib/storage/keychain.d.ts +146 -0
  132. package/dist/lib/storage/keychain.d.ts.map +1 -0
  133. package/dist/lib/storage/keychain.js +278 -0
  134. package/dist/lib/storage/keychain.js.map +1 -0
  135. package/dist/lib/storage/load-save.d.ts +61 -0
  136. package/dist/lib/storage/load-save.d.ts.map +1 -0
  137. package/dist/lib/storage/load-save.js +606 -0
  138. package/dist/lib/storage/load-save.js.map +1 -0
  139. package/dist/lib/storage/migrations.d.ts +35 -0
  140. package/dist/lib/storage/migrations.d.ts.map +1 -1
  141. package/dist/lib/storage/migrations.js +37 -0
  142. package/dist/lib/storage/migrations.js.map +1 -1
  143. package/dist/lib/storage/normalize.d.ts +22 -0
  144. package/dist/lib/storage/normalize.d.ts.map +1 -0
  145. package/dist/lib/storage/normalize.js +114 -0
  146. package/dist/lib/storage/normalize.js.map +1 -0
  147. package/dist/lib/storage/paths.d.ts.map +1 -1
  148. package/dist/lib/storage/paths.js +2 -1
  149. package/dist/lib/storage/paths.js.map +1 -1
  150. package/dist/lib/storage/state.d.ts +46 -0
  151. package/dist/lib/storage/state.d.ts.map +1 -0
  152. package/dist/lib/storage/state.js +100 -0
  153. package/dist/lib/storage/state.js.map +1 -0
  154. package/dist/lib/storage/worktree-lock.d.ts +79 -0
  155. package/dist/lib/storage/worktree-lock.d.ts.map +1 -0
  156. package/dist/lib/storage/worktree-lock.js +265 -0
  157. package/dist/lib/storage/worktree-lock.js.map +1 -0
  158. package/dist/lib/storage.d.ts +21 -155
  159. package/dist/lib/storage.d.ts.map +1 -1
  160. package/dist/lib/storage.js +24 -1167
  161. package/dist/lib/storage.js.map +1 -1
  162. package/dist/lib/tools/_shared.d.ts +16 -0
  163. package/dist/lib/tools/_shared.d.ts.map +1 -0
  164. package/dist/lib/tools/_shared.js +21 -0
  165. package/dist/lib/tools/_shared.js.map +1 -0
  166. package/dist/lib/tools/codex-dashboard.d.ts +8 -0
  167. package/dist/lib/tools/codex-dashboard.d.ts.map +1 -0
  168. package/dist/lib/tools/codex-dashboard.js +176 -0
  169. package/dist/lib/tools/codex-dashboard.js.map +1 -0
  170. package/dist/lib/tools/codex-diag.d.ts +19 -0
  171. package/dist/lib/tools/codex-diag.d.ts.map +1 -0
  172. package/dist/lib/tools/codex-diag.js +139 -0
  173. package/dist/lib/tools/codex-diag.js.map +1 -0
  174. package/dist/lib/tools/codex-diff.d.ts +44 -0
  175. package/dist/lib/tools/codex-diff.d.ts.map +1 -0
  176. package/dist/lib/tools/codex-diff.js +284 -0
  177. package/dist/lib/tools/codex-diff.js.map +1 -0
  178. package/dist/lib/tools/codex-doctor.d.ts +8 -0
  179. package/dist/lib/tools/codex-doctor.d.ts.map +1 -0
  180. package/dist/lib/tools/codex-doctor.js +284 -0
  181. package/dist/lib/tools/codex-doctor.js.map +1 -0
  182. package/dist/lib/tools/codex-export.d.ts +8 -0
  183. package/dist/lib/tools/codex-export.d.ts.map +1 -0
  184. package/dist/lib/tools/codex-export.js +63 -0
  185. package/dist/lib/tools/codex-export.js.map +1 -0
  186. package/dist/lib/tools/codex-health.d.ts +8 -0
  187. package/dist/lib/tools/codex-health.d.ts.map +1 -0
  188. package/dist/lib/tools/codex-health.js +124 -0
  189. package/dist/lib/tools/codex-health.js.map +1 -0
  190. package/dist/lib/tools/codex-help.d.ts +8 -0
  191. package/dist/lib/tools/codex-help.d.ts.map +1 -0
  192. package/dist/lib/tools/codex-help.js +127 -0
  193. package/dist/lib/tools/codex-help.js.map +1 -0
  194. package/dist/lib/tools/codex-import.d.ts +8 -0
  195. package/dist/lib/tools/codex-import.d.ts.map +1 -0
  196. package/dist/lib/tools/codex-import.js +97 -0
  197. package/dist/lib/tools/codex-import.js.map +1 -0
  198. package/dist/lib/tools/codex-keychain.d.ts +36 -0
  199. package/dist/lib/tools/codex-keychain.d.ts.map +1 -0
  200. package/dist/lib/tools/codex-keychain.js +289 -0
  201. package/dist/lib/tools/codex-keychain.js.map +1 -0
  202. package/dist/lib/tools/codex-label.d.ts +8 -0
  203. package/dist/lib/tools/codex-label.d.ts.map +1 -0
  204. package/dist/lib/tools/codex-label.js +143 -0
  205. package/dist/lib/tools/codex-label.js.map +1 -0
  206. package/dist/lib/tools/codex-limits.d.ts +8 -0
  207. package/dist/lib/tools/codex-limits.d.ts.map +1 -0
  208. package/dist/lib/tools/codex-limits.js +497 -0
  209. package/dist/lib/tools/codex-limits.js.map +1 -0
  210. package/dist/lib/tools/codex-list.d.ts +8 -0
  211. package/dist/lib/tools/codex-list.d.ts.map +1 -0
  212. package/dist/lib/tools/codex-list.js +260 -0
  213. package/dist/lib/tools/codex-list.js.map +1 -0
  214. package/dist/lib/tools/codex-metrics.d.ts +8 -0
  215. package/dist/lib/tools/codex-metrics.d.ts.map +1 -0
  216. package/dist/lib/tools/codex-metrics.js +180 -0
  217. package/dist/lib/tools/codex-metrics.js.map +1 -0
  218. package/dist/lib/tools/codex-next.d.ts +8 -0
  219. package/dist/lib/tools/codex-next.d.ts.map +1 -0
  220. package/dist/lib/tools/codex-next.js +54 -0
  221. package/dist/lib/tools/codex-next.js.map +1 -0
  222. package/dist/lib/tools/codex-note.d.ts +8 -0
  223. package/dist/lib/tools/codex-note.d.ts.map +1 -0
  224. package/dist/lib/tools/codex-note.js +79 -0
  225. package/dist/lib/tools/codex-note.js.map +1 -0
  226. package/dist/lib/tools/codex-refresh.d.ts +8 -0
  227. package/dist/lib/tools/codex-refresh.d.ts.map +1 -0
  228. package/dist/lib/tools/codex-refresh.js +78 -0
  229. package/dist/lib/tools/codex-refresh.js.map +1 -0
  230. package/dist/lib/tools/codex-remove.d.ts +8 -0
  231. package/dist/lib/tools/codex-remove.d.ts.map +1 -0
  232. package/dist/lib/tools/codex-remove.js +184 -0
  233. package/dist/lib/tools/codex-remove.js.map +1 -0
  234. package/dist/lib/tools/codex-setup.d.ts +8 -0
  235. package/dist/lib/tools/codex-setup.d.ts.map +1 -0
  236. package/dist/lib/tools/codex-setup.js +26 -0
  237. package/dist/lib/tools/codex-setup.js.map +1 -0
  238. package/dist/lib/tools/codex-status.d.ts +8 -0
  239. package/dist/lib/tools/codex-status.d.ts.map +1 -0
  240. package/dist/lib/tools/codex-status.js +263 -0
  241. package/dist/lib/tools/codex-status.js.map +1 -0
  242. package/dist/lib/tools/codex-switch.d.ts +8 -0
  243. package/dist/lib/tools/codex-switch.d.ts.map +1 -0
  244. package/dist/lib/tools/codex-switch.js +122 -0
  245. package/dist/lib/tools/codex-switch.js.map +1 -0
  246. package/dist/lib/tools/codex-tag.d.ts +8 -0
  247. package/dist/lib/tools/codex-tag.d.ts.map +1 -0
  248. package/dist/lib/tools/codex-tag.js +101 -0
  249. package/dist/lib/tools/codex-tag.js.map +1 -0
  250. package/dist/lib/tools/index.d.ts +143 -0
  251. package/dist/lib/tools/index.d.ts.map +1 -0
  252. package/dist/lib/tools/index.js +67 -0
  253. package/dist/lib/tools/index.js.map +1 -0
  254. package/dist/lib/ui/format.d.ts.map +1 -1
  255. package/dist/lib/ui/format.js +5 -0
  256. package/dist/lib/ui/format.js.map +1 -1
  257. package/dist/lib/ui/runtime.d.ts +10 -0
  258. package/dist/lib/ui/runtime.d.ts.map +1 -1
  259. package/dist/lib/ui/runtime.js +16 -11
  260. package/dist/lib/ui/runtime.js.map +1 -1
  261. package/dist/lib/ui/select.d.ts.map +1 -1
  262. package/dist/lib/ui/select.js +12 -3
  263. package/dist/lib/ui/select.js.map +1 -1
  264. package/dist/lib/ui/theme.d.ts +19 -0
  265. package/dist/lib/ui/theme.d.ts.map +1 -1
  266. package/dist/lib/ui/theme.js +27 -0
  267. package/dist/lib/ui/theme.js.map +1 -1
  268. package/package.json +24 -16
  269. package/scripts/copy-oauth-success.js +16 -6
  270. package/scripts/install-oc-codex-multi-auth-core.js +462 -381
  271. package/scripts/install-oc-codex-multi-auth.js +15 -15
  272. package/scripts/test-all-models.sh +1 -1
  273. package/dist/lib/audit.d.ts +0 -45
  274. package/dist/lib/audit.d.ts.map +0 -1
  275. package/dist/lib/audit.js +0 -154
  276. package/dist/lib/audit.js.map +0 -1
  277. package/dist/lib/auth-rate-limit.d.ts +0 -20
  278. package/dist/lib/auth-rate-limit.d.ts.map +0 -1
  279. package/dist/lib/auth-rate-limit.js +0 -91
  280. package/dist/lib/auth-rate-limit.js.map +0 -1
  281. package/dist/lib/runtime-contracts.d.ts +0 -16
  282. package/dist/lib/runtime-contracts.d.ts.map +0 -1
  283. package/dist/lib/runtime-contracts.js +0 -24
  284. package/dist/lib/runtime-contracts.js.map +0 -1
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Opt-in OS-keychain credential backend.
3
+ *
4
+ * Phase 4 F1. Addresses the audit finding (docs/audits/09-security-trust.md)
5
+ * that per-project accounts are stored as plaintext V3 JSON on disk. This
6
+ * module introduces an alternate storage backend that persists the SAME
7
+ * V3 JSON blob as a secret value in the OS keychain:
8
+ *
9
+ * - macOS: Keychain
10
+ * - Windows: Credential Manager
11
+ * - Linux: Secret Service / libsecret
12
+ *
13
+ * **Default behavior is unchanged.** The backend only activates when
14
+ * `CODEX_KEYCHAIN=1` is set in the environment. Any other value (unset,
15
+ * `"0"`, `"false"`, `""`, `"yes"`, ...) leaves the existing JSON path in
16
+ * full control. This is deliberate: credential storage is the highest-trust
17
+ * surface in the plugin and every new failure mode is a login regression.
18
+ *
19
+ * Data model:
20
+ * - Service name: `oc-codex-multi-auth` (fixed)
21
+ * - Account key: `accounts:<project-storage-key>` where the project key
22
+ * is the `projectName-sha256hash12` string already produced by
23
+ * `lib/storage/paths.ts#getProjectStorageKey`. For the global file we
24
+ * use the literal key `accounts:global`.
25
+ * - Secret payload: the exact JSON string that would otherwise be written
26
+ * to disk (UTF-8, pretty-printed to match the file contents; the OS
27
+ * keychain APIs used here store arbitrary UTF-8 so no base64 encoding
28
+ * is required — documented here for reviewers).
29
+ *
30
+ * Fallback contract:
31
+ * - Every entry point returns a soft failure (`null` on read,
32
+ * `{ok: false}` on write) instead of throwing when the native module
33
+ * cannot load or a keychain call fails. Callers MUST fall back to the
34
+ * JSON backend on failure; they must never silently lose credentials.
35
+ * - All errors are logged with `log.warn` / `log.error`. Secret values
36
+ * never appear in log text — only the service name, account key, and
37
+ * the native error message.
38
+ *
39
+ * Testing:
40
+ * - The native module is loaded lazily so unit tests can substitute
41
+ * `_setBackendForTests` to avoid hitting the real OS keychain.
42
+ * Integration tests that DO want to hit the real keychain can set
43
+ * `CODEX_KEYCHAIN=1` and let `loadBackend` resolve the module.
44
+ */
45
+ import { createLogger } from "../logger.js";
46
+ const log = createLogger("storage.keychain");
47
+ /**
48
+ * The keychain service identifier this plugin owns. Chosen to match the npm
49
+ * package name so a human inspecting Keychain Access / Credential Manager
50
+ * can easily identify which program owns a stored credential.
51
+ */
52
+ export const KEYCHAIN_SERVICE_NAME = "oc-codex-multi-auth";
53
+ /**
54
+ * Account key used when the plugin is operating without a per-project
55
+ * storage path (i.e. the global accounts file). Kept distinct from any
56
+ * per-project key so the two storage scopes never collide in the OS
57
+ * keychain's (service, account) index.
58
+ */
59
+ export const GLOBAL_KEYCHAIN_ACCOUNT_KEY = "accounts:global";
60
+ /**
61
+ * Reserved account key used exclusively by the availability probe. Kept
62
+ * double-underscored and service-suffixed so a future refactor that drops
63
+ * the `accounts:` prefix on real entries (or adds a different prefix) still
64
+ * cannot collide with this probe (F1 post-merge LOW finding).
65
+ */
66
+ export const KEYCHAIN_PROBE_ACCOUNT_KEY = `__probe__@${KEYCHAIN_SERVICE_NAME}`;
67
+ let cachedBackend = null;
68
+ let cacheResolved = false;
69
+ async function loadNativeBackend() {
70
+ try {
71
+ // Dynamic import keeps the module resolution inside the try/catch so
72
+ // a missing prebuilt binary (e.g. unsupported platform, partial npm
73
+ // install) degrades to JSON fallback instead of crashing at require
74
+ // time. The cast narrows the type without using `any`.
75
+ const mod = (await import("@napi-rs/keyring"));
76
+ if (!mod || typeof mod.Entry !== "function") {
77
+ log.warn("keychain: @napi-rs/keyring module shape unexpected; disabling keychain backend");
78
+ return null;
79
+ }
80
+ // @napi-rs/keyring's `Entry` methods are synchronous by design (they
81
+ // delegate to native OS keychain calls). The backend interface is
82
+ // Promise-typed so it can be mocked with in-memory Promise-returning
83
+ // stubs in tests, so each method returns `Promise.resolve(...)` of
84
+ // the native sync result. Using non-async methods keeps
85
+ // `@typescript-eslint/require-await` satisfied without per-method
86
+ // suppressions while the Promise-returning interface stays intact.
87
+ const backend = {
88
+ get(service, account) {
89
+ try {
90
+ const entry = new mod.Entry(service, account);
91
+ const value = entry.getPassword();
92
+ return Promise.resolve(value ?? null);
93
+ }
94
+ catch (err) {
95
+ log.warn("keychain: read failed", {
96
+ service,
97
+ account,
98
+ error: err.message,
99
+ });
100
+ return Promise.resolve(null);
101
+ }
102
+ },
103
+ set(service, account, secret) {
104
+ const entry = new mod.Entry(service, account);
105
+ entry.setPassword(secret);
106
+ return Promise.resolve();
107
+ },
108
+ delete(service, account) {
109
+ try {
110
+ const entry = new mod.Entry(service, account);
111
+ return Promise.resolve(entry.deletePassword());
112
+ }
113
+ catch (err) {
114
+ log.warn("keychain: delete failed", {
115
+ service,
116
+ account,
117
+ error: err.message,
118
+ });
119
+ return Promise.resolve(false);
120
+ }
121
+ },
122
+ isAvailable() {
123
+ // Round-trip a throwaway entry under a reserved, namespaced
124
+ // account key so we actually exercise the OS keychain rather
125
+ // than trusting that import succeeded. The key is prefixed
126
+ // with the service name so a future refactor that drops the
127
+ // `accounts:` prefix on real entries cannot collide with
128
+ // this probe (F1 post-merge LOW finding).
129
+ try {
130
+ const entry = new mod.Entry(KEYCHAIN_SERVICE_NAME, KEYCHAIN_PROBE_ACCOUNT_KEY);
131
+ entry.setPassword("probe");
132
+ entry.deletePassword();
133
+ return Promise.resolve(true);
134
+ }
135
+ catch (err) {
136
+ log.warn("keychain: availability probe failed", {
137
+ error: err.message,
138
+ });
139
+ return Promise.resolve(false);
140
+ }
141
+ },
142
+ };
143
+ return backend;
144
+ }
145
+ catch (err) {
146
+ log.warn("keychain: native module unavailable", {
147
+ error: err.message,
148
+ });
149
+ return null;
150
+ }
151
+ }
152
+ /**
153
+ * Resolve and memoize the backend. Returns `null` when the native module
154
+ * cannot be loaded; callers fall back to JSON in that case.
155
+ */
156
+ async function getBackend() {
157
+ if (cacheResolved)
158
+ return cachedBackend;
159
+ cachedBackend = await loadNativeBackend();
160
+ cacheResolved = true;
161
+ return cachedBackend;
162
+ }
163
+ /**
164
+ * Build the keychain account key for a given project storage key. When the
165
+ * plugin is running against the global accounts file, pass `null` to use
166
+ * the reserved global account key.
167
+ */
168
+ export function buildKeychainAccountKey(projectStorageKey) {
169
+ if (!projectStorageKey)
170
+ return GLOBAL_KEYCHAIN_ACCOUNT_KEY;
171
+ return `accounts:${projectStorageKey}`;
172
+ }
173
+ /**
174
+ * Reads the V3 JSON blob from the OS keychain for the given project storage
175
+ * key. Returns `null` when:
176
+ * - the native module is unavailable (not installed, missing prebuilt)
177
+ * - no entry exists yet
178
+ * - any keychain error occurs
179
+ *
180
+ * The caller MUST treat `null` as "fall back to JSON". Callers must never
181
+ * interpret `null` as "no credentials" unconditionally — it may mean
182
+ * "keychain locked / permission denied" and the JSON file still holds the
183
+ * authoritative copy.
184
+ */
185
+ export async function readFromKeychain(projectStorageKey) {
186
+ const backend = await getBackend();
187
+ if (!backend)
188
+ return null;
189
+ const account = buildKeychainAccountKey(projectStorageKey);
190
+ return backend.get(KEYCHAIN_SERVICE_NAME, account);
191
+ }
192
+ /**
193
+ * Persist the V3 JSON blob to the OS keychain. Returns `{ok:false}` on
194
+ * failure so callers can fall back to JSON. Never throws.
195
+ */
196
+ export async function writeToKeychain(projectStorageKey, jsonBlob) {
197
+ const backend = await getBackend();
198
+ if (!backend) {
199
+ return { ok: false, error: "backend unavailable" };
200
+ }
201
+ const account = buildKeychainAccountKey(projectStorageKey);
202
+ try {
203
+ await backend.set(KEYCHAIN_SERVICE_NAME, account, jsonBlob);
204
+ return { ok: true };
205
+ }
206
+ catch (err) {
207
+ const message = err.message;
208
+ log.error("keychain: write failed", {
209
+ service: KEYCHAIN_SERVICE_NAME,
210
+ account,
211
+ error: message,
212
+ });
213
+ return { ok: false, error: message };
214
+ }
215
+ }
216
+ /**
217
+ * Remove the plugin's keychain entry for the given project key. Returns
218
+ * true when the entry existed and was deleted, false otherwise (including
219
+ * "not present"). Never throws.
220
+ */
221
+ export async function deleteFromKeychain(projectStorageKey) {
222
+ const backend = await getBackend();
223
+ if (!backend)
224
+ return false;
225
+ const account = buildKeychainAccountKey(projectStorageKey);
226
+ return backend.delete(KEYCHAIN_SERVICE_NAME, account);
227
+ }
228
+ /**
229
+ * Probe the backend end-to-end (write + read + delete a throwaway entry)
230
+ * to confirm the OS keychain is reachable and unlocked. Used by the
231
+ * `codex-keychain status` tool to give the operator a clear yes/no signal
232
+ * instead of waiting until the next real save.
233
+ *
234
+ * Gated on the opt-in flag (F1 post-merge LOW finding): with
235
+ * `CODEX_KEYCHAIN` unset the probe is a no-op that returns `false` without
236
+ * touching the OS keychain. This preserves the "unset -> zero keychain code
237
+ * path" invariant the feature advertises and avoids the first-run macOS
238
+ * "allow/always allow" prompt that `entry.setPassword` can otherwise
239
+ * trigger for users who run `codex-keychain status` without opting in.
240
+ *
241
+ * Pass `env` to override the opt-in lookup in tests.
242
+ */
243
+ export async function keychainIsAvailable(env = process.env) {
244
+ if (!isKeychainOptInEnabled(env))
245
+ return false;
246
+ const backend = await getBackend();
247
+ if (!backend)
248
+ return false;
249
+ return backend.isAvailable();
250
+ }
251
+ /**
252
+ * Parse the `CODEX_KEYCHAIN` environment variable. Only the literal string
253
+ * `"1"` enables the opt-in. Anything else (unset, `"0"`, `"false"`, `""`,
254
+ * `"yes"`, ...) leaves the JSON backend in full control. This mirrors
255
+ * `EnvBooleanSchema`'s contract and is documented in README.md.
256
+ */
257
+ export function isKeychainOptInEnabled(env = process.env) {
258
+ return env.CODEX_KEYCHAIN === "1";
259
+ }
260
+ /**
261
+ * Test-only hook: inject a deterministic backend (typically an in-memory
262
+ * Map-backed stub) and mark the cache as resolved so `getBackend` returns
263
+ * the injected backend without attempting to load the native module.
264
+ *
265
+ * Passing `null` clears the cache so a subsequent call re-runs the lazy
266
+ * loader — useful when a test needs to verify the unavailable-backend
267
+ * branch.
268
+ */
269
+ export function _setBackendForTests(backend) {
270
+ cachedBackend = backend;
271
+ cacheResolved = backend !== null;
272
+ }
273
+ /** Test-only reset to the initial "not yet loaded" state. */
274
+ export function _resetBackendForTests() {
275
+ cachedBackend = null;
276
+ cacheResolved = false;
277
+ }
278
+ //# sourceMappingURL=keychain.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"keychain.js","sourceRoot":"","sources":["../../../lib/storage/keychain.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,GAAG,GAAG,YAAY,CAAC,kBAAkB,CAAC,CAAC;AAE7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC;AAE3D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,iBAAiB,CAAC;AAE7D;;;;;GAKG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,aAAa,qBAAqB,EAAE,CAAC;AAc/E,IAAI,aAAa,GAA2B,IAAI,CAAC;AACjD,IAAI,aAAa,GAAG,KAAK,CAAC;AAc1B,KAAK,UAAU,iBAAiB;IAC/B,IAAI,CAAC;QACJ,qEAAqE;QACrE,oEAAoE;QACpE,oEAAoE;QACpE,uDAAuD;QACvD,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CAAC,kBAAkB,CAAC,CAAmC,CAAC;QACjF,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;YAC7C,GAAG,CAAC,IAAI,CACP,gFAAgF,CAChF,CAAC;YACF,OAAO,IAAI,CAAC;QACb,CAAC;QACD,qEAAqE;QACrE,kEAAkE;QAClE,qEAAqE;QACrE,mEAAmE;QACnE,wDAAwD;QACxD,kEAAkE;QAClE,mEAAmE;QACnE,MAAM,OAAO,GAAoB;YAChC,GAAG,CAAC,OAAO,EAAE,OAAO;gBACnB,IAAI,CAAC;oBACJ,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBAC9C,MAAM,KAAK,GAAG,KAAK,CAAC,WAAW,EAAE,CAAC;oBAClC,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;gBACvC,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,GAAG,CAAC,IAAI,CAAC,uBAAuB,EAAE;wBACjC,OAAO;wBACP,OAAO;wBACP,KAAK,EAAG,GAAa,CAAC,OAAO;qBAC7B,CAAC,CAAC;oBACH,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;YACF,CAAC;YACD,GAAG,CAAC,OAAO,EAAE,OAAO,EAAE,MAAM;gBAC3B,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBAC9C,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBAC1B,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;YAC1B,CAAC;YACD,MAAM,CAAC,OAAO,EAAE,OAAO;gBACtB,IAAI,CAAC;oBACJ,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;oBAC9C,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,EAAE,CAAC,CAAC;gBAChD,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,GAAG,CAAC,IAAI,CAAC,yBAAyB,EAAE;wBACnC,OAAO;wBACP,OAAO;wBACP,KAAK,EAAG,GAAa,CAAC,OAAO;qBAC7B,CAAC,CAAC;oBACH,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC/B,CAAC;YACF,CAAC;YACD,WAAW;gBACV,4DAA4D;gBAC5D,6DAA6D;gBAC7D,2DAA2D;gBAC3D,4DAA4D;gBAC5D,yDAAyD;gBACzD,0CAA0C;gBAC1C,IAAI,CAAC;oBACJ,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,KAAK,CAC1B,qBAAqB,EACrB,0BAA0B,CAC1B,CAAC;oBACF,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;oBAC3B,KAAK,CAAC,cAAc,EAAE,CAAC;oBACvB,OAAO,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;gBAC9B,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACd,GAAG,CAAC,IAAI,CAAC,qCAAqC,EAAE;wBAC/C,KAAK,EAAG,GAAa,CAAC,OAAO;qBAC7B,CAAC,CAAC;oBACH,OAAO,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC/B,CAAC;YACF,CAAC;SACD,CAAC;QACF,OAAO,OAAO,CAAC;IAChB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,GAAG,CAAC,IAAI,CAAC,qCAAqC,EAAE;YAC/C,KAAK,EAAG,GAAa,CAAC,OAAO;SAC7B,CAAC,CAAC;QACH,OAAO,IAAI,CAAC;IACb,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,UAAU;IACxB,IAAI,aAAa;QAAE,OAAO,aAAa,CAAC;IACxC,aAAa,GAAG,MAAM,iBAAiB,EAAE,CAAC;IAC1C,aAAa,GAAG,IAAI,CAAC;IACrB,OAAO,aAAa,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,iBAAgC;IACvE,IAAI,CAAC,iBAAiB;QAAE,OAAO,2BAA2B,CAAC;IAC3D,OAAO,YAAY,iBAAiB,EAAE,CAAC;AACxC,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACrC,iBAAgC;IAEhC,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;IACnC,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,OAAO,GAAG,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;IAC3D,OAAO,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;AACpD,CAAC;AAQD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACpC,iBAAgC,EAChC,QAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;IACnC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,qBAAqB,EAAE,CAAC;IACpD,CAAC;IACD,MAAM,OAAO,GAAG,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;IAC3D,IAAI,CAAC;QACJ,MAAM,OAAO,CAAC,GAAG,CAAC,qBAAqB,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;QAC5D,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;IACrB,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,MAAM,OAAO,GAAI,GAAa,CAAC,OAAO,CAAC;QACvC,GAAG,CAAC,KAAK,CAAC,wBAAwB,EAAE;YACnC,OAAO,EAAE,qBAAqB;YAC9B,OAAO;YACP,KAAK,EAAE,OAAO;SACd,CAAC,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;IACtC,CAAC;AACF,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACvC,iBAAgC;IAEhC,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;IACnC,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,MAAM,OAAO,GAAG,uBAAuB,CAAC,iBAAiB,CAAC,CAAC;IAC3D,OAAO,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,OAAO,CAAC,CAAC;AACvD,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACxC,MAAyB,OAAO,CAAC,GAAG;IAEpC,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAC;IAC/C,MAAM,OAAO,GAAG,MAAM,UAAU,EAAE,CAAC;IACnC,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,OAAO,OAAO,CAAC,WAAW,EAAE,CAAC;AAC9B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC1E,OAAO,GAAG,CAAC,cAAc,KAAK,GAAG,CAAC;AACnC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAA+B;IAClE,aAAa,GAAG,OAAO,CAAC;IACxB,aAAa,GAAG,OAAO,KAAK,IAAI,CAAC;AAClC,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,qBAAqB;IACpC,aAAa,GAAG,IAAI,CAAC;IACrB,aAAa,GAAG,KAAK,CAAC;AACvB,CAAC"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Account storage load/save pipeline.
3
+ *
4
+ * Split out of `lib/storage.ts` in RC-2. This module owns:
5
+ * - the `.gitignore` side-effect when writing into a project repo,
6
+ * - the legacy project + global storage migrations triggered on ENOENT,
7
+ * - the project-global fallback seed flow,
8
+ * - the atomic write (temp file + rename + EEMPTY guard),
9
+ * - and the `withAccountStorageTransaction` read-modify-write primitive
10
+ * that every mutating caller above the storage layer uses.
11
+ *
12
+ * The error-handling contract is subtle and load-bearing: forward-compat
13
+ * (`UNSUPPORTED_SCHEMA_VERSION`) and unknown-V2 failures MUST reach the
14
+ * caller. Swallowing either would overwrite future-schema credentials or
15
+ * silently discard a user's V2 file, which is exactly the class of bug the
16
+ * audit flagged.
17
+ */
18
+ import { type AccountStorageV3 } from "./migrations.js";
19
+ /**
20
+ * Loads OAuth accounts from disk storage.
21
+ * Automatically migrates v1 storage to v3 format if needed.
22
+ * @returns AccountStorageV3 if file exists and is valid, null otherwise
23
+ * @throws StorageError (code `UNSUPPORTED_SCHEMA_VERSION`) when the on-disk
24
+ * `version` field is greater than the newest format this plugin understands.
25
+ * Surfacing the error stops a downgraded plugin from overwriting the user's
26
+ * future-schema credentials with a stale or empty payload.
27
+ */
28
+ export declare function loadAccounts(): Promise<AccountStorageV3 | null>;
29
+ /**
30
+ * Executes a read-modify-write transaction under the storage lock and exposes
31
+ * an unlocked persist callback so nested save operations do not deadlock.
32
+ */
33
+ export declare function withAccountStorageTransaction<T>(handler: (current: AccountStorageV3 | null, persist: (storage: AccountStorageV3) => Promise<void>) => Promise<T>): Promise<T>;
34
+ /**
35
+ * Persists account storage to disk using atomic write (temp file + rename).
36
+ * Creates the .opencode directory if it doesn't exist.
37
+ * Verifies file was written correctly and provides detailed error messages.
38
+ * @param storage - Account storage data to save
39
+ * @throws StorageError with platform-aware hints on failure
40
+ */
41
+ export declare function saveAccounts(storage: AccountStorageV3): Promise<void>;
42
+ /**
43
+ * Deletes the account storage file from disk.
44
+ * Silently ignores if file doesn't exist.
45
+ *
46
+ * Ordering (F1 post-merge MEDIUM finding): unlink the on-disk JSON FIRST,
47
+ * then delete the keychain entry. If we cleared the keychain first and the
48
+ * unlink failed for a non-ENOENT reason (EACCES, EBUSY, filesystem drift),
49
+ * a subsequent load with opt-in still on would take the "no keychain entry,
50
+ * fall back to JSON" branch (see `loadAccountsInternal`) and resurrect the
51
+ * credentials from the still-present JSON file. Callers typically run
52
+ * `clearAccounts` to recover from a compromised token, so a silent
53
+ * resurrection is a meaningful failure mode.
54
+ *
55
+ * Fail-safe invariant: if the JSON unlink fails (non-ENOENT), we skip the
56
+ * keychain delete and log at `error`. Both copies remain in sync so the
57
+ * caller can retry safely. The operation is still best-effort (never
58
+ * throws) to preserve the existing contract above the storage layer.
59
+ */
60
+ export declare function clearAccounts(): Promise<void>;
61
+ //# sourceMappingURL=load-save.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-save.d.ts","sourceRoot":"","sources":["../../../lib/storage/load-save.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAmBH,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,iBAAiB,CAAC;AAkjBzB;;;;;;;;GAQG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAErE;AAED;;;GAGG;AACH,wBAAsB,6BAA6B,CAAC,CAAC,EACnD,OAAO,EAAE,CACP,OAAO,EAAE,gBAAgB,GAAG,IAAI,EAChC,OAAO,EAAE,CAAC,OAAO,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,KAClD,OAAO,CAAC,CAAC,CAAC,GACd,OAAO,CAAC,CAAC,CAAC,CAKZ;AAED;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAI3E;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAgCnD"}