codex-team 0.0.5 → 0.0.7

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.
package/dist/cli.cjs CHANGED
@@ -13,8 +13,9 @@ var __webpack_modules__ = {
13
13
  const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
14
14
  var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
15
15
  var package_namespaceObject = {
16
- rE: "0.0.5"
16
+ rE: "0.0.7"
17
17
  };
18
+ const external_node_crypto_namespaceObject = require("node:crypto");
18
19
  const promises_namespaceObject = require("node:fs/promises");
19
20
  function isRecord(value) {
20
21
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -37,6 +38,29 @@ var __webpack_modules__ = {
37
38
  if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
38
39
  return value;
39
40
  }
41
+ function normalizeAuthMode(authMode) {
42
+ return authMode.trim().toLowerCase();
43
+ }
44
+ function isApiKeyAuthMode(authMode) {
45
+ return "apikey" === normalizeAuthMode(authMode);
46
+ }
47
+ function isSupportedChatGPTAuthMode(authMode) {
48
+ const normalized = normalizeAuthMode(authMode);
49
+ return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
50
+ }
51
+ function fingerprintApiKey(apiKey) {
52
+ return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
53
+ }
54
+ function getSnapshotIdentity(snapshot) {
55
+ if (isApiKeyAuthMode(snapshot.auth_mode)) {
56
+ const apiKey = snapshot.OPENAI_API_KEY;
57
+ if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
58
+ return `key_${fingerprintApiKey(apiKey)}`;
59
+ }
60
+ const accountId = snapshot.tokens?.account_id;
61
+ if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
62
+ return accountId;
63
+ }
40
64
  function defaultQuotaSnapshot() {
41
65
  return {
42
66
  status: "stale"
@@ -81,15 +105,28 @@ var __webpack_modules__ = {
81
105
  }
82
106
  if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
83
107
  const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
84
- if (!isRecord(parsed.tokens)) throw new Error('Field "tokens" must be an object.');
85
- const accountId = asNonEmptyString(parsed.tokens.account_id, "tokens.account_id");
108
+ const tokens = parsed.tokens;
109
+ if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
110
+ const apiKeyMode = isApiKeyAuthMode(authMode);
111
+ const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
112
+ if (apiKeyMode) {
113
+ if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
114
+ } else {
115
+ if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
116
+ asNonEmptyString(tokens.account_id, "tokens.account_id");
117
+ }
86
118
  return {
87
119
  ...parsed,
88
120
  auth_mode: authMode,
89
- tokens: {
90
- ...parsed.tokens,
91
- account_id: accountId
92
- }
121
+ OPENAI_API_KEY: normalizedApiKey,
122
+ ...isRecord(tokens) ? {
123
+ tokens: {
124
+ ...tokens,
125
+ ..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
126
+ account_id: tokens.account_id
127
+ } : {}
128
+ }
129
+ } : {}
93
130
  };
94
131
  }
95
132
  async function readAuthSnapshotFile(filePath) {
@@ -101,7 +138,7 @@ var __webpack_modules__ = {
101
138
  return {
102
139
  name,
103
140
  auth_mode: snapshot.auth_mode,
104
- account_id: snapshot.tokens.account_id,
141
+ account_id: getSnapshotIdentity(snapshot),
105
142
  created_at: existingCreatedAt ?? timestamp,
106
143
  updated_at: timestamp,
107
144
  last_switched_at: null,
@@ -158,16 +195,13 @@ var __webpack_modules__ = {
158
195
  const value = payload[key];
159
196
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
160
197
  }
161
- function isSupportedChatGPTMode(authMode) {
162
- const normalized = authMode.trim().toLowerCase();
163
- return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
164
- }
165
198
  function parsePlanType(snapshot) {
199
+ const tokens = snapshot.tokens ?? {};
166
200
  for (const tokenName of [
167
201
  "id_token",
168
202
  "access_token"
169
203
  ]){
170
- const token = snapshot.tokens[tokenName];
204
+ const token = tokens[tokenName];
171
205
  if ("string" == typeof token && "" !== token.trim()) try {
172
206
  const payload = decodeJwtPayload(token);
173
207
  const authClaim = extractAuthClaim(payload);
@@ -178,10 +212,11 @@ var __webpack_modules__ = {
178
212
  }
179
213
  function extractChatGPTAuth(snapshot) {
180
214
  const authMode = snapshot.auth_mode ?? "";
181
- const supported = isSupportedChatGPTMode(authMode);
182
- const accessTokenValue = snapshot.tokens.access_token;
183
- const refreshTokenValue = snapshot.tokens.refresh_token;
184
- const directAccountId = snapshot.tokens.account_id;
215
+ const supported = isSupportedChatGPTAuthMode(authMode);
216
+ const tokens = snapshot.tokens ?? {};
217
+ const accessTokenValue = tokens.access_token;
218
+ const refreshTokenValue = tokens.refresh_token;
219
+ const directAccountId = tokens.account_id;
185
220
  let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
186
221
  let planType;
187
222
  let issuer;
@@ -190,7 +225,7 @@ var __webpack_modules__ = {
190
225
  "id_token",
191
226
  "access_token"
192
227
  ]){
193
- const token = snapshot.tokens[tokenName];
228
+ const token = tokens[tokenName];
194
229
  if ("string" == typeof token && "" !== token.trim()) try {
195
230
  const payload = decodeJwtPayload(token);
196
231
  const authClaim = extractAuthClaim(payload);
@@ -372,7 +407,7 @@ var __webpack_modules__ = {
372
407
  ...snapshot,
373
408
  last_refresh: (options.now ?? new Date()).toISOString(),
374
409
  tokens: {
375
- ...snapshot.tokens,
410
+ ...snapshot.tokens ?? {},
376
411
  access_token: payload.access_token,
377
412
  id_token: payload.id_token,
378
413
  refresh_token: payload.refresh_token ?? extracted.refreshToken,
@@ -421,6 +456,7 @@ var __webpack_modules__ = {
421
456
  codexDir,
422
457
  codexTeamDir,
423
458
  currentAuthPath: (0, external_node_path_namespaceObject.join)(codexDir, "auth.json"),
459
+ currentConfigPath: (0, external_node_path_namespaceObject.join)(codexDir, "config.toml"),
424
460
  accountsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "accounts"),
425
461
  backupsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "backups"),
426
462
  statePath: (0, external_node_path_namespaceObject.join)(codexTeamDir, "state.json")
@@ -513,17 +549,49 @@ var __webpack_modules__ = {
513
549
  accountMetaPath(name) {
514
550
  return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "meta.json");
515
551
  }
552
+ accountConfigPath(name) {
553
+ return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "config.toml");
554
+ }
516
555
  async writeAccountAuthSnapshot(name, snapshot) {
517
556
  await atomicWriteFile(this.accountAuthPath(name), stringifyJson(snapshot));
518
557
  }
519
558
  async writeAccountMeta(name, meta) {
520
559
  await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
521
560
  }
561
+ validateConfigSnapshot(name, snapshot, rawConfig) {
562
+ if ("apikey" !== snapshot.auth_mode) return;
563
+ if (!rawConfig) throw new Error(`Current ~/.codex/config.toml is required to save apikey account "${name}".`);
564
+ if (!/^\s*model_provider\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing model_provider for apikey account "${name}".`);
565
+ if (!/^\s*base_url\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing base_url for apikey account "${name}".`);
566
+ }
567
+ sanitizeConfigForAccountAuth(rawConfig) {
568
+ const lines = rawConfig.split(/\r?\n/u);
569
+ const result = [];
570
+ let skippingProviderSection = false;
571
+ for (const line of lines){
572
+ const trimmed = line.trim();
573
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
574
+ skippingProviderSection = /^\[model_providers\.[^\]]+\]$/u.test(trimmed);
575
+ if (skippingProviderSection) continue;
576
+ }
577
+ if (!skippingProviderSection) {
578
+ if (!/^\s*model_provider\s*=/u.test(line)) {
579
+ if (!/^\s*preferred_auth_method\s*=\s*["']apikey["']\s*$/u.test(line)) result.push(line);
580
+ }
581
+ }
582
+ }
583
+ return `${result.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
584
+ }
585
+ async ensureEmptyAccountConfigSnapshot(name) {
586
+ const configPath = this.accountConfigPath(name);
587
+ await atomicWriteFile(configPath, "");
588
+ return configPath;
589
+ }
522
590
  async syncCurrentAuthIfMatching(snapshot) {
523
591
  if (!await pathExists(this.paths.currentAuthPath)) return;
524
592
  try {
525
593
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
526
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
594
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
527
595
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
528
596
  } catch {}
529
597
  }
@@ -579,11 +647,12 @@ var __webpack_modules__ = {
579
647
  ]);
580
648
  const meta = parseSnapshotMeta(rawMeta);
581
649
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
582
- if (meta.account_id !== snapshot.tokens.account_id) throw new Error(`Account metadata account_id mismatch for "${name}".`);
650
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
583
651
  return {
584
652
  ...meta,
585
653
  authPath,
586
654
  metaPath,
655
+ configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
587
656
  duplicateAccountId: false
588
657
  };
589
658
  }
@@ -622,11 +691,12 @@ var __webpack_modules__ = {
622
691
  warnings
623
692
  };
624
693
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
625
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
694
+ const currentIdentity = getSnapshotIdentity(snapshot);
695
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
626
696
  return {
627
697
  exists: true,
628
698
  auth_mode: snapshot.auth_mode,
629
- account_id: snapshot.tokens.account_id,
699
+ account_id: currentIdentity,
630
700
  matched_accounts: matchedAccounts,
631
701
  managed: matchedAccounts.length > 0,
632
702
  duplicate_match: matchedAccounts.length > 1,
@@ -639,14 +709,21 @@ var __webpack_modules__ = {
639
709
  if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
640
710
  const rawSnapshot = await readJsonFile(this.paths.currentAuthPath);
641
711
  const snapshot = parseAuthSnapshot(rawSnapshot);
712
+ const rawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
642
713
  const accountDir = this.accountDirectory(name);
643
714
  const authPath = this.accountAuthPath(name);
644
715
  const metaPath = this.accountMetaPath(name);
716
+ const configPath = this.accountConfigPath(name);
645
717
  const accountExists = await pathExists(accountDir);
646
718
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
647
719
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
720
+ this.validateConfigSnapshot(name, snapshot, rawConfig);
648
721
  await ensureDirectory(accountDir, DIRECTORY_MODE);
649
722
  await atomicWriteFile(authPath, `${rawSnapshot.trimEnd()}\n`);
723
+ if ("apikey" === snapshot.auth_mode && rawConfig) await atomicWriteFile(configPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
724
+ else if (await pathExists(configPath)) await (0, promises_namespaceObject.rm)(configPath, {
725
+ force: true
726
+ });
650
727
  const meta = createSnapshotMeta(name, snapshot, new Date(), existingMeta?.created_at);
651
728
  meta.last_switched_at = existingMeta?.last_switched_at ?? null;
652
729
  meta.quota = existingMeta?.quota ?? meta.quota;
@@ -662,9 +739,15 @@ var __webpack_modules__ = {
662
739
  const name = current.matched_accounts[0];
663
740
  const currentRawSnapshot = await readJsonFile(this.paths.currentAuthPath);
664
741
  const currentSnapshot = parseAuthSnapshot(currentRawSnapshot);
742
+ const currentRawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
665
743
  const metaPath = this.accountMetaPath(name);
666
744
  const existingMeta = parseSnapshotMeta(await readJsonFile(metaPath));
745
+ this.validateConfigSnapshot(name, currentSnapshot, currentRawConfig);
667
746
  await atomicWriteFile(this.accountAuthPath(name), `${currentRawSnapshot.trimEnd()}\n`);
747
+ if ("apikey" === currentSnapshot.auth_mode && currentRawConfig) await atomicWriteFile(this.accountConfigPath(name), currentRawConfig.endsWith("\n") ? currentRawConfig : `${currentRawConfig}\n`);
748
+ else if (await pathExists(this.accountConfigPath(name))) await (0, promises_namespaceObject.rm)(this.accountConfigPath(name), {
749
+ force: true
750
+ });
668
751
  await atomicWriteFile(metaPath, stringifyJson({
669
752
  ...createSnapshotMeta(name, currentSnapshot, new Date(), existingMeta.created_at),
670
753
  last_switched_at: existingMeta.last_switched_at,
@@ -686,10 +769,25 @@ var __webpack_modules__ = {
686
769
  await (0, promises_namespaceObject.copyFile)(this.paths.currentAuthPath, backupPath);
687
770
  await chmodIfPossible(backupPath, FILE_MODE);
688
771
  }
772
+ if (await pathExists(this.paths.currentConfigPath)) {
773
+ const configBackupPath = (0, external_node_path_namespaceObject.join)(this.paths.backupsDir, "last-active-config.toml");
774
+ await (0, promises_namespaceObject.copyFile)(this.paths.currentConfigPath, configBackupPath);
775
+ await chmodIfPossible(configBackupPath, FILE_MODE);
776
+ }
689
777
  const rawAuth = await readJsonFile(account.authPath);
690
778
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
779
+ if ("apikey" === account.auth_mode && account.configPath) {
780
+ const rawConfig = await readJsonFile(account.configPath);
781
+ await atomicWriteFile(this.paths.currentConfigPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
782
+ } else if ("apikey" === account.auth_mode) {
783
+ await this.ensureEmptyAccountConfigSnapshot(name);
784
+ warnings.push(`Saved apikey account "${name}" was missing config.toml snapshot. Created an empty snapshot; configure baseUrl manually if needed.`);
785
+ } else if (await pathExists(this.paths.currentConfigPath)) {
786
+ const currentRawConfig = await readJsonFile(this.paths.currentConfigPath);
787
+ await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
788
+ }
691
789
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
692
- if (writtenSnapshot.tokens.account_id !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
790
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
693
791
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
694
792
  meta.last_switched_at = new Date().toISOString();
695
793
  meta.updated_at = meta.last_switched_at;
@@ -733,7 +831,7 @@ var __webpack_modules__ = {
733
831
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
734
832
  }
735
833
  meta.auth_mode = result.authSnapshot.auth_mode;
736
- meta.account_id = result.authSnapshot.tokens.account_id;
834
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
737
835
  meta.updated_at = now.toISOString();
738
836
  meta.quota = result.quota;
739
837
  await this.writeAccountMeta(name, meta);
@@ -845,7 +943,8 @@ var __webpack_modules__ = {
845
943
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
846
944
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
847
945
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
848
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares account_id ${account.account_id} with another saved account.`);
946
+ if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
947
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
849
948
  }
850
949
  let currentAuthPresent = false;
851
950
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -928,7 +1027,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
928
1027
  if (status.exists) {
929
1028
  lines.push("Current auth: present");
930
1029
  lines.push(`Auth mode: ${status.auth_mode}`);
931
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
1030
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
932
1031
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
933
1032
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
934
1033
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1061,7 +1160,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1061
1160
  },
1062
1161
  {
1063
1162
  key: "account_id",
1064
- label: "ACCOUNT ID"
1163
+ label: "IDENTITY"
1065
1164
  },
1066
1165
  {
1067
1166
  key: "plan_type",
package/dist/main.cjs CHANGED
@@ -43,8 +43,9 @@ var timezone_js_default = /*#__PURE__*/ __webpack_require__.n(timezone_js_namesp
43
43
  const utc_js_namespaceObject = require("dayjs/plugin/utc.js");
44
44
  var utc_js_default = /*#__PURE__*/ __webpack_require__.n(utc_js_namespaceObject);
45
45
  var package_namespaceObject = {
46
- rE: "0.0.5"
46
+ rE: "0.0.7"
47
47
  };
48
+ const external_node_crypto_namespaceObject = require("node:crypto");
48
49
  const promises_namespaceObject = require("node:fs/promises");
49
50
  function isRecord(value) {
50
51
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -67,6 +68,29 @@ function asOptionalNumber(value, fieldName) {
67
68
  if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
68
69
  return value;
69
70
  }
71
+ function normalizeAuthMode(authMode) {
72
+ return authMode.trim().toLowerCase();
73
+ }
74
+ function isApiKeyAuthMode(authMode) {
75
+ return "apikey" === normalizeAuthMode(authMode);
76
+ }
77
+ function isSupportedChatGPTAuthMode(authMode) {
78
+ const normalized = normalizeAuthMode(authMode);
79
+ return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
80
+ }
81
+ function fingerprintApiKey(apiKey) {
82
+ return (0, external_node_crypto_namespaceObject.createHash)("sha256").update(apiKey).digest("hex").slice(0, 16);
83
+ }
84
+ function getSnapshotIdentity(snapshot) {
85
+ if (isApiKeyAuthMode(snapshot.auth_mode)) {
86
+ const apiKey = snapshot.OPENAI_API_KEY;
87
+ if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
88
+ return `key_${fingerprintApiKey(apiKey)}`;
89
+ }
90
+ const accountId = snapshot.tokens?.account_id;
91
+ if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
92
+ return accountId;
93
+ }
70
94
  function defaultQuotaSnapshot() {
71
95
  return {
72
96
  status: "stale"
@@ -111,15 +135,28 @@ function parseAuthSnapshot(raw) {
111
135
  }
112
136
  if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
113
137
  const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
114
- if (!isRecord(parsed.tokens)) throw new Error('Field "tokens" must be an object.');
115
- const accountId = asNonEmptyString(parsed.tokens.account_id, "tokens.account_id");
138
+ const tokens = parsed.tokens;
139
+ if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
140
+ const apiKeyMode = isApiKeyAuthMode(authMode);
141
+ const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
142
+ if (apiKeyMode) {
143
+ if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
144
+ } else {
145
+ if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
146
+ asNonEmptyString(tokens.account_id, "tokens.account_id");
147
+ }
116
148
  return {
117
149
  ...parsed,
118
150
  auth_mode: authMode,
119
- tokens: {
120
- ...parsed.tokens,
121
- account_id: accountId
122
- }
151
+ OPENAI_API_KEY: normalizedApiKey,
152
+ ...isRecord(tokens) ? {
153
+ tokens: {
154
+ ...tokens,
155
+ ..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
156
+ account_id: tokens.account_id
157
+ } : {}
158
+ }
159
+ } : {}
123
160
  };
124
161
  }
125
162
  async function readAuthSnapshotFile(filePath) {
@@ -131,7 +168,7 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
131
168
  return {
132
169
  name,
133
170
  auth_mode: snapshot.auth_mode,
134
- account_id: snapshot.tokens.account_id,
171
+ account_id: getSnapshotIdentity(snapshot),
135
172
  created_at: existingCreatedAt ?? timestamp,
136
173
  updated_at: timestamp,
137
174
  last_switched_at: null,
@@ -188,16 +225,13 @@ function extractStringClaim(payload, key) {
188
225
  const value = payload[key];
189
226
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
190
227
  }
191
- function isSupportedChatGPTMode(authMode) {
192
- const normalized = authMode.trim().toLowerCase();
193
- return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
194
- }
195
228
  function parsePlanType(snapshot) {
229
+ const tokens = snapshot.tokens ?? {};
196
230
  for (const tokenName of [
197
231
  "id_token",
198
232
  "access_token"
199
233
  ]){
200
- const token = snapshot.tokens[tokenName];
234
+ const token = tokens[tokenName];
201
235
  if ("string" == typeof token && "" !== token.trim()) try {
202
236
  const payload = decodeJwtPayload(token);
203
237
  const authClaim = extractAuthClaim(payload);
@@ -208,10 +242,11 @@ function parsePlanType(snapshot) {
208
242
  }
209
243
  function extractChatGPTAuth(snapshot) {
210
244
  const authMode = snapshot.auth_mode ?? "";
211
- const supported = isSupportedChatGPTMode(authMode);
212
- const accessTokenValue = snapshot.tokens.access_token;
213
- const refreshTokenValue = snapshot.tokens.refresh_token;
214
- const directAccountId = snapshot.tokens.account_id;
245
+ const supported = isSupportedChatGPTAuthMode(authMode);
246
+ const tokens = snapshot.tokens ?? {};
247
+ const accessTokenValue = tokens.access_token;
248
+ const refreshTokenValue = tokens.refresh_token;
249
+ const directAccountId = tokens.account_id;
215
250
  let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
216
251
  let planType;
217
252
  let issuer;
@@ -220,7 +255,7 @@ function extractChatGPTAuth(snapshot) {
220
255
  "id_token",
221
256
  "access_token"
222
257
  ]){
223
- const token = snapshot.tokens[tokenName];
258
+ const token = tokens[tokenName];
224
259
  if ("string" == typeof token && "" !== token.trim()) try {
225
260
  const payload = decodeJwtPayload(token);
226
261
  const authClaim = extractAuthClaim(payload);
@@ -402,7 +437,7 @@ async function refreshChatGPTAuthTokens(snapshot, options) {
402
437
  ...snapshot,
403
438
  last_refresh: (options.now ?? new Date()).toISOString(),
404
439
  tokens: {
405
- ...snapshot.tokens,
440
+ ...snapshot.tokens ?? {},
406
441
  access_token: payload.access_token,
407
442
  id_token: payload.id_token,
408
443
  refresh_token: payload.refresh_token ?? extracted.refreshToken,
@@ -451,6 +486,7 @@ function defaultPaths(homeDir = (0, external_node_os_namespaceObject.homedir)())
451
486
  codexDir,
452
487
  codexTeamDir,
453
488
  currentAuthPath: (0, external_node_path_namespaceObject.join)(codexDir, "auth.json"),
489
+ currentConfigPath: (0, external_node_path_namespaceObject.join)(codexDir, "config.toml"),
454
490
  accountsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "accounts"),
455
491
  backupsDir: (0, external_node_path_namespaceObject.join)(codexTeamDir, "backups"),
456
492
  statePath: (0, external_node_path_namespaceObject.join)(codexTeamDir, "state.json")
@@ -543,17 +579,49 @@ class AccountStore {
543
579
  accountMetaPath(name) {
544
580
  return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "meta.json");
545
581
  }
582
+ accountConfigPath(name) {
583
+ return (0, external_node_path_namespaceObject.join)(this.accountDirectory(name), "config.toml");
584
+ }
546
585
  async writeAccountAuthSnapshot(name, snapshot) {
547
586
  await atomicWriteFile(this.accountAuthPath(name), stringifyJson(snapshot));
548
587
  }
549
588
  async writeAccountMeta(name, meta) {
550
589
  await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
551
590
  }
591
+ validateConfigSnapshot(name, snapshot, rawConfig) {
592
+ if ("apikey" !== snapshot.auth_mode) return;
593
+ if (!rawConfig) throw new Error(`Current ~/.codex/config.toml is required to save apikey account "${name}".`);
594
+ if (!/^\s*model_provider\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing model_provider for apikey account "${name}".`);
595
+ if (!/^\s*base_url\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing base_url for apikey account "${name}".`);
596
+ }
597
+ sanitizeConfigForAccountAuth(rawConfig) {
598
+ const lines = rawConfig.split(/\r?\n/u);
599
+ const result = [];
600
+ let skippingProviderSection = false;
601
+ for (const line of lines){
602
+ const trimmed = line.trim();
603
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
604
+ skippingProviderSection = /^\[model_providers\.[^\]]+\]$/u.test(trimmed);
605
+ if (skippingProviderSection) continue;
606
+ }
607
+ if (!skippingProviderSection) {
608
+ if (!/^\s*model_provider\s*=/u.test(line)) {
609
+ if (!/^\s*preferred_auth_method\s*=\s*["']apikey["']\s*$/u.test(line)) result.push(line);
610
+ }
611
+ }
612
+ }
613
+ return `${result.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
614
+ }
615
+ async ensureEmptyAccountConfigSnapshot(name) {
616
+ const configPath = this.accountConfigPath(name);
617
+ await atomicWriteFile(configPath, "");
618
+ return configPath;
619
+ }
552
620
  async syncCurrentAuthIfMatching(snapshot) {
553
621
  if (!await pathExists(this.paths.currentAuthPath)) return;
554
622
  try {
555
623
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
556
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
624
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
557
625
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
558
626
  } catch {}
559
627
  }
@@ -609,11 +677,12 @@ class AccountStore {
609
677
  ]);
610
678
  const meta = parseSnapshotMeta(rawMeta);
611
679
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
612
- if (meta.account_id !== snapshot.tokens.account_id) throw new Error(`Account metadata account_id mismatch for "${name}".`);
680
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
613
681
  return {
614
682
  ...meta,
615
683
  authPath,
616
684
  metaPath,
685
+ configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
617
686
  duplicateAccountId: false
618
687
  };
619
688
  }
@@ -652,11 +721,12 @@ class AccountStore {
652
721
  warnings
653
722
  };
654
723
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
655
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
724
+ const currentIdentity = getSnapshotIdentity(snapshot);
725
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
656
726
  return {
657
727
  exists: true,
658
728
  auth_mode: snapshot.auth_mode,
659
- account_id: snapshot.tokens.account_id,
729
+ account_id: currentIdentity,
660
730
  matched_accounts: matchedAccounts,
661
731
  managed: matchedAccounts.length > 0,
662
732
  duplicate_match: matchedAccounts.length > 1,
@@ -669,14 +739,21 @@ class AccountStore {
669
739
  if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
670
740
  const rawSnapshot = await readJsonFile(this.paths.currentAuthPath);
671
741
  const snapshot = parseAuthSnapshot(rawSnapshot);
742
+ const rawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
672
743
  const accountDir = this.accountDirectory(name);
673
744
  const authPath = this.accountAuthPath(name);
674
745
  const metaPath = this.accountMetaPath(name);
746
+ const configPath = this.accountConfigPath(name);
675
747
  const accountExists = await pathExists(accountDir);
676
748
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
677
749
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
750
+ this.validateConfigSnapshot(name, snapshot, rawConfig);
678
751
  await ensureDirectory(accountDir, DIRECTORY_MODE);
679
752
  await atomicWriteFile(authPath, `${rawSnapshot.trimEnd()}\n`);
753
+ if ("apikey" === snapshot.auth_mode && rawConfig) await atomicWriteFile(configPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
754
+ else if (await pathExists(configPath)) await (0, promises_namespaceObject.rm)(configPath, {
755
+ force: true
756
+ });
680
757
  const meta = createSnapshotMeta(name, snapshot, new Date(), existingMeta?.created_at);
681
758
  meta.last_switched_at = existingMeta?.last_switched_at ?? null;
682
759
  meta.quota = existingMeta?.quota ?? meta.quota;
@@ -692,9 +769,15 @@ class AccountStore {
692
769
  const name = current.matched_accounts[0];
693
770
  const currentRawSnapshot = await readJsonFile(this.paths.currentAuthPath);
694
771
  const currentSnapshot = parseAuthSnapshot(currentRawSnapshot);
772
+ const currentRawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
695
773
  const metaPath = this.accountMetaPath(name);
696
774
  const existingMeta = parseSnapshotMeta(await readJsonFile(metaPath));
775
+ this.validateConfigSnapshot(name, currentSnapshot, currentRawConfig);
697
776
  await atomicWriteFile(this.accountAuthPath(name), `${currentRawSnapshot.trimEnd()}\n`);
777
+ if ("apikey" === currentSnapshot.auth_mode && currentRawConfig) await atomicWriteFile(this.accountConfigPath(name), currentRawConfig.endsWith("\n") ? currentRawConfig : `${currentRawConfig}\n`);
778
+ else if (await pathExists(this.accountConfigPath(name))) await (0, promises_namespaceObject.rm)(this.accountConfigPath(name), {
779
+ force: true
780
+ });
698
781
  await atomicWriteFile(metaPath, stringifyJson({
699
782
  ...createSnapshotMeta(name, currentSnapshot, new Date(), existingMeta.created_at),
700
783
  last_switched_at: existingMeta.last_switched_at,
@@ -716,10 +799,25 @@ class AccountStore {
716
799
  await (0, promises_namespaceObject.copyFile)(this.paths.currentAuthPath, backupPath);
717
800
  await chmodIfPossible(backupPath, FILE_MODE);
718
801
  }
802
+ if (await pathExists(this.paths.currentConfigPath)) {
803
+ const configBackupPath = (0, external_node_path_namespaceObject.join)(this.paths.backupsDir, "last-active-config.toml");
804
+ await (0, promises_namespaceObject.copyFile)(this.paths.currentConfigPath, configBackupPath);
805
+ await chmodIfPossible(configBackupPath, FILE_MODE);
806
+ }
719
807
  const rawAuth = await readJsonFile(account.authPath);
720
808
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
809
+ if ("apikey" === account.auth_mode && account.configPath) {
810
+ const rawConfig = await readJsonFile(account.configPath);
811
+ await atomicWriteFile(this.paths.currentConfigPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
812
+ } else if ("apikey" === account.auth_mode) {
813
+ await this.ensureEmptyAccountConfigSnapshot(name);
814
+ warnings.push(`Saved apikey account "${name}" was missing config.toml snapshot. Created an empty snapshot; configure baseUrl manually if needed.`);
815
+ } else if (await pathExists(this.paths.currentConfigPath)) {
816
+ const currentRawConfig = await readJsonFile(this.paths.currentConfigPath);
817
+ await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
818
+ }
721
819
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
722
- if (writtenSnapshot.tokens.account_id !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
820
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
723
821
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
724
822
  meta.last_switched_at = new Date().toISOString();
725
823
  meta.updated_at = meta.last_switched_at;
@@ -763,7 +861,7 @@ class AccountStore {
763
861
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
764
862
  }
765
863
  meta.auth_mode = result.authSnapshot.auth_mode;
766
- meta.account_id = result.authSnapshot.tokens.account_id;
864
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
767
865
  meta.updated_at = now.toISOString();
768
866
  meta.quota = result.quota;
769
867
  await this.writeAccountMeta(name, meta);
@@ -875,7 +973,8 @@ class AccountStore {
875
973
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
876
974
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
877
975
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
878
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares account_id ${account.account_id} with another saved account.`);
976
+ if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
977
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
879
978
  }
880
979
  let currentAuthPresent = false;
881
980
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -958,7 +1057,7 @@ function describeCurrentStatus(status) {
958
1057
  if (status.exists) {
959
1058
  lines.push("Current auth: present");
960
1059
  lines.push(`Auth mode: ${status.auth_mode}`);
961
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
1060
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
962
1061
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
963
1062
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
964
1063
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1091,7 +1190,7 @@ function describeQuotaAccounts(accounts, warnings) {
1091
1190
  },
1092
1191
  {
1093
1192
  key: "account_id",
1094
- label: "ACCOUNT ID"
1193
+ label: "IDENTITY"
1095
1194
  },
1096
1195
  {
1097
1196
  key: "plan_type",
package/dist/main.js CHANGED
@@ -2,13 +2,14 @@ import { stderr, stdin, stdout as external_node_process_stdout } from "node:proc
2
2
  import dayjs from "dayjs";
3
3
  import timezone from "dayjs/plugin/timezone.js";
4
4
  import utc from "dayjs/plugin/utc.js";
5
+ import { createHash } from "node:crypto";
5
6
  import { chmod, copyFile, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
6
7
  import { homedir } from "node:os";
7
8
  import { basename, dirname, join } from "node:path";
8
9
  import { execFile } from "node:child_process";
9
10
  import { promisify } from "node:util";
10
11
  var package_namespaceObject = {
11
- rE: "0.0.5"
12
+ rE: "0.0.7"
12
13
  };
13
14
  function isRecord(value) {
14
15
  return "object" == typeof value && null !== value && !Array.isArray(value);
@@ -31,6 +32,29 @@ function asOptionalNumber(value, fieldName) {
31
32
  if ("number" != typeof value || Number.isNaN(value)) throw new Error(`Field "${fieldName}" must be a number.`);
32
33
  return value;
33
34
  }
35
+ function normalizeAuthMode(authMode) {
36
+ return authMode.trim().toLowerCase();
37
+ }
38
+ function isApiKeyAuthMode(authMode) {
39
+ return "apikey" === normalizeAuthMode(authMode);
40
+ }
41
+ function isSupportedChatGPTAuthMode(authMode) {
42
+ const normalized = normalizeAuthMode(authMode);
43
+ return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
44
+ }
45
+ function fingerprintApiKey(apiKey) {
46
+ return createHash("sha256").update(apiKey).digest("hex").slice(0, 16);
47
+ }
48
+ function getSnapshotIdentity(snapshot) {
49
+ if (isApiKeyAuthMode(snapshot.auth_mode)) {
50
+ const apiKey = snapshot.OPENAI_API_KEY;
51
+ if ("string" != typeof apiKey || "" === apiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
52
+ return `key_${fingerprintApiKey(apiKey)}`;
53
+ }
54
+ const accountId = snapshot.tokens?.account_id;
55
+ if ("string" != typeof accountId || "" === accountId.trim()) throw new Error('Field "tokens.account_id" must be a non-empty string.');
56
+ return accountId;
57
+ }
34
58
  function defaultQuotaSnapshot() {
35
59
  return {
36
60
  status: "stale"
@@ -75,15 +99,28 @@ function parseAuthSnapshot(raw) {
75
99
  }
76
100
  if (!isRecord(parsed)) throw new Error("Auth snapshot must be a JSON object.");
77
101
  const authMode = asNonEmptyString(parsed.auth_mode, "auth_mode");
78
- if (!isRecord(parsed.tokens)) throw new Error('Field "tokens" must be an object.');
79
- const accountId = asNonEmptyString(parsed.tokens.account_id, "tokens.account_id");
102
+ const tokens = parsed.tokens;
103
+ if (null != tokens && !isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
104
+ const apiKeyMode = isApiKeyAuthMode(authMode);
105
+ const normalizedApiKey = null === parsed.OPENAI_API_KEY || void 0 === parsed.OPENAI_API_KEY ? parsed.OPENAI_API_KEY : asNonEmptyString(parsed.OPENAI_API_KEY, "OPENAI_API_KEY");
106
+ if (apiKeyMode) {
107
+ if ("string" != typeof normalizedApiKey || "" === normalizedApiKey.trim()) throw new Error('Field "OPENAI_API_KEY" must be a non-empty string for apikey auth.');
108
+ } else {
109
+ if (!isRecord(tokens)) throw new Error('Field "tokens" must be an object.');
110
+ asNonEmptyString(tokens.account_id, "tokens.account_id");
111
+ }
80
112
  return {
81
113
  ...parsed,
82
114
  auth_mode: authMode,
83
- tokens: {
84
- ...parsed.tokens,
85
- account_id: accountId
86
- }
115
+ OPENAI_API_KEY: normalizedApiKey,
116
+ ...isRecord(tokens) ? {
117
+ tokens: {
118
+ ...tokens,
119
+ ..."string" == typeof tokens.account_id && "" !== tokens.account_id.trim() ? {
120
+ account_id: tokens.account_id
121
+ } : {}
122
+ }
123
+ } : {}
87
124
  };
88
125
  }
89
126
  async function readAuthSnapshotFile(filePath) {
@@ -95,7 +132,7 @@ function createSnapshotMeta(name, snapshot, now, existingCreatedAt) {
95
132
  return {
96
133
  name,
97
134
  auth_mode: snapshot.auth_mode,
98
- account_id: snapshot.tokens.account_id,
135
+ account_id: getSnapshotIdentity(snapshot),
99
136
  created_at: existingCreatedAt ?? timestamp,
100
137
  updated_at: timestamp,
101
138
  last_switched_at: null,
@@ -148,16 +185,13 @@ function extractStringClaim(payload, key) {
148
185
  const value = payload[key];
149
186
  return "string" == typeof value && "" !== value.trim() ? value : void 0;
150
187
  }
151
- function isSupportedChatGPTMode(authMode) {
152
- const normalized = authMode.trim().toLowerCase();
153
- return "chatgpt" === normalized || "chatgpt_auth_tokens" === normalized;
154
- }
155
188
  function parsePlanType(snapshot) {
189
+ const tokens = snapshot.tokens ?? {};
156
190
  for (const tokenName of [
157
191
  "id_token",
158
192
  "access_token"
159
193
  ]){
160
- const token = snapshot.tokens[tokenName];
194
+ const token = tokens[tokenName];
161
195
  if ("string" == typeof token && "" !== token.trim()) try {
162
196
  const payload = decodeJwtPayload(token);
163
197
  const authClaim = extractAuthClaim(payload);
@@ -168,10 +202,11 @@ function parsePlanType(snapshot) {
168
202
  }
169
203
  function extractChatGPTAuth(snapshot) {
170
204
  const authMode = snapshot.auth_mode ?? "";
171
- const supported = isSupportedChatGPTMode(authMode);
172
- const accessTokenValue = snapshot.tokens.access_token;
173
- const refreshTokenValue = snapshot.tokens.refresh_token;
174
- const directAccountId = snapshot.tokens.account_id;
205
+ const supported = isSupportedChatGPTAuthMode(authMode);
206
+ const tokens = snapshot.tokens ?? {};
207
+ const accessTokenValue = tokens.access_token;
208
+ const refreshTokenValue = tokens.refresh_token;
209
+ const directAccountId = tokens.account_id;
175
210
  let accountId = "string" == typeof directAccountId && "" !== directAccountId.trim() ? directAccountId : void 0;
176
211
  let planType;
177
212
  let issuer;
@@ -180,7 +215,7 @@ function extractChatGPTAuth(snapshot) {
180
215
  "id_token",
181
216
  "access_token"
182
217
  ]){
183
- const token = snapshot.tokens[tokenName];
218
+ const token = tokens[tokenName];
184
219
  if ("string" == typeof token && "" !== token.trim()) try {
185
220
  const payload = decodeJwtPayload(token);
186
221
  const authClaim = extractAuthClaim(payload);
@@ -362,7 +397,7 @@ async function refreshChatGPTAuthTokens(snapshot, options) {
362
397
  ...snapshot,
363
398
  last_refresh: (options.now ?? new Date()).toISOString(),
364
399
  tokens: {
365
- ...snapshot.tokens,
400
+ ...snapshot.tokens ?? {},
366
401
  access_token: payload.access_token,
367
402
  id_token: payload.id_token,
368
403
  refresh_token: payload.refresh_token ?? extracted.refreshToken,
@@ -411,6 +446,7 @@ function defaultPaths(homeDir = homedir()) {
411
446
  codexDir,
412
447
  codexTeamDir,
413
448
  currentAuthPath: join(codexDir, "auth.json"),
449
+ currentConfigPath: join(codexDir, "config.toml"),
414
450
  accountsDir: join(codexTeamDir, "accounts"),
415
451
  backupsDir: join(codexTeamDir, "backups"),
416
452
  statePath: join(codexTeamDir, "state.json")
@@ -503,17 +539,49 @@ class AccountStore {
503
539
  accountMetaPath(name) {
504
540
  return join(this.accountDirectory(name), "meta.json");
505
541
  }
542
+ accountConfigPath(name) {
543
+ return join(this.accountDirectory(name), "config.toml");
544
+ }
506
545
  async writeAccountAuthSnapshot(name, snapshot) {
507
546
  await atomicWriteFile(this.accountAuthPath(name), stringifyJson(snapshot));
508
547
  }
509
548
  async writeAccountMeta(name, meta) {
510
549
  await atomicWriteFile(this.accountMetaPath(name), stringifyJson(meta));
511
550
  }
551
+ validateConfigSnapshot(name, snapshot, rawConfig) {
552
+ if ("apikey" !== snapshot.auth_mode) return;
553
+ if (!rawConfig) throw new Error(`Current ~/.codex/config.toml is required to save apikey account "${name}".`);
554
+ if (!/^\s*model_provider\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing model_provider for apikey account "${name}".`);
555
+ if (!/^\s*base_url\s*=\s*["'][^"']+["']/mu.test(rawConfig)) throw new Error(`Current ~/.codex/config.toml is missing base_url for apikey account "${name}".`);
556
+ }
557
+ sanitizeConfigForAccountAuth(rawConfig) {
558
+ const lines = rawConfig.split(/\r?\n/u);
559
+ const result = [];
560
+ let skippingProviderSection = false;
561
+ for (const line of lines){
562
+ const trimmed = line.trim();
563
+ if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
564
+ skippingProviderSection = /^\[model_providers\.[^\]]+\]$/u.test(trimmed);
565
+ if (skippingProviderSection) continue;
566
+ }
567
+ if (!skippingProviderSection) {
568
+ if (!/^\s*model_provider\s*=/u.test(line)) {
569
+ if (!/^\s*preferred_auth_method\s*=\s*["']apikey["']\s*$/u.test(line)) result.push(line);
570
+ }
571
+ }
572
+ }
573
+ return `${result.join("\n").replace(/\n{3,}/gu, "\n\n").trimEnd()}\n`;
574
+ }
575
+ async ensureEmptyAccountConfigSnapshot(name) {
576
+ const configPath = this.accountConfigPath(name);
577
+ await atomicWriteFile(configPath, "");
578
+ return configPath;
579
+ }
512
580
  async syncCurrentAuthIfMatching(snapshot) {
513
581
  if (!await pathExists(this.paths.currentAuthPath)) return;
514
582
  try {
515
583
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
516
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
584
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
517
585
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
518
586
  } catch {}
519
587
  }
@@ -569,11 +637,12 @@ class AccountStore {
569
637
  ]);
570
638
  const meta = parseSnapshotMeta(rawMeta);
571
639
  if (meta.name !== name) throw new Error(`Account metadata name mismatch for "${name}".`);
572
- if (meta.account_id !== snapshot.tokens.account_id) throw new Error(`Account metadata account_id mismatch for "${name}".`);
640
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
573
641
  return {
574
642
  ...meta,
575
643
  authPath,
576
644
  metaPath,
645
+ configPath: await pathExists(this.accountConfigPath(name)) ? this.accountConfigPath(name) : null,
577
646
  duplicateAccountId: false
578
647
  };
579
648
  }
@@ -612,11 +681,12 @@ class AccountStore {
612
681
  warnings
613
682
  };
614
683
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
615
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
684
+ const currentIdentity = getSnapshotIdentity(snapshot);
685
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
616
686
  return {
617
687
  exists: true,
618
688
  auth_mode: snapshot.auth_mode,
619
- account_id: snapshot.tokens.account_id,
689
+ account_id: currentIdentity,
620
690
  matched_accounts: matchedAccounts,
621
691
  managed: matchedAccounts.length > 0,
622
692
  duplicate_match: matchedAccounts.length > 1,
@@ -629,14 +699,21 @@ class AccountStore {
629
699
  if (!await pathExists(this.paths.currentAuthPath)) throw new Error("Current ~/.codex/auth.json does not exist.");
630
700
  const rawSnapshot = await readJsonFile(this.paths.currentAuthPath);
631
701
  const snapshot = parseAuthSnapshot(rawSnapshot);
702
+ const rawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
632
703
  const accountDir = this.accountDirectory(name);
633
704
  const authPath = this.accountAuthPath(name);
634
705
  const metaPath = this.accountMetaPath(name);
706
+ const configPath = this.accountConfigPath(name);
635
707
  const accountExists = await pathExists(accountDir);
636
708
  const existingMeta = accountExists && await pathExists(metaPath) ? parseSnapshotMeta(await readJsonFile(metaPath)) : void 0;
637
709
  if (accountExists && !force) throw new Error(`Account "${name}" already exists. Use --force to overwrite it.`);
710
+ this.validateConfigSnapshot(name, snapshot, rawConfig);
638
711
  await ensureDirectory(accountDir, DIRECTORY_MODE);
639
712
  await atomicWriteFile(authPath, `${rawSnapshot.trimEnd()}\n`);
713
+ if ("apikey" === snapshot.auth_mode && rawConfig) await atomicWriteFile(configPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
714
+ else if (await pathExists(configPath)) await rm(configPath, {
715
+ force: true
716
+ });
640
717
  const meta = createSnapshotMeta(name, snapshot, new Date(), existingMeta?.created_at);
641
718
  meta.last_switched_at = existingMeta?.last_switched_at ?? null;
642
719
  meta.quota = existingMeta?.quota ?? meta.quota;
@@ -652,9 +729,15 @@ class AccountStore {
652
729
  const name = current.matched_accounts[0];
653
730
  const currentRawSnapshot = await readJsonFile(this.paths.currentAuthPath);
654
731
  const currentSnapshot = parseAuthSnapshot(currentRawSnapshot);
732
+ const currentRawConfig = await pathExists(this.paths.currentConfigPath) ? await readJsonFile(this.paths.currentConfigPath) : null;
655
733
  const metaPath = this.accountMetaPath(name);
656
734
  const existingMeta = parseSnapshotMeta(await readJsonFile(metaPath));
735
+ this.validateConfigSnapshot(name, currentSnapshot, currentRawConfig);
657
736
  await atomicWriteFile(this.accountAuthPath(name), `${currentRawSnapshot.trimEnd()}\n`);
737
+ if ("apikey" === currentSnapshot.auth_mode && currentRawConfig) await atomicWriteFile(this.accountConfigPath(name), currentRawConfig.endsWith("\n") ? currentRawConfig : `${currentRawConfig}\n`);
738
+ else if (await pathExists(this.accountConfigPath(name))) await rm(this.accountConfigPath(name), {
739
+ force: true
740
+ });
658
741
  await atomicWriteFile(metaPath, stringifyJson({
659
742
  ...createSnapshotMeta(name, currentSnapshot, new Date(), existingMeta.created_at),
660
743
  last_switched_at: existingMeta.last_switched_at,
@@ -676,10 +759,25 @@ class AccountStore {
676
759
  await copyFile(this.paths.currentAuthPath, backupPath);
677
760
  await chmodIfPossible(backupPath, FILE_MODE);
678
761
  }
762
+ if (await pathExists(this.paths.currentConfigPath)) {
763
+ const configBackupPath = join(this.paths.backupsDir, "last-active-config.toml");
764
+ await copyFile(this.paths.currentConfigPath, configBackupPath);
765
+ await chmodIfPossible(configBackupPath, FILE_MODE);
766
+ }
679
767
  const rawAuth = await readJsonFile(account.authPath);
680
768
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
769
+ if ("apikey" === account.auth_mode && account.configPath) {
770
+ const rawConfig = await readJsonFile(account.configPath);
771
+ await atomicWriteFile(this.paths.currentConfigPath, rawConfig.endsWith("\n") ? rawConfig : `${rawConfig}\n`);
772
+ } else if ("apikey" === account.auth_mode) {
773
+ await this.ensureEmptyAccountConfigSnapshot(name);
774
+ warnings.push(`Saved apikey account "${name}" was missing config.toml snapshot. Created an empty snapshot; configure baseUrl manually if needed.`);
775
+ } else if (await pathExists(this.paths.currentConfigPath)) {
776
+ const currentRawConfig = await readJsonFile(this.paths.currentConfigPath);
777
+ await atomicWriteFile(this.paths.currentConfigPath, this.sanitizeConfigForAccountAuth(currentRawConfig));
778
+ }
681
779
  const writtenSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
682
- if (writtenSnapshot.tokens.account_id !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
780
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
683
781
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
684
782
  meta.last_switched_at = new Date().toISOString();
685
783
  meta.updated_at = meta.last_switched_at;
@@ -723,7 +821,7 @@ class AccountStore {
723
821
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
724
822
  }
725
823
  meta.auth_mode = result.authSnapshot.auth_mode;
726
- meta.account_id = result.authSnapshot.tokens.account_id;
824
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
727
825
  meta.updated_at = now.toISOString();
728
826
  meta.quota = result.quota;
729
827
  await this.writeAccountMeta(name, meta);
@@ -835,7 +933,8 @@ class AccountStore {
835
933
  const metaStat = await stat(account.metaPath);
836
934
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
837
935
  if ((511 & metaStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" metadata permissions must be 600.`);
838
- if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares account_id ${account.account_id} with another saved account.`);
936
+ if ("apikey" === account.auth_mode && !account.configPath) issues.push(`Account "${account.name}" is missing config.toml snapshot required for apikey auth.`);
937
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
839
938
  }
840
939
  let currentAuthPresent = false;
841
940
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -918,7 +1017,7 @@ function describeCurrentStatus(status) {
918
1017
  if (status.exists) {
919
1018
  lines.push("Current auth: present");
920
1019
  lines.push(`Auth mode: ${status.auth_mode}`);
921
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
1020
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
922
1021
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
923
1022
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
924
1023
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1051,7 +1150,7 @@ function describeQuotaAccounts(accounts, warnings) {
1051
1150
  },
1052
1151
  {
1053
1152
  key: "account_id",
1054
- label: "ACCOUNT ID"
1153
+ label: "IDENTITY"
1055
1154
  },
1056
1155
  {
1057
1156
  key: "plan_type",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-team",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",