@imdeadpool/codex-account-switcher 0.1.5 → 0.1.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/README.md +79 -5
- 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 +9 -6
- package/scripts/postinstall-login-hook.cjs +90 -0
|
@@ -0,0 +1,83 @@
|
|
|
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
|
+
exports.parseAuthSnapshotData = parseAuthSnapshotData;
|
|
7
|
+
exports.parseAuthSnapshotFile = parseAuthSnapshotFile;
|
|
8
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
9
|
+
function decodeJwtPayload(idToken) {
|
|
10
|
+
const parts = idToken.split(".");
|
|
11
|
+
if (parts.length < 2)
|
|
12
|
+
return null;
|
|
13
|
+
try {
|
|
14
|
+
const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
15
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
16
|
+
const json = Buffer.from(padded, "base64").toString("utf8");
|
|
17
|
+
const payload = JSON.parse(json);
|
|
18
|
+
return payload && typeof payload === "object" ? payload : null;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function asNonEmptyString(value) {
|
|
25
|
+
return typeof value === "string" && value.trim().length > 0 ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
function asAuthObject(payload) {
|
|
28
|
+
if (!payload)
|
|
29
|
+
return null;
|
|
30
|
+
const authClaim = payload["https://api.openai.com/auth"];
|
|
31
|
+
if (!authClaim || typeof authClaim !== "object")
|
|
32
|
+
return null;
|
|
33
|
+
return authClaim;
|
|
34
|
+
}
|
|
35
|
+
function firstNonEmptyString(...values) {
|
|
36
|
+
for (const value of values) {
|
|
37
|
+
const normalized = asNonEmptyString(value);
|
|
38
|
+
if (normalized)
|
|
39
|
+
return normalized;
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function parseAuthSnapshotData(data) {
|
|
44
|
+
var _a;
|
|
45
|
+
if (!data || typeof data !== "object") {
|
|
46
|
+
return { authMode: "unknown" };
|
|
47
|
+
}
|
|
48
|
+
const root = data;
|
|
49
|
+
const apiKey = asNonEmptyString(root.OPENAI_API_KEY);
|
|
50
|
+
if (apiKey) {
|
|
51
|
+
return { authMode: "apikey" };
|
|
52
|
+
}
|
|
53
|
+
const tokens = root.tokens;
|
|
54
|
+
if (!tokens || typeof tokens !== "object") {
|
|
55
|
+
return { authMode: "unknown" };
|
|
56
|
+
}
|
|
57
|
+
const tokenRecord = tokens;
|
|
58
|
+
const idToken = asNonEmptyString(tokenRecord.id_token);
|
|
59
|
+
const payload = idToken ? decodeJwtPayload(idToken) : null;
|
|
60
|
+
const authObject = asAuthObject(payload);
|
|
61
|
+
const email = (_a = firstNonEmptyString(payload === null || payload === void 0 ? void 0 : payload.email, root.email)) === null || _a === void 0 ? void 0 : _a.toLowerCase();
|
|
62
|
+
const accountId = firstNonEmptyString(tokenRecord.account_id, tokenRecord.chatgpt_account_id, tokenRecord.default_account_id, authObject === null || authObject === void 0 ? void 0 : authObject.chatgpt_account_id, authObject === null || authObject === void 0 ? void 0 : authObject.account_id, authObject === null || authObject === void 0 ? void 0 : authObject.default_account_id, root.account_id, root.chatgpt_account_id);
|
|
63
|
+
const userId = firstNonEmptyString(authObject === null || authObject === void 0 ? void 0 : authObject.chatgpt_user_id, authObject === null || authObject === void 0 ? void 0 : authObject.user_id, payload === null || payload === void 0 ? void 0 : payload.sub, payload === null || payload === void 0 ? void 0 : payload.user_id, root.user_id, root.chatgpt_user_id);
|
|
64
|
+
const planType = firstNonEmptyString(authObject === null || authObject === void 0 ? void 0 : authObject.chatgpt_plan_type, payload === null || payload === void 0 ? void 0 : payload.chatgpt_plan_type, root.chatgpt_plan_type);
|
|
65
|
+
return {
|
|
66
|
+
authMode: "chatgpt",
|
|
67
|
+
email,
|
|
68
|
+
accessToken: asNonEmptyString(tokenRecord.access_token),
|
|
69
|
+
accountId,
|
|
70
|
+
userId,
|
|
71
|
+
planType,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
async function parseAuthSnapshotFile(snapshotPath) {
|
|
75
|
+
try {
|
|
76
|
+
const raw = await promises_1.default.readFile(snapshotPath, "utf8");
|
|
77
|
+
const data = JSON.parse(raw);
|
|
78
|
+
return parseAuthSnapshotData(data);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return { authMode: "unknown" };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -13,6 +13,21 @@ export declare class NoAccountsSavedError extends CodexAuthError {
|
|
|
13
13
|
export declare class InvalidAccountNameError extends CodexAuthError {
|
|
14
14
|
constructor();
|
|
15
15
|
}
|
|
16
|
+
export declare class AccountNameInferenceError extends CodexAuthError {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
export declare class SnapshotEmailMismatchError extends CodexAuthError {
|
|
20
|
+
constructor(accountName: string, existingEmail: string, incomingEmail: string);
|
|
21
|
+
}
|
|
16
22
|
export declare class PromptCancelledError extends CodexAuthError {
|
|
17
23
|
constructor();
|
|
18
24
|
}
|
|
25
|
+
export declare class InvalidRemoveSelectionError extends CodexAuthError {
|
|
26
|
+
constructor();
|
|
27
|
+
}
|
|
28
|
+
export declare class AmbiguousAccountQueryError extends CodexAuthError {
|
|
29
|
+
constructor(query: string);
|
|
30
|
+
}
|
|
31
|
+
export declare class AutoSwitchConfigError extends CodexAuthError {
|
|
32
|
+
constructor(message: string);
|
|
33
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PromptCancelledError = exports.InvalidAccountNameError = exports.NoAccountsSavedError = exports.AccountNotFoundError = exports.AuthFileMissingError = exports.CodexAuthError = void 0;
|
|
3
|
+
exports.AutoSwitchConfigError = exports.AmbiguousAccountQueryError = exports.InvalidRemoveSelectionError = exports.PromptCancelledError = exports.SnapshotEmailMismatchError = exports.AccountNameInferenceError = exports.InvalidAccountNameError = exports.NoAccountsSavedError = exports.AccountNotFoundError = exports.AuthFileMissingError = exports.CodexAuthError = void 0;
|
|
4
4
|
class CodexAuthError extends Error {
|
|
5
5
|
constructor(message) {
|
|
6
6
|
super(message);
|
|
@@ -30,13 +30,45 @@ exports.NoAccountsSavedError = NoAccountsSavedError;
|
|
|
30
30
|
class InvalidAccountNameError extends CodexAuthError {
|
|
31
31
|
constructor() {
|
|
32
32
|
super("Account names must include at least one non-space character and " +
|
|
33
|
-
"may contain letters, numbers, dashes, underscores, and
|
|
33
|
+
"may contain letters, numbers, dashes, underscores, dots, and @.");
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
exports.InvalidAccountNameError = InvalidAccountNameError;
|
|
37
|
+
class AccountNameInferenceError extends CodexAuthError {
|
|
38
|
+
constructor() {
|
|
39
|
+
super("Could not infer account name from auth email. Pass one explicitly: codex-auth save <name>.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.AccountNameInferenceError = AccountNameInferenceError;
|
|
43
|
+
class SnapshotEmailMismatchError extends CodexAuthError {
|
|
44
|
+
constructor(accountName, existingEmail, incomingEmail) {
|
|
45
|
+
super(`Refusing to overwrite snapshot "${accountName}" because it belongs to ` +
|
|
46
|
+
`${existingEmail}, but current auth is ${incomingEmail}. ` +
|
|
47
|
+
`Use a different name, run "codex-auth remove ${accountName}" first, or re-run with --force.`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.SnapshotEmailMismatchError = SnapshotEmailMismatchError;
|
|
37
51
|
class PromptCancelledError extends CodexAuthError {
|
|
38
52
|
constructor() {
|
|
39
53
|
super("No account selected. The operation was cancelled.");
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
56
|
exports.PromptCancelledError = PromptCancelledError;
|
|
57
|
+
class InvalidRemoveSelectionError extends CodexAuthError {
|
|
58
|
+
constructor() {
|
|
59
|
+
super("No accounts were selected for removal.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.InvalidRemoveSelectionError = InvalidRemoveSelectionError;
|
|
63
|
+
class AmbiguousAccountQueryError extends CodexAuthError {
|
|
64
|
+
constructor(query) {
|
|
65
|
+
super(`Query "${query}" matched multiple accounts. Refine the query or use interactive mode.`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.AmbiguousAccountQueryError = AmbiguousAccountQueryError;
|
|
69
|
+
class AutoSwitchConfigError extends CodexAuthError {
|
|
70
|
+
constructor(message) {
|
|
71
|
+
super(message);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
exports.AutoSwitchConfigError = AutoSwitchConfigError;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { AccountService } from "./account-service";
|
|
2
2
|
export { AccountService } from "./account-service";
|
|
3
|
-
export {
|
|
3
|
+
export type { AccountChoice, RemoveResult } from "./account-service";
|
|
4
|
+
export { AccountNotFoundError, AmbiguousAccountQueryError, AuthFileMissingError, AutoSwitchConfigError, CodexAuthError, InvalidAccountNameError, InvalidRemoveSelectionError, NoAccountsSavedError, PromptCancelledError, SnapshotEmailMismatchError, } from "./errors";
|
|
5
|
+
export type { AutoSwitchRunResult, StatusReport } from "./types";
|
|
4
6
|
export declare const accountService: AccountService;
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.accountService = exports.PromptCancelledError = exports.NoAccountsSavedError = exports.InvalidAccountNameError = exports.CodexAuthError = exports.AuthFileMissingError = exports.AccountNotFoundError = exports.AccountService = void 0;
|
|
3
|
+
exports.accountService = exports.SnapshotEmailMismatchError = exports.PromptCancelledError = exports.NoAccountsSavedError = exports.InvalidRemoveSelectionError = exports.InvalidAccountNameError = exports.CodexAuthError = exports.AutoSwitchConfigError = exports.AuthFileMissingError = exports.AmbiguousAccountQueryError = exports.AccountNotFoundError = exports.AccountService = void 0;
|
|
4
4
|
const account_service_1 = require("./account-service");
|
|
5
5
|
var account_service_2 = require("./account-service");
|
|
6
6
|
Object.defineProperty(exports, "AccountService", { enumerable: true, get: function () { return account_service_2.AccountService; } });
|
|
7
7
|
var errors_1 = require("./errors");
|
|
8
8
|
Object.defineProperty(exports, "AccountNotFoundError", { enumerable: true, get: function () { return errors_1.AccountNotFoundError; } });
|
|
9
|
+
Object.defineProperty(exports, "AmbiguousAccountQueryError", { enumerable: true, get: function () { return errors_1.AmbiguousAccountQueryError; } });
|
|
9
10
|
Object.defineProperty(exports, "AuthFileMissingError", { enumerable: true, get: function () { return errors_1.AuthFileMissingError; } });
|
|
11
|
+
Object.defineProperty(exports, "AutoSwitchConfigError", { enumerable: true, get: function () { return errors_1.AutoSwitchConfigError; } });
|
|
10
12
|
Object.defineProperty(exports, "CodexAuthError", { enumerable: true, get: function () { return errors_1.CodexAuthError; } });
|
|
11
13
|
Object.defineProperty(exports, "InvalidAccountNameError", { enumerable: true, get: function () { return errors_1.InvalidAccountNameError; } });
|
|
14
|
+
Object.defineProperty(exports, "InvalidRemoveSelectionError", { enumerable: true, get: function () { return errors_1.InvalidRemoveSelectionError; } });
|
|
12
15
|
Object.defineProperty(exports, "NoAccountsSavedError", { enumerable: true, get: function () { return errors_1.NoAccountsSavedError; } });
|
|
13
16
|
Object.defineProperty(exports, "PromptCancelledError", { enumerable: true, get: function () { return errors_1.PromptCancelledError; } });
|
|
17
|
+
Object.defineProperty(exports, "SnapshotEmailMismatchError", { enumerable: true, get: function () { return errors_1.SnapshotEmailMismatchError; } });
|
|
14
18
|
exports.accountService = new account_service_1.AccountService();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { RegistryData } from "./types";
|
|
2
|
+
export declare function createDefaultRegistry(): RegistryData;
|
|
3
|
+
export declare function sanitizeRegistry(input: unknown): RegistryData;
|
|
4
|
+
export declare function loadRegistry(): Promise<RegistryData>;
|
|
5
|
+
export declare function saveRegistry(registry: RegistryData): Promise<void>;
|
|
6
|
+
export declare function reconcileRegistryWithAccounts(registry: RegistryData, accountNames: string[]): RegistryData;
|
|
@@ -0,0 +1,166 @@
|
|
|
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
|
+
exports.createDefaultRegistry = createDefaultRegistry;
|
|
7
|
+
exports.sanitizeRegistry = sanitizeRegistry;
|
|
8
|
+
exports.loadRegistry = loadRegistry;
|
|
9
|
+
exports.saveRegistry = saveRegistry;
|
|
10
|
+
exports.reconcileRegistryWithAccounts = reconcileRegistryWithAccounts;
|
|
11
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const paths_1 = require("../config/paths");
|
|
14
|
+
const types_1 = require("./types");
|
|
15
|
+
function nowIso() {
|
|
16
|
+
return new Date().toISOString();
|
|
17
|
+
}
|
|
18
|
+
function clampPercent(value, fallback) {
|
|
19
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
20
|
+
return fallback;
|
|
21
|
+
const rounded = Math.round(value);
|
|
22
|
+
if (rounded < 1 || rounded > 100)
|
|
23
|
+
return fallback;
|
|
24
|
+
return rounded;
|
|
25
|
+
}
|
|
26
|
+
function sanitizeUsageSnapshot(input) {
|
|
27
|
+
if (!input || typeof input !== "object")
|
|
28
|
+
return undefined;
|
|
29
|
+
const sourceRaw = input.source;
|
|
30
|
+
const source = sourceRaw === "api" || sourceRaw === "local" || sourceRaw === "cached" ? sourceRaw : "cached";
|
|
31
|
+
const normalizeWindow = (raw) => {
|
|
32
|
+
if (!raw || typeof raw !== "object")
|
|
33
|
+
return undefined;
|
|
34
|
+
const windowRaw = raw;
|
|
35
|
+
const used = windowRaw.usedPercent;
|
|
36
|
+
if (typeof used !== "number" || !Number.isFinite(used))
|
|
37
|
+
return undefined;
|
|
38
|
+
const windowMinutes = typeof windowRaw.windowMinutes === "number" && Number.isFinite(windowRaw.windowMinutes)
|
|
39
|
+
? Math.round(windowRaw.windowMinutes)
|
|
40
|
+
: undefined;
|
|
41
|
+
const resetsAt = typeof windowRaw.resetsAt === "number" && Number.isFinite(windowRaw.resetsAt)
|
|
42
|
+
? Math.round(windowRaw.resetsAt)
|
|
43
|
+
: undefined;
|
|
44
|
+
return {
|
|
45
|
+
usedPercent: Math.max(0, Math.min(100, used)),
|
|
46
|
+
windowMinutes,
|
|
47
|
+
resetsAt,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const primary = normalizeWindow(input.primary);
|
|
51
|
+
const secondary = normalizeWindow(input.secondary);
|
|
52
|
+
if (!primary && !secondary)
|
|
53
|
+
return undefined;
|
|
54
|
+
return {
|
|
55
|
+
primary,
|
|
56
|
+
secondary,
|
|
57
|
+
fetchedAt: typeof input.fetchedAt === "string"
|
|
58
|
+
? input.fetchedAt
|
|
59
|
+
: nowIso(),
|
|
60
|
+
planType: typeof input.planType === "string"
|
|
61
|
+
? input.planType
|
|
62
|
+
: undefined,
|
|
63
|
+
source,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function sanitizeEntry(name, entry) {
|
|
67
|
+
const raw = entry && typeof entry === "object" ? entry : {};
|
|
68
|
+
const sanitizedUsage = sanitizeUsageSnapshot(raw.lastUsage);
|
|
69
|
+
return {
|
|
70
|
+
name,
|
|
71
|
+
createdAt: typeof raw.createdAt === "string" ? raw.createdAt : nowIso(),
|
|
72
|
+
email: typeof raw.email === "string" ? raw.email : undefined,
|
|
73
|
+
accountId: typeof raw.accountId === "string" ? raw.accountId : undefined,
|
|
74
|
+
userId: typeof raw.userId === "string" ? raw.userId : undefined,
|
|
75
|
+
planType: typeof raw.planType === "string" ? raw.planType : undefined,
|
|
76
|
+
lastUsageAt: typeof raw.lastUsageAt === "string" ? raw.lastUsageAt : undefined,
|
|
77
|
+
lastUsage: sanitizedUsage,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function createDefaultRegistry() {
|
|
81
|
+
return {
|
|
82
|
+
version: 1,
|
|
83
|
+
autoSwitch: {
|
|
84
|
+
enabled: false,
|
|
85
|
+
threshold5hPercent: types_1.DEFAULT_THRESHOLD_5H_PERCENT,
|
|
86
|
+
thresholdWeeklyPercent: types_1.DEFAULT_THRESHOLD_WEEKLY_PERCENT,
|
|
87
|
+
},
|
|
88
|
+
api: {
|
|
89
|
+
usage: true,
|
|
90
|
+
},
|
|
91
|
+
accounts: {},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function sanitizeRegistry(input) {
|
|
95
|
+
const defaults = createDefaultRegistry();
|
|
96
|
+
if (!input || typeof input !== "object")
|
|
97
|
+
return defaults;
|
|
98
|
+
const root = input;
|
|
99
|
+
const autoSwitch = root.autoSwitch && typeof root.autoSwitch === "object"
|
|
100
|
+
? root.autoSwitch
|
|
101
|
+
: {};
|
|
102
|
+
const api = root.api && typeof root.api === "object" ? root.api : {};
|
|
103
|
+
const accountsRaw = root.accounts && typeof root.accounts === "object"
|
|
104
|
+
? root.accounts
|
|
105
|
+
: {};
|
|
106
|
+
const accounts = {};
|
|
107
|
+
for (const [name, value] of Object.entries(accountsRaw)) {
|
|
108
|
+
accounts[name] = sanitizeEntry(name, value);
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
version: 1,
|
|
112
|
+
autoSwitch: {
|
|
113
|
+
enabled: typeof autoSwitch.enabled === "boolean" ? autoSwitch.enabled : defaults.autoSwitch.enabled,
|
|
114
|
+
threshold5hPercent: clampPercent(autoSwitch.threshold5hPercent, defaults.autoSwitch.threshold5hPercent),
|
|
115
|
+
thresholdWeeklyPercent: clampPercent(autoSwitch.thresholdWeeklyPercent, defaults.autoSwitch.thresholdWeeklyPercent),
|
|
116
|
+
},
|
|
117
|
+
api: {
|
|
118
|
+
usage: typeof api.usage === "boolean" ? api.usage : defaults.api.usage,
|
|
119
|
+
},
|
|
120
|
+
activeAccountName: typeof root.activeAccountName === "string" && root.activeAccountName.length > 0
|
|
121
|
+
? root.activeAccountName
|
|
122
|
+
: undefined,
|
|
123
|
+
accounts,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
async function loadRegistry() {
|
|
127
|
+
const registryPath = (0, paths_1.resolveRegistryPath)();
|
|
128
|
+
try {
|
|
129
|
+
const raw = await promises_1.default.readFile(registryPath, "utf8");
|
|
130
|
+
const parsed = JSON.parse(raw);
|
|
131
|
+
return sanitizeRegistry(parsed);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return createDefaultRegistry();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function saveRegistry(registry) {
|
|
138
|
+
const registryPath = (0, paths_1.resolveRegistryPath)();
|
|
139
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(registryPath), { recursive: true });
|
|
140
|
+
await promises_1.default.writeFile(registryPath, `${JSON.stringify(registry, null, 2)}\n`, "utf8");
|
|
141
|
+
}
|
|
142
|
+
function reconcileRegistryWithAccounts(registry, accountNames) {
|
|
143
|
+
const next = sanitizeRegistry(registry);
|
|
144
|
+
const accountSet = new Set(accountNames);
|
|
145
|
+
const now = nowIso();
|
|
146
|
+
for (const name of accountNames) {
|
|
147
|
+
if (!next.accounts[name]) {
|
|
148
|
+
next.accounts[name] = {
|
|
149
|
+
name,
|
|
150
|
+
createdAt: now,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
next.accounts[name].name = name;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const name of Object.keys(next.accounts)) {
|
|
158
|
+
if (!accountSet.has(name)) {
|
|
159
|
+
delete next.accounts[name];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (next.activeAccountName && !accountSet.has(next.activeAccountName)) {
|
|
163
|
+
delete next.activeAccountName;
|
|
164
|
+
}
|
|
165
|
+
return next;
|
|
166
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
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
|
+
exports.enableManagedService = enableManagedService;
|
|
7
|
+
exports.disableManagedService = disableManagedService;
|
|
8
|
+
exports.getManagedServiceState = getManagedServiceState;
|
|
9
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
10
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const node_child_process_1 = require("node:child_process");
|
|
13
|
+
const LINUX_SERVICE_NAME = "codex-auth-autoswitch.service";
|
|
14
|
+
const MAC_LABEL = "com.codex.auth.autoswitch";
|
|
15
|
+
const WINDOWS_TASK_NAME = "codex-auth-autoswitch";
|
|
16
|
+
function runCommand(command, args) {
|
|
17
|
+
var _a;
|
|
18
|
+
const result = (0, node_child_process_1.spawnSync)(command, args, {
|
|
19
|
+
encoding: "utf8",
|
|
20
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
21
|
+
});
|
|
22
|
+
return {
|
|
23
|
+
code: result.status,
|
|
24
|
+
stdout: ((_a = result.stdout) !== null && _a !== void 0 ? _a : "").toString(),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function linuxUnitPath() {
|
|
28
|
+
return node_path_1.default.join(node_os_1.default.homedir(), ".config", "systemd", "user", LINUX_SERVICE_NAME);
|
|
29
|
+
}
|
|
30
|
+
function linuxUnitContents() {
|
|
31
|
+
return [
|
|
32
|
+
"[Unit]",
|
|
33
|
+
"Description=codex-auth auto-switch watcher",
|
|
34
|
+
"",
|
|
35
|
+
"[Service]",
|
|
36
|
+
"Type=simple",
|
|
37
|
+
"Restart=always",
|
|
38
|
+
"RestartSec=1",
|
|
39
|
+
"ExecStart=codex-auth daemon --watch",
|
|
40
|
+
"",
|
|
41
|
+
"[Install]",
|
|
42
|
+
"WantedBy=default.target",
|
|
43
|
+
"",
|
|
44
|
+
].join("\n");
|
|
45
|
+
}
|
|
46
|
+
async function enableLinuxService() {
|
|
47
|
+
const unitPath = linuxUnitPath();
|
|
48
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(unitPath), { recursive: true });
|
|
49
|
+
await promises_1.default.writeFile(unitPath, linuxUnitContents(), "utf8");
|
|
50
|
+
const daemonReload = runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
51
|
+
if (daemonReload.code !== 0) {
|
|
52
|
+
throw new Error("systemctl --user daemon-reload failed");
|
|
53
|
+
}
|
|
54
|
+
const enable = runCommand("systemctl", ["--user", "enable", LINUX_SERVICE_NAME]);
|
|
55
|
+
if (enable.code !== 0) {
|
|
56
|
+
throw new Error("systemctl --user enable failed");
|
|
57
|
+
}
|
|
58
|
+
const start = runCommand("systemctl", ["--user", "start", LINUX_SERVICE_NAME]);
|
|
59
|
+
if (start.code !== 0) {
|
|
60
|
+
throw new Error("systemctl --user start failed");
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
async function disableLinuxService() {
|
|
64
|
+
runCommand("systemctl", ["--user", "stop", LINUX_SERVICE_NAME]);
|
|
65
|
+
runCommand("systemctl", ["--user", "disable", LINUX_SERVICE_NAME]);
|
|
66
|
+
try {
|
|
67
|
+
await promises_1.default.rm(linuxUnitPath(), { force: true });
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// ignore
|
|
71
|
+
}
|
|
72
|
+
runCommand("systemctl", ["--user", "daemon-reload"]);
|
|
73
|
+
}
|
|
74
|
+
function linuxServiceState() {
|
|
75
|
+
const result = runCommand("systemctl", ["--user", "is-active", LINUX_SERVICE_NAME]);
|
|
76
|
+
if (result.code === null)
|
|
77
|
+
return "unknown";
|
|
78
|
+
if (result.code === 0 && result.stdout.trim().startsWith("active"))
|
|
79
|
+
return "active";
|
|
80
|
+
return "inactive";
|
|
81
|
+
}
|
|
82
|
+
function macPlistPath() {
|
|
83
|
+
return node_path_1.default.join(node_os_1.default.homedir(), "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
|
|
84
|
+
}
|
|
85
|
+
function macPlistContents() {
|
|
86
|
+
return [
|
|
87
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
88
|
+
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
89
|
+
'<plist version="1.0">',
|
|
90
|
+
"<dict>",
|
|
91
|
+
" <key>Label</key>",
|
|
92
|
+
` <string>${MAC_LABEL}</string>`,
|
|
93
|
+
" <key>ProgramArguments</key>",
|
|
94
|
+
" <array>",
|
|
95
|
+
" <string>codex-auth</string>",
|
|
96
|
+
" <string>daemon</string>",
|
|
97
|
+
" <string>--watch</string>",
|
|
98
|
+
" </array>",
|
|
99
|
+
" <key>RunAtLoad</key>",
|
|
100
|
+
" <true/>",
|
|
101
|
+
" <key>KeepAlive</key>",
|
|
102
|
+
" <true/>",
|
|
103
|
+
"</dict>",
|
|
104
|
+
"</plist>",
|
|
105
|
+
"",
|
|
106
|
+
].join("\n");
|
|
107
|
+
}
|
|
108
|
+
async function enableMacService() {
|
|
109
|
+
const plistPath = macPlistPath();
|
|
110
|
+
await promises_1.default.mkdir(node_path_1.default.dirname(plistPath), { recursive: true });
|
|
111
|
+
await promises_1.default.writeFile(plistPath, macPlistContents(), "utf8");
|
|
112
|
+
runCommand("launchctl", ["unload", plistPath]);
|
|
113
|
+
const load = runCommand("launchctl", ["load", plistPath]);
|
|
114
|
+
if (load.code !== 0) {
|
|
115
|
+
throw new Error("launchctl load failed");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
async function disableMacService() {
|
|
119
|
+
const plistPath = macPlistPath();
|
|
120
|
+
runCommand("launchctl", ["unload", plistPath]);
|
|
121
|
+
try {
|
|
122
|
+
await promises_1.default.rm(plistPath, { force: true });
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// ignore
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function macServiceState() {
|
|
129
|
+
const result = runCommand("launchctl", ["list", MAC_LABEL]);
|
|
130
|
+
if (result.code === null)
|
|
131
|
+
return "unknown";
|
|
132
|
+
return result.code === 0 ? "active" : "inactive";
|
|
133
|
+
}
|
|
134
|
+
async function enableWindowsService() {
|
|
135
|
+
const create = runCommand("schtasks", [
|
|
136
|
+
"/Create",
|
|
137
|
+
"/TN",
|
|
138
|
+
WINDOWS_TASK_NAME,
|
|
139
|
+
"/SC",
|
|
140
|
+
"ONLOGON",
|
|
141
|
+
"/TR",
|
|
142
|
+
"cmd /c codex-auth daemon --watch",
|
|
143
|
+
"/F",
|
|
144
|
+
]);
|
|
145
|
+
if (create.code !== 0) {
|
|
146
|
+
throw new Error("schtasks /Create failed");
|
|
147
|
+
}
|
|
148
|
+
runCommand("schtasks", ["/Run", "/TN", WINDOWS_TASK_NAME]);
|
|
149
|
+
}
|
|
150
|
+
async function disableWindowsService() {
|
|
151
|
+
runCommand("schtasks", ["/Delete", "/TN", WINDOWS_TASK_NAME, "/F"]);
|
|
152
|
+
}
|
|
153
|
+
function windowsServiceState() {
|
|
154
|
+
const query = runCommand("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME]);
|
|
155
|
+
if (query.code === null)
|
|
156
|
+
return "unknown";
|
|
157
|
+
if (query.code !== 0)
|
|
158
|
+
return "inactive";
|
|
159
|
+
const verbose = runCommand("schtasks", ["/Query", "/TN", WINDOWS_TASK_NAME, "/V", "/FO", "LIST"]);
|
|
160
|
+
if (verbose.code !== 0)
|
|
161
|
+
return "inactive";
|
|
162
|
+
const text = verbose.stdout.toLowerCase();
|
|
163
|
+
if (text.includes("running"))
|
|
164
|
+
return "active";
|
|
165
|
+
return "inactive";
|
|
166
|
+
}
|
|
167
|
+
async function enableManagedService() {
|
|
168
|
+
if (process.platform === "linux") {
|
|
169
|
+
await enableLinuxService();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (process.platform === "darwin") {
|
|
173
|
+
await enableMacService();
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (process.platform === "win32") {
|
|
177
|
+
await enableWindowsService();
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
throw new Error(`Managed auto-switch is not supported on platform ${process.platform}.`);
|
|
181
|
+
}
|
|
182
|
+
async function disableManagedService() {
|
|
183
|
+
if (process.platform === "linux") {
|
|
184
|
+
await disableLinuxService();
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (process.platform === "darwin") {
|
|
188
|
+
await disableMacService();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
if (process.platform === "win32") {
|
|
192
|
+
await disableWindowsService();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function getManagedServiceState() {
|
|
197
|
+
if (process.platform === "linux")
|
|
198
|
+
return linuxServiceState();
|
|
199
|
+
if (process.platform === "darwin")
|
|
200
|
+
return macServiceState();
|
|
201
|
+
if (process.platform === "win32")
|
|
202
|
+
return windowsServiceState();
|
|
203
|
+
return "unknown";
|
|
204
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export declare const DEFAULT_THRESHOLD_5H_PERCENT = 10;
|
|
2
|
+
export declare const DEFAULT_THRESHOLD_WEEKLY_PERCENT = 5;
|
|
3
|
+
export type UsageSource = "api" | "local" | "cached";
|
|
4
|
+
export interface RateLimitWindow {
|
|
5
|
+
usedPercent: number;
|
|
6
|
+
windowMinutes?: number;
|
|
7
|
+
resetsAt?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface UsageSnapshot {
|
|
10
|
+
primary?: RateLimitWindow;
|
|
11
|
+
secondary?: RateLimitWindow;
|
|
12
|
+
planType?: string;
|
|
13
|
+
fetchedAt: string;
|
|
14
|
+
source: UsageSource;
|
|
15
|
+
}
|
|
16
|
+
export interface AccountRegistryEntry {
|
|
17
|
+
name: string;
|
|
18
|
+
email?: string;
|
|
19
|
+
accountId?: string;
|
|
20
|
+
userId?: string;
|
|
21
|
+
planType?: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
lastUsageAt?: string;
|
|
24
|
+
lastUsage?: UsageSnapshot;
|
|
25
|
+
}
|
|
26
|
+
export interface AutoSwitchConfig {
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
threshold5hPercent: number;
|
|
29
|
+
thresholdWeeklyPercent: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ApiConfig {
|
|
32
|
+
usage: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface RegistryData {
|
|
35
|
+
version: 1;
|
|
36
|
+
autoSwitch: AutoSwitchConfig;
|
|
37
|
+
api: ApiConfig;
|
|
38
|
+
activeAccountName?: string;
|
|
39
|
+
accounts: Record<string, AccountRegistryEntry>;
|
|
40
|
+
}
|
|
41
|
+
export interface ParsedAuthSnapshot {
|
|
42
|
+
authMode: "chatgpt" | "apikey" | "unknown";
|
|
43
|
+
email?: string;
|
|
44
|
+
accountId?: string;
|
|
45
|
+
userId?: string;
|
|
46
|
+
planType?: string;
|
|
47
|
+
accessToken?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface StatusReport {
|
|
50
|
+
autoSwitchEnabled: boolean;
|
|
51
|
+
serviceState: "active" | "inactive" | "unknown";
|
|
52
|
+
threshold5hPercent: number;
|
|
53
|
+
thresholdWeeklyPercent: number;
|
|
54
|
+
usageMode: "api" | "local";
|
|
55
|
+
}
|
|
56
|
+
export interface AutoSwitchRunResult {
|
|
57
|
+
switched: boolean;
|
|
58
|
+
fromAccount?: string;
|
|
59
|
+
toAccount?: string;
|
|
60
|
+
reason: string;
|
|
61
|
+
}
|
|
62
|
+
export interface AccountMapping {
|
|
63
|
+
name: string;
|
|
64
|
+
active: boolean;
|
|
65
|
+
email?: string;
|
|
66
|
+
accountId?: string;
|
|
67
|
+
userId?: string;
|
|
68
|
+
planType?: string;
|
|
69
|
+
lastUsageAt?: string;
|
|
70
|
+
usageSource?: UsageSource;
|
|
71
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ParsedAuthSnapshot, RateLimitWindow, UsageSnapshot } from "./types";
|
|
2
|
+
export declare function resolveRateWindow(snapshot: UsageSnapshot | undefined, minutes: number, fallbackPrimary: boolean): RateLimitWindow | undefined;
|
|
3
|
+
export declare function remainingPercent(window: RateLimitWindow | undefined, nowSeconds: number): number | undefined;
|
|
4
|
+
export declare function usageScore(snapshot: UsageSnapshot | undefined, nowSeconds: number): number | undefined;
|
|
5
|
+
export declare function shouldSwitchCurrent(snapshot: UsageSnapshot | undefined, thresholds: {
|
|
6
|
+
threshold5hPercent: number;
|
|
7
|
+
thresholdWeeklyPercent: number;
|
|
8
|
+
}, nowSeconds: number): boolean;
|
|
9
|
+
export declare function fetchUsageFromApi(snapshotInfo: ParsedAuthSnapshot): Promise<UsageSnapshot | null>;
|
|
10
|
+
export declare function fetchUsageFromLocal(codexDir: string): Promise<UsageSnapshot | null>;
|