codex-multi-auth 2.7.1 → 2.8.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-multi-auth",
3
- "version": "2.7.1",
3
+ "version": "2.8.1",
4
4
  "description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
5
5
  "interface": {
6
6
  "composerIcon": "./assets/codex-multi-auth-icon.svg"
package/README.md CHANGED
@@ -391,7 +391,8 @@ codex-multi-auth doctor --json
391
391
 
392
392
  ## Release Notes
393
393
 
394
- - Current stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md) — install via `npm i -g codex-multi-auth`
394
+ - Current stable: [docs/releases/v2.8.0.md](docs/releases/v2.8.0.md) — install via `npm i -g codex-multi-auth`
395
+ - Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
395
396
  - Previous stable: [docs/releases/v2.7.0.md](docs/releases/v2.7.0.md)
396
397
  - Previous stable: [docs/releases/v2.6.0.md](docs/releases/v2.6.0.md)
397
398
  - Previous stable: [docs/releases/v2.5.0.md](docs/releases/v2.5.0.md)
@@ -6,6 +6,57 @@ interface ActiveSelection {
6
6
  expiresAt?: number;
7
7
  idToken?: string;
8
8
  }
9
+ /**
10
+ * Whether `config.toml` may be rewritten to pin the credential store.
11
+ *
12
+ * Exported so callers that only *plan* a rewrite — `doctor --fix --dry-run` —
13
+ * can report the same outcome the real run would produce, instead of promising
14
+ * a change the opt-out would suppress.
15
+ */
16
+ export declare function shouldEnforceCodexCliFileAuthStore(): boolean;
17
+ /**
18
+ * Reads the top-level `cli_auth_credentials_store` value out of a raw
19
+ * `config.toml`.
20
+ *
21
+ * Mirrors the scoping rules of {@link ensureCodexCliFileAuthStore}: assignments
22
+ * inside a `[table]` belong to that table, so only lines before the first table
23
+ * header describe the official CLI's default credential store. Reporting a
24
+ * profile-scoped value as the effective one would let `doctor` call a config
25
+ * healthy while the CLI still reaches for the keychain.
26
+ *
27
+ * Both TOML string forms count. A literal string (`'file'`) is as valid as a
28
+ * basic string (`"file"`), so treating only the latter as real would make
29
+ * `doctor` warn about a perfectly healthy config. Literal strings have no
30
+ * escape sequences, and the values in play here are bare identifiers, so a
31
+ * non-greedy character class is an exact match for both forms.
32
+ *
33
+ * @param rawConfig - Full text of `config.toml`.
34
+ * @returns The declared mode, or `null` when no top-level assignment exists.
35
+ */
36
+ export declare function readTopLevelCodexCliAuthStoreMode(rawConfig: string): string | null;
37
+ /**
38
+ * Persists `cli_auth_credentials_store = "file"` as a top-level key in the
39
+ * official Codex CLI `config.toml`.
40
+ *
41
+ * The wrapper already appends `-c cli_auth_credentials_store="file"` to every
42
+ * command it forwards, but that only covers processes it launches itself.
43
+ * Third-party front-ends (menu-bar apps such as CodexBar, editor extensions)
44
+ * exec the official binary directly, so the *persisted* value is what decides
45
+ * whether the official CLI reaches into the macOS login keychain. Leaving it on
46
+ * `"keychain"` is what produces the repeated "Always Allow" prompts reported in
47
+ * issue #641.
48
+ *
49
+ * Only top-level assignments are rewritten. A `cli_auth_credentials_store`
50
+ * inside a `[profiles.*]` table must be left alone: rewriting it in place would
51
+ * mangle a scoped setting while still leaving the top-level default pointed at
52
+ * the keychain, so the prompts would continue.
53
+ *
54
+ * @param configPath - Config file to reconcile; defaults to the resolved Codex
55
+ * CLI `config.toml`.
56
+ * @returns `true` when the file was rewritten, `false` when it was already
57
+ * correct or enforcement is disabled.
58
+ */
59
+ export declare function ensureCodexCliFileAuthStore(configPath?: string): Promise<boolean>;
9
60
  export declare function setCodexCliActiveSelection(selection: ActiveSelection): Promise<boolean>;
10
61
  export declare function getLastCodexCliSelectionWriteTimestamp(): number;
