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 +127 -28
- package/dist/main.cjs +127 -28
- package/dist/main.js +127 -28
- package/package.json +1 -1
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.
|
|
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
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
182
|
-
const
|
|
183
|
-
const
|
|
184
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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(`
|
|
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: "
|
|
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.
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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(`
|
|
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: "
|
|
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.
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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.
|
|
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(`
|
|
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: "
|
|
1153
|
+
label: "IDENTITY"
|
|
1055
1154
|
},
|
|
1056
1155
|
{
|
|
1057
1156
|
key: "plan_type",
|