codex-team 0.0.5 → 0.0.6

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.6"
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,
@@ -523,7 +558,7 @@ var __webpack_modules__ = {
523
558
  if (!await pathExists(this.paths.currentAuthPath)) return;
524
559
  try {
525
560
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
526
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
561
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
527
562
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
528
563
  } catch {}
529
564
  }
@@ -579,7 +614,7 @@ var __webpack_modules__ = {
579
614
  ]);
580
615
  const meta = parseSnapshotMeta(rawMeta);
581
616
  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}".`);
617
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
583
618
  return {
584
619
  ...meta,
585
620
  authPath,
@@ -622,11 +657,12 @@ var __webpack_modules__ = {
622
657
  warnings
623
658
  };
624
659
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
625
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
660
+ const currentIdentity = getSnapshotIdentity(snapshot);
661
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
626
662
  return {
627
663
  exists: true,
628
664
  auth_mode: snapshot.auth_mode,
629
- account_id: snapshot.tokens.account_id,
665
+ account_id: currentIdentity,
630
666
  matched_accounts: matchedAccounts,
631
667
  managed: matchedAccounts.length > 0,
632
668
  duplicate_match: matchedAccounts.length > 1,
@@ -689,7 +725,7 @@ var __webpack_modules__ = {
689
725
  const rawAuth = await readJsonFile(account.authPath);
690
726
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
691
727
  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}".`);
728
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
693
729
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
694
730
  meta.last_switched_at = new Date().toISOString();
695
731
  meta.updated_at = meta.last_switched_at;
@@ -733,7 +769,7 @@ var __webpack_modules__ = {
733
769
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
734
770
  }
735
771
  meta.auth_mode = result.authSnapshot.auth_mode;
736
- meta.account_id = result.authSnapshot.tokens.account_id;
772
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
737
773
  meta.updated_at = now.toISOString();
738
774
  meta.quota = result.quota;
739
775
  await this.writeAccountMeta(name, meta);
@@ -845,7 +881,7 @@ var __webpack_modules__ = {
845
881
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
846
882
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
847
883
  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.`);
884
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
849
885
  }
850
886
  let currentAuthPresent = false;
851
887
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -928,7 +964,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
928
964
  if (status.exists) {
929
965
  lines.push("Current auth: present");
930
966
  lines.push(`Auth mode: ${status.auth_mode}`);
931
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
967
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
932
968
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
933
969
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
934
970
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1061,7 +1097,7 @@ Account names must match /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.
1061
1097
  },
1062
1098
  {
1063
1099
  key: "account_id",
1064
- label: "ACCOUNT ID"
1100
+ label: "IDENTITY"
1065
1101
  },
1066
1102
  {
1067
1103
  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.6"
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,
@@ -553,7 +588,7 @@ class AccountStore {
553
588
  if (!await pathExists(this.paths.currentAuthPath)) return;
554
589
  try {
555
590
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
556
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
591
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
557
592
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
558
593
  } catch {}
559
594
  }
@@ -609,7 +644,7 @@ class AccountStore {
609
644
  ]);
610
645
  const meta = parseSnapshotMeta(rawMeta);
611
646
  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}".`);
647
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
613
648
  return {
614
649
  ...meta,
615
650
  authPath,
@@ -652,11 +687,12 @@ class AccountStore {
652
687
  warnings
653
688
  };
654
689
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
655
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
690
+ const currentIdentity = getSnapshotIdentity(snapshot);
691
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
656
692
  return {
657
693
  exists: true,
658
694
  auth_mode: snapshot.auth_mode,
659
- account_id: snapshot.tokens.account_id,
695
+ account_id: currentIdentity,
660
696
  matched_accounts: matchedAccounts,
661
697
  managed: matchedAccounts.length > 0,
662
698
  duplicate_match: matchedAccounts.length > 1,
@@ -719,7 +755,7 @@ class AccountStore {
719
755
  const rawAuth = await readJsonFile(account.authPath);
720
756
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
721
757
  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}".`);
758
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
723
759
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
724
760
  meta.last_switched_at = new Date().toISOString();
725
761
  meta.updated_at = meta.last_switched_at;
@@ -763,7 +799,7 @@ class AccountStore {
763
799
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
764
800
  }
765
801
  meta.auth_mode = result.authSnapshot.auth_mode;
766
- meta.account_id = result.authSnapshot.tokens.account_id;
802
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
767
803
  meta.updated_at = now.toISOString();
768
804
  meta.quota = result.quota;
769
805
  await this.writeAccountMeta(name, meta);
@@ -875,7 +911,7 @@ class AccountStore {
875
911
  const metaStat = await (0, promises_namespaceObject.stat)(account.metaPath);
876
912
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
877
913
  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.`);
914
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
879
915
  }
880
916
  let currentAuthPresent = false;
881
917
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -958,7 +994,7 @@ function describeCurrentStatus(status) {
958
994
  if (status.exists) {
959
995
  lines.push("Current auth: present");
960
996
  lines.push(`Auth mode: ${status.auth_mode}`);
961
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
997
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
962
998
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
963
999
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
964
1000
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1091,7 +1127,7 @@ function describeQuotaAccounts(accounts, warnings) {
1091
1127
  },
1092
1128
  {
1093
1129
  key: "account_id",
1094
- label: "ACCOUNT ID"
1130
+ label: "IDENTITY"
1095
1131
  },
1096
1132
  {
1097
1133
  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.6"
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,
@@ -513,7 +548,7 @@ class AccountStore {
513
548
  if (!await pathExists(this.paths.currentAuthPath)) return;
514
549
  try {
515
550
  const currentSnapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
516
- if (currentSnapshot.tokens.account_id !== snapshot.tokens.account_id) return;
551
+ if (getSnapshotIdentity(currentSnapshot) !== getSnapshotIdentity(snapshot)) return;
517
552
  await atomicWriteFile(this.paths.currentAuthPath, stringifyJson(snapshot));
518
553
  } catch {}
519
554
  }
@@ -569,7 +604,7 @@ class AccountStore {
569
604
  ]);
570
605
  const meta = parseSnapshotMeta(rawMeta);
571
606
  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}".`);
607
+ if (meta.account_id !== getSnapshotIdentity(snapshot)) throw new Error(`Account metadata account_id mismatch for "${name}".`);
573
608
  return {
574
609
  ...meta,
575
610
  authPath,
@@ -612,11 +647,12 @@ class AccountStore {
612
647
  warnings
613
648
  };
614
649
  const snapshot = await readAuthSnapshotFile(this.paths.currentAuthPath);
615
- const matchedAccounts = accounts.filter((account)=>account.account_id === snapshot.tokens.account_id).map((account)=>account.name);
650
+ const currentIdentity = getSnapshotIdentity(snapshot);
651
+ const matchedAccounts = accounts.filter((account)=>account.account_id === currentIdentity).map((account)=>account.name);
616
652
  return {
617
653
  exists: true,
618
654
  auth_mode: snapshot.auth_mode,
619
- account_id: snapshot.tokens.account_id,
655
+ account_id: currentIdentity,
620
656
  matched_accounts: matchedAccounts,
621
657
  managed: matchedAccounts.length > 0,
622
658
  duplicate_match: matchedAccounts.length > 1,
@@ -679,7 +715,7 @@ class AccountStore {
679
715
  const rawAuth = await readJsonFile(account.authPath);
680
716
  await atomicWriteFile(this.paths.currentAuthPath, `${rawAuth.trimEnd()}\n`);
681
717
  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}".`);
718
+ if (getSnapshotIdentity(writtenSnapshot) !== account.account_id) throw new Error(`Switch verification failed for account "${name}".`);
683
719
  const meta = parseSnapshotMeta(await readJsonFile(account.metaPath));
684
720
  meta.last_switched_at = new Date().toISOString();
685
721
  meta.updated_at = meta.last_switched_at;
@@ -723,7 +759,7 @@ class AccountStore {
723
759
  await this.syncCurrentAuthIfMatching(result.authSnapshot);
724
760
  }
725
761
  meta.auth_mode = result.authSnapshot.auth_mode;
726
- meta.account_id = result.authSnapshot.tokens.account_id;
762
+ meta.account_id = getSnapshotIdentity(result.authSnapshot);
727
763
  meta.updated_at = now.toISOString();
728
764
  meta.quota = result.quota;
729
765
  await this.writeAccountMeta(name, meta);
@@ -835,7 +871,7 @@ class AccountStore {
835
871
  const metaStat = await stat(account.metaPath);
836
872
  if ((511 & authStat.mode) !== FILE_MODE) issues.push(`Account "${account.name}" auth permissions must be 600.`);
837
873
  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.`);
874
+ if (account.duplicateAccountId) warnings.push(`Account "${account.name}" shares identity ${account.account_id} with another saved account.`);
839
875
  }
840
876
  let currentAuthPresent = false;
841
877
  if (await pathExists(this.paths.currentAuthPath)) {
@@ -918,7 +954,7 @@ function describeCurrentStatus(status) {
918
954
  if (status.exists) {
919
955
  lines.push("Current auth: present");
920
956
  lines.push(`Auth mode: ${status.auth_mode}`);
921
- lines.push(`Account ID: ${maskAccountId(status.account_id ?? "")}`);
957
+ lines.push(`Identity: ${maskAccountId(status.account_id ?? "")}`);
922
958
  if (0 === status.matched_accounts.length) lines.push("Managed account: no (unmanaged)");
923
959
  else if (1 === status.matched_accounts.length) lines.push(`Managed account: ${status.matched_accounts[0]}`);
924
960
  else lines.push(`Managed account: multiple (${status.matched_accounts.join(", ")})`);
@@ -1051,7 +1087,7 @@ function describeQuotaAccounts(accounts, warnings) {
1051
1087
  },
1052
1088
  {
1053
1089
  key: "account_id",
1054
- label: "ACCOUNT ID"
1090
+ label: "IDENTITY"
1055
1091
  },
1056
1092
  {
1057
1093
  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.6",
4
4
  "description": "Manage multiple Codex ChatGPT auth snapshots and quota usage from the command line.",
5
5
  "license": "MIT",
6
6
  "type": "module",