@imdeadpool/codex-account-switcher 0.1.5 → 0.1.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/README.md +68 -2
- package/dist/commands/config.d.ts +15 -0
- package/dist/commands/config.js +81 -0
- package/dist/commands/daemon.d.ts +9 -0
- package/dist/commands/daemon.js +39 -0
- package/dist/commands/list.d.ts +3 -0
- package/dist/commands/list.js +30 -5
- package/dist/commands/login.d.ts +15 -0
- package/dist/commands/login.js +97 -0
- package/dist/commands/remove.d.ts +14 -0
- package/dist/commands/remove.js +104 -0
- package/dist/commands/save.d.ts +4 -1
- package/dist/commands/save.js +24 -6
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +16 -0
- package/dist/lib/accounts/account-service.d.ts +59 -2
- package/dist/lib/accounts/account-service.js +551 -36
- package/dist/lib/accounts/auth-parser.d.ts +3 -0
- package/dist/lib/accounts/auth-parser.js +83 -0
- package/dist/lib/accounts/errors.d.ts +15 -0
- package/dist/lib/accounts/errors.js +34 -2
- package/dist/lib/accounts/index.d.ts +3 -1
- package/dist/lib/accounts/index.js +5 -1
- package/dist/lib/accounts/registry.d.ts +6 -0
- package/dist/lib/accounts/registry.js +166 -0
- package/dist/lib/accounts/service-manager.d.ts +4 -0
- package/dist/lib/accounts/service-manager.js +204 -0
- package/dist/lib/accounts/types.d.ts +71 -0
- package/dist/lib/accounts/types.js +5 -0
- package/dist/lib/accounts/usage.d.ts +10 -0
- package/dist/lib/accounts/usage.js +246 -0
- package/dist/lib/base-command.d.ts +1 -0
- package/dist/lib/base-command.js +4 -0
- package/dist/lib/config/paths.d.ts +6 -0
- package/dist/lib/config/paths.js +46 -5
- package/dist/tests/auth-parser.test.d.ts +1 -0
- package/dist/tests/auth-parser.test.js +65 -0
- package/dist/tests/registry.test.d.ts +1 -0
- package/dist/tests/registry.test.js +37 -0
- package/dist/tests/save-account-safety.test.d.ts +1 -0
- package/dist/tests/save-account-safety.test.js +399 -0
- package/dist/tests/usage.test.d.ts +1 -0
- package/dist/tests/usage.test.js +29 -0
- package/package.json +7 -6
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
11
|
+
const account_service_1 = require("../lib/accounts/account-service");
|
|
12
|
+
const errors_1 = require("../lib/accounts/errors");
|
|
13
|
+
const auth_parser_1 = require("../lib/accounts/auth-parser");
|
|
14
|
+
function encodeBase64Url(input) {
|
|
15
|
+
return Buffer.from(input, "utf8")
|
|
16
|
+
.toString("base64")
|
|
17
|
+
.replace(/\+/g, "-")
|
|
18
|
+
.replace(/\//g, "_")
|
|
19
|
+
.replace(/=+$/g, "");
|
|
20
|
+
}
|
|
21
|
+
function buildAuthPayload(email, options) {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
const accountId = (_a = options === null || options === void 0 ? void 0 : options.accountId) !== null && _a !== void 0 ? _a : "acct-1";
|
|
24
|
+
const userId = (_b = options === null || options === void 0 ? void 0 : options.userId) !== null && _b !== void 0 ? _b : "user-1";
|
|
25
|
+
const idTokenPayload = {
|
|
26
|
+
email,
|
|
27
|
+
"https://api.openai.com/auth": {
|
|
28
|
+
chatgpt_account_id: accountId,
|
|
29
|
+
chatgpt_user_id: userId,
|
|
30
|
+
chatgpt_plan_type: "team",
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
const idToken = `${encodeBase64Url(JSON.stringify({ alg: "none" }))}.${encodeBase64Url(JSON.stringify(idTokenPayload))}.sig`;
|
|
34
|
+
return JSON.stringify({
|
|
35
|
+
tokens: {
|
|
36
|
+
access_token: `token-${email}`,
|
|
37
|
+
refresh_token: `refresh-${email}`,
|
|
38
|
+
id_token: idToken,
|
|
39
|
+
account_id: accountId,
|
|
40
|
+
},
|
|
41
|
+
}, null, 2);
|
|
42
|
+
}
|
|
43
|
+
async function withIsolatedCodexDir(t, fn) {
|
|
44
|
+
const codexDir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "codex-auth-save-"));
|
|
45
|
+
const accountsDir = node_path_1.default.join(codexDir, "accounts");
|
|
46
|
+
const authPath = node_path_1.default.join(codexDir, "auth.json");
|
|
47
|
+
await promises_1.default.mkdir(accountsDir, { recursive: true });
|
|
48
|
+
const previousEnv = {
|
|
49
|
+
CODEX_AUTH_CODEX_DIR: process.env.CODEX_AUTH_CODEX_DIR,
|
|
50
|
+
CODEX_AUTH_ACCOUNTS_DIR: process.env.CODEX_AUTH_ACCOUNTS_DIR,
|
|
51
|
+
CODEX_AUTH_JSON_PATH: process.env.CODEX_AUTH_JSON_PATH,
|
|
52
|
+
CODEX_AUTH_CURRENT_PATH: process.env.CODEX_AUTH_CURRENT_PATH,
|
|
53
|
+
};
|
|
54
|
+
process.env.CODEX_AUTH_CODEX_DIR = codexDir;
|
|
55
|
+
delete process.env.CODEX_AUTH_ACCOUNTS_DIR;
|
|
56
|
+
delete process.env.CODEX_AUTH_JSON_PATH;
|
|
57
|
+
delete process.env.CODEX_AUTH_CURRENT_PATH;
|
|
58
|
+
t.after(async () => {
|
|
59
|
+
process.env.CODEX_AUTH_CODEX_DIR = previousEnv.CODEX_AUTH_CODEX_DIR;
|
|
60
|
+
process.env.CODEX_AUTH_ACCOUNTS_DIR = previousEnv.CODEX_AUTH_ACCOUNTS_DIR;
|
|
61
|
+
process.env.CODEX_AUTH_JSON_PATH = previousEnv.CODEX_AUTH_JSON_PATH;
|
|
62
|
+
process.env.CODEX_AUTH_CURRENT_PATH = previousEnv.CODEX_AUTH_CURRENT_PATH;
|
|
63
|
+
await promises_1.default.rm(codexDir, { recursive: true, force: true });
|
|
64
|
+
});
|
|
65
|
+
await fn({ codexDir, accountsDir, authPath });
|
|
66
|
+
}
|
|
67
|
+
(0, node_test_1.default)("saveAccount blocks overwriting snapshot when existing and incoming emails differ", async (t) => {
|
|
68
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
69
|
+
const service = new account_service_1.AccountService();
|
|
70
|
+
const destinationPath = node_path_1.default.join(accountsDir, "codexina.json");
|
|
71
|
+
await promises_1.default.writeFile(destinationPath, buildAuthPayload("bia@edixai.com"), "utf8");
|
|
72
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("codexina@edixai.com"), "utf8");
|
|
73
|
+
await strict_1.default.rejects(() => service.saveAccount("codexina"), (error) => error instanceof errors_1.SnapshotEmailMismatchError &&
|
|
74
|
+
error.message.includes("bia@edixai.com") &&
|
|
75
|
+
error.message.includes("codexina@edixai.com"));
|
|
76
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(destinationPath);
|
|
77
|
+
strict_1.default.equal(parsed.email, "bia@edixai.com");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
(0, node_test_1.default)("saveAccount allows force overwrite when emails differ", async (t) => {
|
|
81
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
82
|
+
const service = new account_service_1.AccountService();
|
|
83
|
+
const destinationPath = node_path_1.default.join(accountsDir, "codexina.json");
|
|
84
|
+
await promises_1.default.writeFile(destinationPath, buildAuthPayload("bia@edixai.com"), "utf8");
|
|
85
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("codexina@edixai.com"), "utf8");
|
|
86
|
+
await service.saveAccount("codexina", { force: true });
|
|
87
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(destinationPath);
|
|
88
|
+
strict_1.default.equal(parsed.email, "codexina@edixai.com");
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
(0, node_test_1.default)("saveAccount still overwrites when existing snapshot belongs to same email", async (t) => {
|
|
92
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
93
|
+
const service = new account_service_1.AccountService();
|
|
94
|
+
const destinationPath = node_path_1.default.join(accountsDir, "codexina.json");
|
|
95
|
+
await promises_1.default.writeFile(destinationPath, buildAuthPayload("codexina@edixai.com"), "utf8");
|
|
96
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("codexina@edixai.com"), "utf8");
|
|
97
|
+
await strict_1.default.doesNotReject(() => service.saveAccount("codexina"));
|
|
98
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(destinationPath);
|
|
99
|
+
strict_1.default.equal(parsed.email, "codexina@edixai.com");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
(0, node_test_1.default)("saveAccount accepts an email-shaped snapshot name", async (t) => {
|
|
103
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
104
|
+
const service = new account_service_1.AccountService();
|
|
105
|
+
const accountName = "viktor@edia.com";
|
|
106
|
+
const destinationPath = node_path_1.default.join(accountsDir, `${accountName}.json`);
|
|
107
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("viktor@edia.com"), "utf8");
|
|
108
|
+
await strict_1.default.doesNotReject(() => service.saveAccount(accountName));
|
|
109
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(destinationPath);
|
|
110
|
+
strict_1.default.equal(parsed.email, "viktor@edia.com");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
(0, node_test_1.default)("saveAccount accepts an email-shaped snapshot name containing plus aliases", async (t) => {
|
|
114
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
115
|
+
const service = new account_service_1.AccountService();
|
|
116
|
+
const accountName = "viktor+biz@edia.com";
|
|
117
|
+
const destinationPath = node_path_1.default.join(accountsDir, `${accountName}.json`);
|
|
118
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("viktor+biz@edia.com"), "utf8");
|
|
119
|
+
await strict_1.default.doesNotReject(() => service.saveAccount(accountName));
|
|
120
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(destinationPath);
|
|
121
|
+
strict_1.default.equal(parsed.email, "viktor+biz@edia.com");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
(0, node_test_1.default)("saveAccount blocks overwrite when emails match but account identity differs", async (t) => {
|
|
125
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
126
|
+
const service = new account_service_1.AccountService();
|
|
127
|
+
const destinationPath = node_path_1.default.join(accountsDir, "work.json");
|
|
128
|
+
await promises_1.default.writeFile(destinationPath, buildAuthPayload("codexina@edixai.com", {
|
|
129
|
+
accountId: "acct-a",
|
|
130
|
+
userId: "user-a",
|
|
131
|
+
}), "utf8");
|
|
132
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("codexina@edixai.com", {
|
|
133
|
+
accountId: "acct-b",
|
|
134
|
+
userId: "user-a",
|
|
135
|
+
}), "utf8");
|
|
136
|
+
await strict_1.default.rejects(() => service.saveAccount("work"), (error) => error instanceof errors_1.SnapshotEmailMismatchError &&
|
|
137
|
+
error.message.includes("account:acct-a") &&
|
|
138
|
+
error.message.includes("account:acct-b"));
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
(0, node_test_1.default)("inferAccountNameFromCurrentAuth returns email-shaped duplicate suffix for same-email different account identities", async (t) => {
|
|
142
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
143
|
+
const service = new account_service_1.AccountService();
|
|
144
|
+
const firstSnapshotPath = node_path_1.default.join(accountsDir, "codexina@edixai.com.json");
|
|
145
|
+
await promises_1.default.writeFile(firstSnapshotPath, buildAuthPayload("codexina@edixai.com", {
|
|
146
|
+
accountId: "acct-a",
|
|
147
|
+
userId: "user-a",
|
|
148
|
+
}), "utf8");
|
|
149
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("codexina@edixai.com", {
|
|
150
|
+
accountId: "acct-b",
|
|
151
|
+
userId: "user-a",
|
|
152
|
+
}), "utf8");
|
|
153
|
+
const inferred = await service.inferAccountNameFromCurrentAuth();
|
|
154
|
+
strict_1.default.equal(inferred, "codexina@edixai.com--dup-2");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
(0, node_test_1.default)("resolveLoginAccountNameFromCurrentAuth creates an email-shaped duplicate when canonical email snapshot identity differs", async (t) => {
|
|
158
|
+
await withIsolatedCodexDir(t, async ({ accountsDir, authPath }) => {
|
|
159
|
+
const service = new account_service_1.AccountService();
|
|
160
|
+
const email = "csoves@edixai.com";
|
|
161
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, `${email}.json`), buildAuthPayload(email, {
|
|
162
|
+
accountId: "acct-a",
|
|
163
|
+
userId: "user-a",
|
|
164
|
+
}), "utf8");
|
|
165
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload(email, {
|
|
166
|
+
accountId: "acct-b",
|
|
167
|
+
userId: "user-a",
|
|
168
|
+
}), "utf8");
|
|
169
|
+
const resolved = await service.resolveLoginAccountNameFromCurrentAuth();
|
|
170
|
+
strict_1.default.deepEqual(resolved, {
|
|
171
|
+
name: `${email}--dup-2`,
|
|
172
|
+
source: "inferred",
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
(0, node_test_1.default)("resolveLoginAccountNameFromCurrentAuth ignores active alias and infers canonical email snapshot", async (t) => {
|
|
177
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
178
|
+
const service = new account_service_1.AccountService();
|
|
179
|
+
const activeName = "team-primary";
|
|
180
|
+
const email = "csoves@edixai.com";
|
|
181
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, `${activeName}.json`), buildAuthPayload(email, {
|
|
182
|
+
accountId: "acct-a",
|
|
183
|
+
userId: "user-a",
|
|
184
|
+
}), "utf8");
|
|
185
|
+
await promises_1.default.writeFile(node_path_1.default.join(codexDir, "current"), `${activeName}\n`, "utf8");
|
|
186
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload(email, {
|
|
187
|
+
accountId: "acct-b",
|
|
188
|
+
userId: "user-a",
|
|
189
|
+
}), "utf8");
|
|
190
|
+
const resolved = await service.resolveLoginAccountNameFromCurrentAuth();
|
|
191
|
+
strict_1.default.deepEqual(resolved, {
|
|
192
|
+
name: email,
|
|
193
|
+
source: "inferred",
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
(0, node_test_1.default)("resolveLoginAccountNameFromCurrentAuth infers email snapshot when no existing snapshot matches email", async (t) => {
|
|
198
|
+
await withIsolatedCodexDir(t, async ({ authPath }) => {
|
|
199
|
+
const service = new account_service_1.AccountService();
|
|
200
|
+
const email = "new-user@edixai.com";
|
|
201
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload(email, {
|
|
202
|
+
accountId: "acct-new",
|
|
203
|
+
userId: "user-new",
|
|
204
|
+
}), "utf8");
|
|
205
|
+
const resolved = await service.resolveLoginAccountNameFromCurrentAuth();
|
|
206
|
+
strict_1.default.deepEqual(resolved, {
|
|
207
|
+
name: email,
|
|
208
|
+
source: "inferred",
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
(0, node_test_1.default)("inferAccountNameFromCurrentAuth ignores active alias and still infers email-shaped name", async (t) => {
|
|
213
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
214
|
+
const service = new account_service_1.AccountService();
|
|
215
|
+
const activeName = "work";
|
|
216
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
217
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, `${activeName}.json`), buildAuthPayload("admin@recodee.com", {
|
|
218
|
+
accountId: "acct-admin",
|
|
219
|
+
userId: "user-admin",
|
|
220
|
+
}), "utf8");
|
|
221
|
+
await promises_1.default.writeFile(currentPath, `${activeName}\n`, "utf8");
|
|
222
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("admin@recodee.com", {
|
|
223
|
+
accountId: "acct-admin",
|
|
224
|
+
userId: "user-admin",
|
|
225
|
+
}), "utf8");
|
|
226
|
+
const inferred = await service.inferAccountNameFromCurrentAuth();
|
|
227
|
+
strict_1.default.equal(inferred, "admin@recodee.com");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
(0, node_test_1.default)("resolveDefaultAccountNameFromCurrentAuth reuses active snapshot name when identity matches", async (t) => {
|
|
231
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
232
|
+
const service = new account_service_1.AccountService();
|
|
233
|
+
const activeName = "itrexsale";
|
|
234
|
+
const activeSnapshotPath = node_path_1.default.join(accountsDir, `${activeName}.json`);
|
|
235
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
236
|
+
await promises_1.default.writeFile(activeSnapshotPath, buildAuthPayload("codexina@edixai.com", {
|
|
237
|
+
accountId: "acct-a",
|
|
238
|
+
userId: "user-a",
|
|
239
|
+
}), "utf8");
|
|
240
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("codexina@edixai.com", {
|
|
241
|
+
accountId: "acct-a",
|
|
242
|
+
userId: "user-a",
|
|
243
|
+
}), "utf8");
|
|
244
|
+
await promises_1.default.writeFile(currentPath, `${activeName}\n`, "utf8");
|
|
245
|
+
const resolved = await service.resolveDefaultAccountNameFromCurrentAuth();
|
|
246
|
+
strict_1.default.deepEqual(resolved, {
|
|
247
|
+
name: activeName,
|
|
248
|
+
source: "active",
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
(0, node_test_1.default)("resolveDefaultAccountNameFromCurrentAuth falls back to inferred email-shaped name when active snapshot mismatches identity", async (t) => {
|
|
253
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
254
|
+
const service = new account_service_1.AccountService();
|
|
255
|
+
const activeName = "itrexsale";
|
|
256
|
+
const activeSnapshotPath = node_path_1.default.join(accountsDir, `${activeName}.json`);
|
|
257
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
258
|
+
await promises_1.default.writeFile(activeSnapshotPath, buildAuthPayload("other@edixai.com", {
|
|
259
|
+
accountId: "acct-other",
|
|
260
|
+
userId: "user-other",
|
|
261
|
+
}), "utf8");
|
|
262
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("codexina@edixai.com", {
|
|
263
|
+
accountId: "acct-a",
|
|
264
|
+
userId: "user-a",
|
|
265
|
+
}), "utf8");
|
|
266
|
+
await promises_1.default.writeFile(currentPath, `${activeName}\n`, "utf8");
|
|
267
|
+
const resolved = await service.resolveDefaultAccountNameFromCurrentAuth();
|
|
268
|
+
strict_1.default.deepEqual(resolved, {
|
|
269
|
+
name: "codexina@edixai.com",
|
|
270
|
+
source: "inferred",
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
(0, node_test_1.default)("listAccountMappings returns active flag and identity metadata for each snapshot", async (t) => {
|
|
275
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
276
|
+
const service = new account_service_1.AccountService();
|
|
277
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
278
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "itrexsale.json"), buildAuthPayload("itrex@edixai.com", {
|
|
279
|
+
accountId: "acct-itrex",
|
|
280
|
+
userId: "user-itrex",
|
|
281
|
+
}), "utf8");
|
|
282
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, "deadpool.json"), buildAuthPayload("deadpool@edixai.com", {
|
|
283
|
+
accountId: "acct-deadpool",
|
|
284
|
+
userId: "user-deadpool",
|
|
285
|
+
}), "utf8");
|
|
286
|
+
await promises_1.default.writeFile(currentPath, "itrexsale\n", "utf8");
|
|
287
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload("itrex@edixai.com", {
|
|
288
|
+
accountId: "acct-itrex",
|
|
289
|
+
userId: "user-itrex",
|
|
290
|
+
}), "utf8");
|
|
291
|
+
await service.saveAccount("itrexsale");
|
|
292
|
+
const mappings = await service.listAccountMappings();
|
|
293
|
+
strict_1.default.deepEqual(mappings.map((item) => ({
|
|
294
|
+
name: item.name,
|
|
295
|
+
active: item.active,
|
|
296
|
+
email: item.email,
|
|
297
|
+
accountId: item.accountId,
|
|
298
|
+
userId: item.userId,
|
|
299
|
+
})), [
|
|
300
|
+
{
|
|
301
|
+
name: "deadpool",
|
|
302
|
+
active: false,
|
|
303
|
+
email: "deadpool@edixai.com",
|
|
304
|
+
accountId: "acct-deadpool",
|
|
305
|
+
userId: "user-deadpool",
|
|
306
|
+
},
|
|
307
|
+
{
|
|
308
|
+
name: "itrexsale",
|
|
309
|
+
active: true,
|
|
310
|
+
email: "itrex@edixai.com",
|
|
311
|
+
accountId: "acct-itrex",
|
|
312
|
+
userId: "user-itrex",
|
|
313
|
+
},
|
|
314
|
+
]);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
(0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded disables auto-switch and snapshots external codex login into inferred email name", async (t) => {
|
|
318
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
319
|
+
const service = new account_service_1.AccountService();
|
|
320
|
+
const previousName = "odin@recodee.com";
|
|
321
|
+
const incomingEmail = "odin@edixai.com";
|
|
322
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
323
|
+
const registryPath = node_path_1.default.join(accountsDir, "registry.json");
|
|
324
|
+
await promises_1.default.writeFile(node_path_1.default.join(accountsDir, `${previousName}.json`), buildAuthPayload(previousName, {
|
|
325
|
+
accountId: "acct-prev",
|
|
326
|
+
userId: "user-prev",
|
|
327
|
+
}), "utf8");
|
|
328
|
+
await promises_1.default.writeFile(currentPath, `${previousName}\n`, "utf8");
|
|
329
|
+
await promises_1.default.writeFile(registryPath, `${JSON.stringify({
|
|
330
|
+
version: 1,
|
|
331
|
+
autoSwitch: {
|
|
332
|
+
enabled: true,
|
|
333
|
+
threshold5hPercent: 10,
|
|
334
|
+
thresholdWeeklyPercent: 5,
|
|
335
|
+
},
|
|
336
|
+
api: {
|
|
337
|
+
usage: true,
|
|
338
|
+
},
|
|
339
|
+
activeAccountName: previousName,
|
|
340
|
+
accounts: {
|
|
341
|
+
[previousName]: {
|
|
342
|
+
name: previousName,
|
|
343
|
+
createdAt: new Date().toISOString(),
|
|
344
|
+
email: previousName,
|
|
345
|
+
},
|
|
346
|
+
},
|
|
347
|
+
}, null, 2)}\n`, "utf8");
|
|
348
|
+
await promises_1.default.writeFile(authPath, buildAuthPayload(incomingEmail, {
|
|
349
|
+
accountId: "acct-new",
|
|
350
|
+
userId: "user-new",
|
|
351
|
+
}), "utf8");
|
|
352
|
+
const result = await service.syncExternalAuthSnapshotIfNeeded();
|
|
353
|
+
strict_1.default.deepEqual(result, {
|
|
354
|
+
synchronized: true,
|
|
355
|
+
savedName: incomingEmail,
|
|
356
|
+
autoSwitchDisabled: true,
|
|
357
|
+
});
|
|
358
|
+
strict_1.default.equal((await promises_1.default.readFile(currentPath, "utf8")).trim(), incomingEmail);
|
|
359
|
+
const parsed = await (0, auth_parser_1.parseAuthSnapshotFile)(node_path_1.default.join(accountsDir, `${incomingEmail}.json`));
|
|
360
|
+
strict_1.default.equal(parsed.email, incomingEmail);
|
|
361
|
+
const registry = JSON.parse(await promises_1.default.readFile(registryPath, "utf8"));
|
|
362
|
+
strict_1.default.equal(registry.autoSwitch.enabled, false);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
(0, node_test_1.default)("syncExternalAuthSnapshotIfNeeded materializes auth symlink so external codex login can no longer overwrite snapshot files", async (t) => {
|
|
366
|
+
if (process.platform === "win32") {
|
|
367
|
+
t.skip("symlink conversion behavior is Unix-specific in this test");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
await withIsolatedCodexDir(t, async ({ codexDir, accountsDir, authPath }) => {
|
|
371
|
+
const service = new account_service_1.AccountService();
|
|
372
|
+
const activeName = "team-primary";
|
|
373
|
+
const snapshotPath = node_path_1.default.join(accountsDir, `${activeName}.json`);
|
|
374
|
+
const currentPath = node_path_1.default.join(codexDir, "current");
|
|
375
|
+
await promises_1.default.writeFile(snapshotPath, buildAuthPayload("team-primary@edixai.com"), "utf8");
|
|
376
|
+
await promises_1.default.symlink(snapshotPath, authPath);
|
|
377
|
+
await promises_1.default.writeFile(currentPath, `${activeName}\n`, "utf8");
|
|
378
|
+
const before = await promises_1.default.lstat(authPath);
|
|
379
|
+
strict_1.default.equal(before.isSymbolicLink(), true);
|
|
380
|
+
const result = await service.syncExternalAuthSnapshotIfNeeded();
|
|
381
|
+
strict_1.default.deepEqual(result, {
|
|
382
|
+
synchronized: false,
|
|
383
|
+
autoSwitchDisabled: false,
|
|
384
|
+
});
|
|
385
|
+
const after = await promises_1.default.lstat(authPath);
|
|
386
|
+
strict_1.default.equal(after.isSymbolicLink(), false);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
(0, node_test_1.default)("useAccount writes auth.json as a regular file (never symlink)", async (t) => {
|
|
390
|
+
await withIsolatedCodexDir(t, async ({ accountsDir }) => {
|
|
391
|
+
const service = new account_service_1.AccountService();
|
|
392
|
+
const accountName = "regular-file-check";
|
|
393
|
+
const sourcePath = node_path_1.default.join(accountsDir, `${accountName}.json`);
|
|
394
|
+
await promises_1.default.writeFile(sourcePath, buildAuthPayload("regular-file-check@edixai.com"), "utf8");
|
|
395
|
+
await service.useAccount(accountName);
|
|
396
|
+
const authStat = await promises_1.default.lstat(node_path_1.default.join(process.env.CODEX_AUTH_CODEX_DIR, "auth.json"));
|
|
397
|
+
strict_1.default.equal(authStat.isSymbolicLink(), false);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const node_test_1 = __importDefault(require("node:test"));
|
|
7
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
8
|
+
const usage_1 = require("../lib/accounts/usage");
|
|
9
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
10
|
+
(0, node_test_1.default)("usageScore uses min remaining window percent", () => {
|
|
11
|
+
const usage = {
|
|
12
|
+
source: "api",
|
|
13
|
+
fetchedAt: new Date().toISOString(),
|
|
14
|
+
primary: { usedPercent: 30, windowMinutes: 300, resetsAt: nowSeconds + 60 },
|
|
15
|
+
secondary: { usedPercent: 10, windowMinutes: 10080, resetsAt: nowSeconds + 60 },
|
|
16
|
+
};
|
|
17
|
+
strict_1.default.equal((0, usage_1.usageScore)(usage, nowSeconds), 70);
|
|
18
|
+
});
|
|
19
|
+
(0, node_test_1.default)("shouldSwitchCurrent triggers when 5h threshold crossed", () => {
|
|
20
|
+
const usage = {
|
|
21
|
+
source: "api",
|
|
22
|
+
fetchedAt: new Date().toISOString(),
|
|
23
|
+
primary: { usedPercent: 96, windowMinutes: 300, resetsAt: nowSeconds + 60 },
|
|
24
|
+
};
|
|
25
|
+
strict_1.default.equal((0, usage_1.shouldSwitchCurrent)(usage, {
|
|
26
|
+
threshold5hPercent: 10,
|
|
27
|
+
thresholdWeeklyPercent: 5,
|
|
28
|
+
}, nowSeconds), true);
|
|
29
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imdeadpool/codex-account-switcher",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "A command-line tool that lets you manage and switch between multiple Codex accounts instantly, no more constant logins and logouts.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
"types": "dist/index.d.ts",
|
|
11
11
|
"scripts": {
|
|
12
12
|
"build": "tsc -p tsconfig.json",
|
|
13
|
-
"prepublishOnly": "npm run build"
|
|
13
|
+
"prepublishOnly": "npm run build",
|
|
14
|
+
"test": "npm run build && node --test dist/tests/**/*.test.js"
|
|
14
15
|
},
|
|
15
16
|
"engines": {
|
|
16
17
|
"node": ">=18"
|
|
@@ -31,13 +32,13 @@
|
|
|
31
32
|
"preferGlobal": true,
|
|
32
33
|
"repository": {
|
|
33
34
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/
|
|
35
|
+
"url": "git+https://github.com/NagyVikt/codex-account-switcher.git"
|
|
35
36
|
},
|
|
36
37
|
"bugs": {
|
|
37
|
-
"url": "https://github.com/
|
|
38
|
+
"url": "https://github.com/NagyVikt/codex-account-switcher/issues"
|
|
38
39
|
},
|
|
39
|
-
"homepage": "https://github.com/
|
|
40
|
-
"author": "
|
|
40
|
+
"homepage": "https://github.com/NagyVikt/codex-account-switcher#readme",
|
|
41
|
+
"author": "NagyVikt",
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@oclif/core": "^3.0.0",
|
|
43
44
|
"prompts": "^2.4.2",
|