11
62
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../../lib/codex-cli/writer.ts"],"names":[],"mappings":"AAoBA,UAAU,eAAe;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AA2XD,wBAAsB,0BAA0B,CAC/C,SAAS,EAAE,eAAe,GACxB,OAAO,CAAC,OAAO,CAAC,CAqMlB;AAED,wBAAgB,sCAAsC,IAAI,MAAM,CAE/D"}
1
+ {"version":3,"file":"writer.d.ts","sourceRoot":"","sources":["../../../lib/codex-cli/writer.ts"],"names":[],"mappings":"AAoBA,UAAU,eAAe;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AA8ND;;;;;;GAMG;AACH,wBAAgB,kCAAkC,IAAI,OAAO,CAG5D;AAiBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,iCAAiC,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASlF;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,2BAA2B,CAChD,UAAU,GAAE,MAAgC,GAC1C,OAAO,CAAC,OAAO,CAAC,CAgDlB;AAmHD,wBAAsB,0BAA0B,CAC/C,SAAS,EAAE,eAAe,GACxB,OAAO,CAAC,OAAO,CAAC,CAqMlB;AAED,wBAAgB,sCAAsC,IAAI,MAAM,CAE/D"}
@@ -199,30 +199,111 @@ async function atomicWriteText(path, content) {
199
199
  async function atomicWriteJson(path, payload) {
200
200
  return atomicWriteText(path, JSON.stringify(payload, null, 2));
201
201
  }
202
- function shouldEnforceCodexCliFileAuthStore() {
202
+ /**
203
+ * Whether `config.toml` may be rewritten to pin the credential store.
204
+ *
205
+ * Exported so callers that only *plan* a rewrite — `doctor --fix --dry-run` —
206
+ * can report the same outcome the real run would produce, instead of promising
207
+ * a change the opt-out would suppress.
208
+ */
209
+ export function shouldEnforceCodexCliFileAuthStore() {
203
210
  const override = (process.env.CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE ?? "1").trim();
204
211
  return override !== "0";
205
212
  }
206
- function ensureTrailingNewline(value) {
207
- return value.endsWith("\n") ? value : `${value}\n`;
213
+ /**
214
+ * Matches a complete TOML table header, mirroring `readTomlTableName` in
215
+ * `lib/runtime/config-toml.ts` (plus an optional trailing comment).
216
+ *
217
+ * A loose `/^\s*\[/` would also match a continuation line of a multi-line array
218
+ * such as `[1, 2],`, cutting the top-level scan short. The writer would then
219
+ * miss a real top-level assignment below it and insert a second one — a
220
+ * duplicate key, which is invalid TOML and stops the official CLI from starting.
221
+ */
222
+ const TOML_TABLE_HEADER_PATTERN = /^\s*\[{1,2}\s*[^\]]+?\s*\]{1,2}\s*(?:#.*)?$/;
223
+ function ensureTrailingNewline(value, lineEnding = "\n") {
224
+ return value.endsWith("\n") ? value : `${value}${lineEnding}`;
208
225
  }
209
- async function ensureCodexCliFileAuthStore(configPath) {
226
+ /**
227
+ * Reads the top-level `cli_auth_credentials_store` value out of a raw
228
+ * `config.toml`.
229
+ *
230
+ * Mirrors the scoping rules of {@link ensureCodexCliFileAuthStore}: assignments
231
+ * inside a `[table]` belong to that table, so only lines before the first table
232
+ * header describe the official CLI's default credential store. Reporting a
233
+ * profile-scoped value as the effective one would let `doctor` call a config
234
+ * healthy while the CLI still reaches for the keychain.
235
+ *
236
+ * Both TOML string forms count. A literal string (`'file'`) is as valid as a
237
+ * basic string (`"file"`), so treating only the latter as real would make
238
+ * `doctor` warn about a perfectly healthy config. Literal strings have no
239
+ * escape sequences, and the values in play here are bare identifiers, so a
240
+ * non-greedy character class is an exact match for both forms.
241
+ *
242
+ * @param rawConfig - Full text of `config.toml`.
243
+ * @returns The declared mode, or `null` when no top-level assignment exists.
244
+ */
245
+ export function readTopLevelCodexCliAuthStoreMode(rawConfig) {
246
+ for (const line of rawConfig.split(/\r?\n/)) {
247
+ if (TOML_TABLE_HEADER_PATTERN.test(line))
248
+ break;
249
+ const match = line.match(/^\s*cli_auth_credentials_store\s*=\s*(?:"([^"]*)"|'([^']*)')\s*(?:#.*)?$/);
250
+ if (match)
251
+ return (match[1] ?? match[2] ?? "").trim() || null;
252
+ }
253
+ return null;
254
+ }
255
+ /**
256
+ * Persists `cli_auth_credentials_store = "file"` as a top-level key in the
257
+ * official Codex CLI `config.toml`.
258
+ *
259
+ * The wrapper already appends `-c cli_auth_credentials_store="file"` to every
260
+ * command it forwards, but that only covers processes it launches itself.
261
+ * Third-party front-ends (menu-bar apps such as CodexBar, editor extensions)
262
+ * exec the official binary directly, so the *persisted* value is what decides
263
+ * whether the official CLI reaches into the macOS login keychain. Leaving it on
264
+ * `"keychain"` is what produces the repeated "Always Allow" prompts reported in
265
+ * issue #641.
266
+ *
267
+ * Only top-level assignments are rewritten. A `cli_auth_credentials_store`
268
+ * inside a `[profiles.*]` table must be left alone: rewriting it in place would
269
+ * mangle a scoped setting while still leaving the top-level default pointed at
270
+ * the keychain, so the prompts would continue.
271
+ *
272
+ * @param configPath - Config file to reconcile; defaults to the resolved Codex
273
+ * CLI `config.toml`.
274
+ * @returns `true` when the file was rewritten, `false` when it was already
275
+ * correct or enforcement is disabled.
276
+ */
277
+ export async function ensureCodexCliFileAuthStore(configPath = getCodexCliConfigPath()) {
210
278
  if (!shouldEnforceCodexCliFileAuthStore())
211
279
  return false;
212
280
  const desired = 'cli_auth_credentials_store = "file"';
213
281
  const raw = existsSync(configPath) ? await fs.readFile(configPath, "utf-8") : "";
282
+ // Quote style is not ours to normalize: a literal-string `'file'` already
283
+ // keeps the CLI off the keychain, so rewriting it to `"file"` would churn a
284
+ // healthy config on every wrapper startup for no behavioral gain.
285
+ if (readTopLevelCodexCliAuthStoreMode(raw) === "file")
286
+ return false;
287
+ // Preserve the file's existing line endings, as the runtime config rewriter
288
+ // does. Splitting on /\r?\n/ and rejoining with "\n" would silently convert a
289
+ // CRLF config.toml to LF -- an edit the user never asked for, and one this
290
+ // code now risks on every wrapper startup rather than only on account switch.
291
+ const lineEnding = raw.includes("\r\n") ? "\r\n" : "\n";
214
292
  const lines = raw.length > 0 ? raw.split(/\r?\n/) : [];
215
293
  const assignmentRegex = /^\s*cli_auth_credentials_store\s*=/;
216
294
  let replaced = false;
295
+ let inTopLevelTable = true;
217
296
  const nextLines = lines.map((line) => {
218
- if (!replaced && assignmentRegex.test(line)) {
297
+ if (TOML_TABLE_HEADER_PATTERN.test(line))
298
+ inTopLevelTable = false;
299
+ if (inTopLevelTable && !replaced && assignmentRegex.test(line)) {
219
300
  replaced = true;
220
301
  return desired;
221
302
  }
222
303
  return line;
223
304
  });
224
305
  if (!replaced) {
225
- let insertAt = nextLines.findIndex((line) => /^\s*\[/.test(line));
306
+ let insertAt = nextLines.findIndex((line) => TOML_TABLE_HEADER_PATTERN.test(line));
226
307
  if (insertAt < 0)
227
308
  insertAt = nextLines.length;
228
309
  nextLines.splice(insertAt, 0, desired);
@@ -230,8 +311,8 @@ async function ensureCodexCliFileAuthStore(configPath) {
230
311
  nextLines.splice(insertAt + 1, 0, "");
231
312
  }
232
313
  }
233
- const nextRaw = ensureTrailingNewline(nextLines.join("\n"));
234
- if (nextRaw === ensureTrailingNewline(raw)) {
314
+ const nextRaw = ensureTrailingNewline(nextLines.join(lineEnding), lineEnding);
315
+ if (nextRaw === ensureTrailingNewline(raw, lineEnding)) {
235
316
  return false;
236
317
  }
237
318
  await atomicWriteText(configPath, nextRaw);
@@ -1 +1 @@
1
- {"version":3,"file":"repair-commands.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/repair-commands.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"repair-commands.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/repair-commands.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,7 @@ import { isCodexUnavailableError } from "../errors.js";
9
9
  import { queuedRefresh } from "../refresh-queue.js";
10
10
  import { findMatchingAccountIndex, getStoragePath, loadAccounts, loadFlaggedAccounts, setStoragePath, withAccountStorageTransaction, withAccountAndFlaggedStorageTransaction, withFlaggedStorageTransaction, } from "../storage.js";
11
11
  import { getCodexCliAuthPath, getCodexCliConfigPath, loadCodexCliState, } from "../codex-cli/state.js";
12
- import { setCodexCliActiveSelection } from "../codex-cli/writer.js";
12
+ import { ensureCodexCliFileAuthStore, readTopLevelCodexCliAuthStoreMode, setCodexCliActiveSelection, shouldEnforceCodexCliFileAuthStore, } from "../codex-cli/writer.js";
13
13
  import { MODEL_FAMILIES } from "../prompts/codex.js";
14
14
  import { DEFAULT_PROBE_MODEL, resolveNormalizedModel } from "../request/helpers/model-map.js";
15
15
  import { loadPersistedRuntimeObservabilitySnapshot } from "../runtime/runtime-observability.js";
@@ -1300,14 +1300,16 @@ export async function runDoctor(args, deps) {
1300
1300
  : "Codex config file does not exist",
1301
1301
  details: codexConfigPath,
1302
1302
  });
1303
+ const supplementalFixActions = [];
1304
+ // Tracked separately from account storage: pinning the auth store is a real
1305
+ // remediation even on a machine with no accounts yet, which is exactly the
1306
+ // state a user hitting repeated keychain prompts tends to be in.
1307
+ let authStoreFixChanged = false;
1303
1308
  let codexAuthStoreMode;
1304
1309
  if (codexConfigFileExists) {
1305
1310
  try {
1306
1311
  const configRaw = await fs.readFile(codexConfigPath, "utf-8");
1307
- const match = configRaw.match(/^\s*cli_auth_credentials_store\s*=\s*"([^"]+)"\s*$/m);
1308
- if (match?.[1]) {
1309
- codexAuthStoreMode = match[1].trim();
1310
- }
1312
+ codexAuthStoreMode = readTopLevelCodexCliAuthStoreMode(configRaw) ?? undefined;
1311
1313
  }
1312
1314
  catch (error) {
1313
1315
  addCheck({
@@ -1319,13 +1321,59 @@ export async function runDoctor(args, deps) {
1319
1321
  }
1320
1322
  }
1321
1323
  if (!checks.some((check) => check.key === "codex-auth-store")) {
1324
+ // A config that is not pinned to "file" is what makes the official CLI
1325
+ // reach into the macOS login keychain, so `--fix` repairs it directly
1326
+ // instead of waiting for the incidental `pendingCodexActiveSync` path
1327
+ // below, which only fires when there is an active account to mirror.
1328
+ if (options.fix &&
1329
+ codexAuthStoreMode !== "file" &&
1330
+ shouldEnforceCodexCliFileAuthStore()) {
1331
+ if (options.dryRun) {
1332
+ authStoreFixChanged = true;
1333
+ supplementalFixActions.push({
1334
+ key: "codex-auth-store",
1335
+ message: "Prepared Codex CLI credential store pin to file in config.toml (dry-run)",
1336
+ });
1337
+ }
1338
+ else {
1339
+ try {
1340
+ if (await ensureCodexCliFileAuthStore(codexConfigPath)) {
1341
+ codexAuthStoreMode = "file";
1342
+ authStoreFixChanged = true;
1343
+ supplementalFixActions.push({
1344
+ key: "codex-auth-store",
1345
+ message: "Pinned Codex CLI credential store to file in config.toml",
1346
+ });
1347
+ }
1348
+ }
1349
+ catch (error) {
1350
+ addCheck({
1351
+ key: "codex-auth-store-fix",
1352
+ severity: "warn",
1353
+ message: "Failed to pin Codex CLI credential store to file",
1354
+ details: error instanceof Error ? error.message : String(error),
1355
+ });
1356
+ }
1357
+ }
1358
+ }
1359
+ const authStoreDetails = [
1360
+ codexAuthStoreMode ? `mode=${codexAuthStoreMode}` : "mode=unset",
1361
+ ];
1362
+ if (codexAuthStoreMode !== "file" && !options.fix) {
1363
+ authStoreDetails.push("run `codex-multi-auth doctor --fix` to pin it to file");
1364
+ }
1365
+ if (process.platform === "darwin" && codexAuthStoreMode === "file") {
1366
+ // Purely informational: reading or deleting keychain items would
1367
+ // itself raise the prompt this check exists to eliminate.
1368
+ authStoreDetails.push("credentials saved by an earlier official `codex login` may still sit in the login keychain; they are no longer read and can be removed manually via Keychain Access");
1369
+ }
1322
1370
  addCheck({
1323
1371
  key: "codex-auth-store",
1324
1372
  severity: codexAuthStoreMode === "file" ? "ok" : "warn",
1325
1373
  message: codexAuthStoreMode === "file"
1326
1374
  ? "Codex auth storage is set to file"
1327
1375
  : "Codex auth storage is not explicitly set to file",
1328
- details: codexAuthStoreMode ? `mode=${codexAuthStoreMode}` : "mode=unset",
1376
+ details: authStoreDetails.join("; "),
1329
1377
  });
1330
1378
  }
1331
1379
  const codexCliState = await loadCodexCliState({ forceRefresh: true });
@@ -1343,7 +1391,6 @@ export async function runDoctor(args, deps) {
1343
1391
  let fixChanged = false;
1344
1392
  let storageFixChanged = false;
1345
1393
  let structuralFixActions = [];
1346
- const supplementalFixActions = [];
1347
1394
  let doctorRefreshMutation = null;
1348
1395
  let pendingCodexActiveSync = null;
1349
1396
  if (options.fix && storageForChecks && storageForChecks.accounts.length > 0) {
@@ -1677,7 +1724,9 @@ export async function runDoctor(args, deps) {
1677
1724
  }
1678
1725
  }
1679
1726
  const fixActions = [...structuralFixActions, ...supplementalFixActions];
1680
- if (options.fix && storageForChecks && storageForChecks.accounts.length > 0) {
1727
+ if (options.fix &&
1728
+ ((storageForChecks && storageForChecks.accounts.length > 0) ||
1729
+ authStoreFixChanged)) {
1681
1730
  fixChanged = storageFixChanged || fixActions.length > 0;
1682
1731
  addCheck({
1683
1732
  key: "auto-fix",
@@ -1,13 +1,18 @@
1
- export declare const FIRST_RUN_MARKER_VERSION = 1;
2
- type FirstRunStepStatus = "completed" | "skipped" | "failed";
1
+ export declare const FIRST_RUN_MARKER_VERSION = 2;
2
+ export type FirstRunStepStatus = "completed" | "skipped" | "failed";
3
3
  interface FirstRunSetupOutcome {
4
4
  appBind: FirstRunStepStatus;
5
5
  launcher: FirstRunStepStatus;
6
+ authStore: FirstRunStepStatus;
6
7
  }
7
8
  type FirstRunSkipReason = "ci" | "not-installed" | "already-done" | "claim-race" | "error";
8
9
  export type FirstRunResult = {
9
10
  ran: false;
10
11
  reason: FirstRunSkipReason;
12
+ } | {
13
+ ran: false;
14
+ reason: "migrated";
15
+ authStore: FirstRunStepStatus;
11
16
  } | ({
12
17
  ran: true;
13
18
  } & FirstRunSetupOutcome);
@@ -19,6 +24,7 @@ export interface FirstRunSetupDeps {
19
24
  resolveRotation?: () => boolean;
20
25
  bindCodexApp?: () => Promise<FirstRunStepStatus>;
21
26
  installLauncher?: () => Promise<FirstRunStepStatus>;
27
+ enforceAuthStore?: () => Promise<FirstRunStepStatus>;
22
28
  notify?: (message: string) => void;
23
29
  now?: () => number;
24
30
  }
@@ -68,10 +74,10 @@ type LauncherInstallFn = (options: {
68
74
  */
69
75
  export declare function loadLauncherInstall(candidateOverride?: readonly string[]): Promise<LauncherInstallFn | null>;
70
76
  /**
71
- * Runs the lazily deferred install setup (Codex app bind + launcher routing)
72
- * exactly once per runtime root. Never throws and never blocks a command on
73
- * failure: every error path resolves with a skip/failed status and only
74
- * debug-logs sanitized messages.
77
+ * Runs the lazily deferred install setup (Codex auth-store enforcement + app
78
+ * bind + launcher routing) exactly once per runtime root. Never throws and
79
+ * never blocks a command on failure: every error path resolves with a
80
+ * skip/failed status and only debug-logs sanitized messages.
75
81
  */
76
82
  export declare function ensureFirstRunSetup(deps?: FirstRunSetupDeps): Promise<FirstRunResult>;
77
83
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"first-run.d.ts","sourceRoot":"","sources":["../../../lib/runtime/first-run.ts"],"names":[],"mappings":"AA2BA,eAAO,MAAM,wBAAwB,IAAI,CAAC;AAkB1C,KAAK,kBAAkB,GAAG,WAAW,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE7D,UAAU,oBAAoB;IAC7B,OAAO,EAAE,kBAAkB,CAAC;IAC5B,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,KAAK,kBAAkB,GACpB,IAAI,GACJ,eAAe,GACf,cAAc,GACd,YAAY,GACZ,OAAO,CAAC;AAEX,MAAM,MAAM,cAAc,GACvB;IAAE,GAAG,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC1C,CAAC;IAAE,GAAG,EAAE,IAAI,CAAA;CAAE,GAAG,oBAAoB,CAAC,CAAC;AAE1C,MAAM,WAAW,iBAAiB;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC;IACjC,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACjD,eAAe,CAAC,EAAE,MAAM,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACnB;AAgBD,wBAAgB,eAAe,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAG7E;AAeD,MAAM,WAAW,0BAA0B;IAC1C,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CACjC,OAAO,GAAE,0BAA+B,GACtC,OAAO,CA8BT;AAED,wBAAgB,sBAAsB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,kBAAkB,GAAE,MAAM,OACgC,GACxD,OAAO,CAWT;AAED,MAAM,WAAW,uBAAuB;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,OAAO,CAAC;CACrB;AAED,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,uBAAuB,GAC9B,OAAO,CAcT;AAED,MAAM,WAAW,2BAA2B;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,eAAe,EAAE,OAAO,CAAC;CACzB;AAED,wBAAgB,uCAAuC,CACtD,OAAO,EAAE,2BAA2B,GAClC,OAAO,CAUT;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CACxC,UAAU,GAAE,MAAuC,EACnD,GAAG,GAAE,MAAsB,GACzB,OAAO,CAST;AAkGD,KAAK,iBAAiB,GAAG,CAAC,OAAO,EAAE;IAClC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/B,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAEvB;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACxC,iBAAiB,CAAC,EAAE,SAAS,MAAM,EAAE,GACnC,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAenC;AAkCD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CACxC,IAAI,GAAE,iBAAsB,GAC1B,OAAO,CAAC,cAAc,CAAC,CAoDzB"}
1
+ {"version":3,"file":"first-run.d.ts","sourceRoot":"","sources":["../../../lib/runtime/first-run.ts"],"names":[],"mappings":"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"}
@@ -9,12 +9,13 @@
9
9
  // a failure is debug-logged (messages only — never tokens or emails) and the
10
10
  // command proceeds normally.
11
11
  import { randomBytes } from "node:crypto";
12
- import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, } from "node:fs";
13
13
  import { open, rename, unlink } from "node:fs/promises";
14
14
  import { homedir } from "node:os";
15
15
  import { basename, dirname, isAbsolute, join, relative, sep } from "node:path";
16
16
  import process from "node:process";
17
17
  import { fileURLToPath, pathToFileURL } from "node:url";
18
+ import { ensureCodexCliFileAuthStore } from "../codex-cli/writer.js";
18
19
  import { getCodexRuntimeRotationProxy, loadPluginConfig } from "../config.js";
19
20
  import { withFileOperationRetry } from "../fs-retry.js";
20
21
  import { createLogger } from "../logger.js";
@@ -22,7 +23,8 @@ import { getCodexMultiAuthDir } from "../runtime-paths.js";
22
23
  import { bindCodexAppRuntimeRotation, getAppBindStatus } from "./app-bind.js";
23
24
  const log = createLogger("first-run");
24
25
  const FIRST_RUN_MARKER_FILE = "first-run-setup.json";
25
- export const FIRST_RUN_MARKER_VERSION = 1;
26
+ // v2 adds the `authStore` step to the marker payload.
27
+ export const FIRST_RUN_MARKER_VERSION = 2;
26
28
  const TRUE_VALUES = new Set(["1", "true", "yes"]);
27
29
  const FALSE_VALUES = new Set(["0", "false", "no"]);
28
30
  const CI_ENV_KEYS = [
@@ -202,12 +204,16 @@ async function atomicWriteMarker(target, content) {
202
204
  });
203
205
  }
204
206
  async function finalizeFirstRunMarker(markerPath, startedAt, outcome, now) {
207
+ // A failed auth-store step records a pre-v2 version so the next invocation
208
+ // replays it through the migration path — which reruns only that step, never
209
+ // app bind or launcher install.
205
210
  const payload = `${JSON.stringify({
206
- version: FIRST_RUN_MARKER_VERSION,
211
+ version: markerVersionFor(outcome.authStore, undefined),
207
212
  startedAt,
208
213
  completedAt: now(),
209
214
  appBind: outcome.appBind,
210
215
  launcher: outcome.launcher,
216
+ authStore: outcome.authStore,
211
217
  }, null, "\t")}\n`;
212
218
  try {
213
219
  await atomicWriteMarker(markerPath, payload);
@@ -254,6 +260,110 @@ async function defaultBindCodexApp(env, rotationEnabled, detectDesktopApp, notif
254
260
  }
255
261
  return "completed";
256
262
  }
263
+ /**
264
+ * Pins the official Codex CLI to the file-backed credential store.
265
+ *
266
+ * Enforcement used to be lazy — it only happened when something called
267
+ * `setCodexCliActiveSelection` (switch, login, health check, repair). A user who
268
+ * installed the package and drove Codex through a third-party front-end could
269
+ * therefore stay on `cli_auth_credentials_store = "keychain"` indefinitely and
270
+ * keep getting macOS login-keychain prompts (issue #641). Doing it at first run
271
+ * closes that window before the official CLI ever reads its store.
272
+ *
273
+ * Deliberately not gated on `isCodexCliSyncEnabled()`: turning CLI sync off is a
274
+ * statement about account mirroring, not a request to re-enable keychain
275
+ * prompts. The `CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE=0` opt-out is
276
+ * honored inside `ensureCodexCliFileAuthStore` itself.
277
+ */
278
+ async function defaultEnforceAuthStore() {
279
+ return (await ensureCodexCliFileAuthStore()) ? "completed" : "skipped";
280
+ }
281
+ /**
282
+ * Decides the version to persist after an auth-store attempt.
283
+ *
284
+ * Recording the current version after a *failed* step would strand the user:
285
+ * `ensureFirstRunSetup` short-circuits on version alone, so a single transient
286
+ * Windows `EPERM`/`EBUSY` on a locked `config.toml` would leave the CLI on
287
+ * keychain mode permanently — the exact symptom this whole change exists to
288
+ * remove. Staying pre-v2 costs one extra marker read per invocation and
289
+ * self-heals as soon as the write succeeds, because the step is idempotent.
290
+ *
291
+ * `"skipped"` deliberately does bump. It is the normal outcome for a config
292
+ * already pinned to `"file"`, so treating it as unfinished would mean the
293
+ * marker never reaches the current version for healthy installs. It also covers
294
+ * an explicit `CODEX_MULTI_AUTH_ENFORCE_CLI_FILE_AUTH_STORE=0` opt-out — a user
295
+ * choice, not a failure — and if that opt-out is later removed, the
296
+ * wrapper-startup guard and `doctor --fix` both still reconcile the config.
297
+ */
298
+ function markerVersionFor(authStore, previousVersion) {
299
+ if (authStore === "failed")
300
+ return previousVersion ?? 1;
301
+ return FIRST_RUN_MARKER_VERSION;
302
+ }
303
+ function readFirstRunMarker(markerPath) {
304
+ try {
305
+ const parsed = JSON.parse(readFileSync(markerPath, "utf8"));
306
+ return typeof parsed === "object" && parsed !== null
307
+ ? parsed
308
+ : null;
309
+ }
310
+ catch {
311
+ // An unreadable or truncated marker is treated as pre-v2 and migrated;
312
+ // the migration only replays the idempotent auth-store step.
313
+ return null;
314
+ }
315
+ }
316
+ /**
317
+ * Brings a marker written before the auth-store step existed up to the current
318
+ * version.
319
+ *
320
+ * `ensureFirstRunSetup` short-circuits on marker existence, so bumping
321
+ * `FIRST_RUN_MARKER_VERSION` on its own would leave every already-installed
322
+ * user — precisely the population reporting repeated keychain prompts — without
323
+ * the enforcement step forever.
324
+ *
325
+ * Only the auth-store step is replayed. Rerunning app bind or launcher install
326
+ * would reinstall shortcuts and rebind the desktop app, which the user may have
327
+ * deliberately removed since.
328
+ *
329
+ * No exclusive claim is taken, unlike the initial setup. Both operations here
330
+ * are idempotent and atomic — `ensureCodexCliFileAuthStore` skips a config
331
+ * already on `"file"`, and `atomicWriteMarker` renames into place — so
332
+ * concurrent invocations converge on the same result instead of racing.
333
+ */
334
+ async function migrateFirstRunMarker(markerPath, marker, deps, now) {
335
+ let authStore;
336
+ try {
337
+ authStore = await (deps.enforceAuthStore
338
+ ? deps.enforceAuthStore()
339
+ : defaultEnforceAuthStore());
340
+ }
341
+ catch (error) {
342
+ authStore = "failed";
343
+ log.debug("first-run marker migration auth store step skipped", {
344
+ error: errorMessage(error),
345
+ });
346
+ }
347
+ const payload = `${JSON.stringify({
348
+ version: markerVersionFor(authStore, marker.version),
349
+ startedAt: marker.startedAt ?? now(),
350
+ completedAt: marker.completedAt ?? now(),
351
+ appBind: marker.appBind ?? "skipped",
352
+ launcher: marker.launcher ?? "skipped",
353
+ authStore,
354
+ }, null, "\t")}\n`;
355
+ try {
356
+ await atomicWriteMarker(markerPath, payload);
357
+ }
358
+ catch (error) {
359
+ // Leaving the marker at v1 just means the idempotent step runs again on
360
+ // the next invocation, so this is safe to swallow.
361
+ log.debug("first-run marker migration write failed", {
362
+ error: errorMessage(error),
363
+ });
364
+ }
365
+ return authStore;
366
+ }
257
367
  async function defaultInstallLauncher(env, rotationEnabled, notify) {
258
368
  if (!shouldInstallCodexAppLauncherOnFirstRun({ env, rotationEnabled })) {
259
369
  return "skipped";
@@ -265,10 +375,10 @@ async function defaultInstallLauncher(env, rotationEnabled, notify) {
265
375
  return "completed";
266
376
  }
267
377
  /**
268
- * Runs the lazily deferred install setup (Codex app bind + launcher routing)
269
- * exactly once per runtime root. Never throws and never blocks a command on
270
- * failure: every error path resolves with a skip/failed status and only
271
- * debug-logs sanitized messages.
378
+ * Runs the lazily deferred install setup (Codex auth-store enforcement + app
379
+ * bind + launcher routing) exactly once per runtime root. Never throws and
380
+ * never blocks a command on failure: every error path resolves with a
381
+ * skip/failed status and only debug-logs sanitized messages.
272
382
  */
273
383
  export async function ensureFirstRunSetup(deps = {}) {
274
384
  try {
@@ -279,9 +389,18 @@ export async function ensureFirstRunSetup(deps = {}) {
279
389
  if (!installed)
280
390
  return { ran: false, reason: "not-installed" };
281
391
  const markerPath = deps.markerPath ?? getFirstRunMarkerPath();
282
- if (existsSync(markerPath))
283
- return { ran: false, reason: "already-done" };
284
392
  const now = deps.now ?? Date.now;
393
+ if (existsSync(markerPath)) {
394
+ const marker = readFirstRunMarker(markerPath);
395
+ if ((marker?.version ?? 1) >= FIRST_RUN_MARKER_VERSION) {
396
+ return { ran: false, reason: "already-done" };
397
+ }
398
+ return {
399
+ ran: false,
400
+ reason: "migrated",
401
+ authStore: await migrateFirstRunMarker(markerPath, marker ?? {}, deps, now),
402
+ };
403
+ }
285
404
  const startedAt = now();
286
405
  if (!claimFirstRunMarker(markerPath, startedAt)) {
287
406
  return { ran: false, reason: "claim-race" };
@@ -296,7 +415,19 @@ export async function ensureFirstRunSetup(deps = {}) {
296
415
  const outcome = {
297
416
  appBind: "skipped",
298
417
  launcher: "skipped",
418
+ authStore: "skipped",
299
419
  };
420
+ try {
421
+ outcome.authStore = await (deps.enforceAuthStore
422
+ ? deps.enforceAuthStore()
423
+ : defaultEnforceAuthStore());
424
+ }
425
+ catch (error) {
426
+ outcome.authStore = "failed";
427
+ log.debug("first-run codex auth store enforcement skipped", {
428
+ error: errorMessage(error),
429
+ });
430
+ }
300
431
  try {
301
432
  outcome.appBind = await (deps.bindCodexApp
302
433
  ? deps.bindCodexApp()