opencode-copilot-account-switcher 0.13.2 → 0.13.3
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/codex-invalid-account.d.ts +32 -0
- package/dist/codex-invalid-account.js +106 -0
- package/dist/codex-oauth.d.ts +26 -1
- package/dist/codex-oauth.js +28 -0
- package/dist/codex-status-command.js +75 -22
- package/dist/codex-status-fetcher.d.ts +4 -0
- package/dist/codex-status-fetcher.js +47 -4
- package/dist/codex-store.d.ts +1 -0
- package/dist/codex-store.js +2 -0
- package/dist/providers/codex-menu-adapter.js +55 -1
- package/package.json +1 -1
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CodexAccountEntry, CodexStoreFile } from "./codex-store.js";
|
|
2
|
+
export type CodexRecoveryCandidate = {
|
|
3
|
+
name: string;
|
|
4
|
+
entry: CodexAccountEntry;
|
|
5
|
+
};
|
|
6
|
+
export type CodexSetAuthInput = {
|
|
7
|
+
path: {
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
body: {
|
|
11
|
+
type: "oauth";
|
|
12
|
+
refresh?: string;
|
|
13
|
+
access?: string;
|
|
14
|
+
expires?: number;
|
|
15
|
+
accountId?: string;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type RecoverInvalidCodexAccountResult = {
|
|
19
|
+
removed: string;
|
|
20
|
+
replacement?: string;
|
|
21
|
+
switched: boolean;
|
|
22
|
+
weekRecoveryOnly: boolean;
|
|
23
|
+
noCandidates: boolean;
|
|
24
|
+
store: CodexStoreFile;
|
|
25
|
+
};
|
|
26
|
+
export declare function getCodexDisplayName(entry: CodexAccountEntry | undefined, fallbackName: string): string;
|
|
27
|
+
export declare function sortCodexRecoveryCandidates(candidates: CodexRecoveryCandidate[]): CodexRecoveryCandidate[];
|
|
28
|
+
export declare function recoverInvalidCodexAccount(input: {
|
|
29
|
+
store: CodexStoreFile;
|
|
30
|
+
invalidAccountName: string;
|
|
31
|
+
setAuth?: (next: CodexSetAuthInput) => Promise<unknown>;
|
|
32
|
+
}): Promise<RecoverInvalidCodexAccountResult>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
function pickPositiveNumber(value) {
|
|
2
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
3
|
+
return 0;
|
|
4
|
+
return value > 0 ? value : 0;
|
|
5
|
+
}
|
|
6
|
+
function pickFiniteNumber(value) {
|
|
7
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
8
|
+
return undefined;
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
function getWeekRemaining(entry) {
|
|
12
|
+
return pickPositiveNumber(entry.snapshot?.usageWeek?.remaining);
|
|
13
|
+
}
|
|
14
|
+
function get5hRemaining(entry) {
|
|
15
|
+
return pickPositiveNumber(entry.snapshot?.usage5h?.remaining);
|
|
16
|
+
}
|
|
17
|
+
function compareResetAt(a, b) {
|
|
18
|
+
const aMissing = a === undefined;
|
|
19
|
+
const bMissing = b === undefined;
|
|
20
|
+
if (aMissing && bMissing)
|
|
21
|
+
return 0;
|
|
22
|
+
if (aMissing)
|
|
23
|
+
return 1;
|
|
24
|
+
if (bMissing)
|
|
25
|
+
return -1;
|
|
26
|
+
if (a === b)
|
|
27
|
+
return 0;
|
|
28
|
+
return a < b ? -1 : 1;
|
|
29
|
+
}
|
|
30
|
+
export function getCodexDisplayName(entry, fallbackName) {
|
|
31
|
+
return entry?.workspaceName
|
|
32
|
+
?? entry?.name
|
|
33
|
+
?? entry?.email
|
|
34
|
+
?? entry?.accountId
|
|
35
|
+
?? fallbackName;
|
|
36
|
+
}
|
|
37
|
+
export function sortCodexRecoveryCandidates(candidates) {
|
|
38
|
+
const withIndex = candidates.map((candidate, index) => ({ candidate, index }));
|
|
39
|
+
const weekPositive = withIndex.filter(({ candidate }) => getWeekRemaining(candidate.entry) > 0);
|
|
40
|
+
const pool = weekPositive.length > 0 ? weekPositive : withIndex;
|
|
41
|
+
const has5hPositiveInPool = pool.some(({ candidate }) => get5hRemaining(candidate.entry) > 0);
|
|
42
|
+
return pool
|
|
43
|
+
.slice()
|
|
44
|
+
.sort((a, b) => {
|
|
45
|
+
if (weekPositive.length > 0 && has5hPositiveInPool) {
|
|
46
|
+
const a5h = get5hRemaining(a.candidate.entry);
|
|
47
|
+
const b5h = get5hRemaining(b.candidate.entry);
|
|
48
|
+
if (a5h > 0 && b5h <= 0)
|
|
49
|
+
return -1;
|
|
50
|
+
if (a5h <= 0 && b5h > 0)
|
|
51
|
+
return 1;
|
|
52
|
+
const by5hResetAt = compareResetAt(pickFiniteNumber(a.candidate.entry.snapshot?.usage5h?.resetAt), pickFiniteNumber(b.candidate.entry.snapshot?.usage5h?.resetAt));
|
|
53
|
+
if (by5hResetAt !== 0)
|
|
54
|
+
return by5hResetAt;
|
|
55
|
+
}
|
|
56
|
+
const byWeekResetAt = compareResetAt(pickFiniteNumber(a.candidate.entry.snapshot?.usageWeek?.resetAt), pickFiniteNumber(b.candidate.entry.snapshot?.usageWeek?.resetAt));
|
|
57
|
+
if (byWeekResetAt !== 0)
|
|
58
|
+
return byWeekResetAt;
|
|
59
|
+
return a.index - b.index;
|
|
60
|
+
})
|
|
61
|
+
.map(({ candidate }) => candidate);
|
|
62
|
+
}
|
|
63
|
+
export async function recoverInvalidCodexAccount(input) {
|
|
64
|
+
const store = {
|
|
65
|
+
...input.store,
|
|
66
|
+
accounts: { ...input.store.accounts },
|
|
67
|
+
};
|
|
68
|
+
delete store.accounts[input.invalidAccountName];
|
|
69
|
+
const candidates = Object.entries(store.accounts).map(([name, entry]) => ({
|
|
70
|
+
name,
|
|
71
|
+
entry,
|
|
72
|
+
}));
|
|
73
|
+
const sorted = sortCodexRecoveryCandidates(candidates);
|
|
74
|
+
const replacement = sorted[0];
|
|
75
|
+
const noCandidates = !replacement;
|
|
76
|
+
const switched = Boolean(replacement);
|
|
77
|
+
const weekRecoveryOnly = Boolean(replacement
|
|
78
|
+
&& getWeekRemaining(replacement.entry) > 0
|
|
79
|
+
&& get5hRemaining(replacement.entry) <= 0);
|
|
80
|
+
if (replacement) {
|
|
81
|
+
store.active = replacement.name;
|
|
82
|
+
}
|
|
83
|
+
else if (store.active === input.invalidAccountName) {
|
|
84
|
+
delete store.active;
|
|
85
|
+
}
|
|
86
|
+
if (replacement && input.setAuth) {
|
|
87
|
+
await input.setAuth({
|
|
88
|
+
path: { id: "openai" },
|
|
89
|
+
body: {
|
|
90
|
+
type: "oauth",
|
|
91
|
+
refresh: replacement.entry.refresh,
|
|
92
|
+
access: replacement.entry.access,
|
|
93
|
+
expires: replacement.entry.expires,
|
|
94
|
+
accountId: replacement.entry.accountId,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
removed: input.invalidAccountName,
|
|
100
|
+
replacement: replacement?.name,
|
|
101
|
+
switched,
|
|
102
|
+
weekRecoveryOnly,
|
|
103
|
+
noCandidates,
|
|
104
|
+
store,
|
|
105
|
+
};
|
|
106
|
+
}
|
package/dist/codex-oauth.d.ts
CHANGED
|
@@ -7,11 +7,33 @@ export type TokenResponse = {
|
|
|
7
7
|
export type IdTokenClaims = {
|
|
8
8
|
chatgpt_account_id?: string;
|
|
9
9
|
organizations?: Array<{
|
|
10
|
-
id
|
|
10
|
+
id?: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
display_name?: string;
|
|
13
|
+
workspace_name?: string;
|
|
14
|
+
slug?: string;
|
|
11
15
|
}>;
|
|
16
|
+
organization?: {
|
|
17
|
+
id?: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
display_name?: string;
|
|
20
|
+
workspace_name?: string;
|
|
21
|
+
slug?: string;
|
|
22
|
+
};
|
|
23
|
+
workspace?: {
|
|
24
|
+
id?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
display_name?: string;
|
|
27
|
+
workspace_name?: string;
|
|
28
|
+
slug?: string;
|
|
29
|
+
};
|
|
30
|
+
workspace_name?: string;
|
|
12
31
|
email?: string;
|
|
13
32
|
"https://api.openai.com/auth"?: {
|
|
14
33
|
chatgpt_account_id?: string;
|
|
34
|
+
workspace_name?: string;
|
|
35
|
+
workspace_id?: string;
|
|
36
|
+
organization_id?: string;
|
|
15
37
|
};
|
|
16
38
|
};
|
|
17
39
|
export type CodexOAuthAccount = {
|
|
@@ -20,6 +42,7 @@ export type CodexOAuthAccount = {
|
|
|
20
42
|
expires?: number;
|
|
21
43
|
accountId?: string;
|
|
22
44
|
email?: string;
|
|
45
|
+
workspaceName?: string;
|
|
23
46
|
};
|
|
24
47
|
type OAuthMode = "browser" | "headless";
|
|
25
48
|
type RunCodexOAuthInput = {
|
|
@@ -35,5 +58,7 @@ type RunCodexOAuthInput = {
|
|
|
35
58
|
export declare function parseJwtClaims(token: string): IdTokenClaims | undefined;
|
|
36
59
|
export declare function extractAccountIdFromClaims(claims: IdTokenClaims): string | undefined;
|
|
37
60
|
export declare function extractAccountId(tokens: TokenResponse): string | undefined;
|
|
61
|
+
export declare function extractWorkspaceNameFromClaims(claims: IdTokenClaims): string | undefined;
|
|
62
|
+
export declare function extractWorkspaceName(tokens: TokenResponse): string | undefined;
|
|
38
63
|
export declare function runCodexOAuth(input?: RunCodexOAuthInput): Promise<CodexOAuthAccount | undefined>;
|
|
39
64
|
export {};
|
package/dist/codex-oauth.js
CHANGED
|
@@ -82,6 +82,32 @@ export function extractAccountId(tokens) {
|
|
|
82
82
|
const claims = parseJwtClaims(tokens.access_token);
|
|
83
83
|
return claims ? extractAccountIdFromClaims(claims) : undefined;
|
|
84
84
|
}
|
|
85
|
+
function pickWorkspaceLikeLabel(input) {
|
|
86
|
+
if (!input)
|
|
87
|
+
return undefined;
|
|
88
|
+
return input.workspace_name ?? input.display_name ?? input.name ?? input.slug ?? input.id;
|
|
89
|
+
}
|
|
90
|
+
export function extractWorkspaceNameFromClaims(claims) {
|
|
91
|
+
return (claims.workspace_name
|
|
92
|
+
|| claims["https://api.openai.com/auth"]?.workspace_name
|
|
93
|
+
|| claims["https://api.openai.com/auth"]?.workspace_id
|
|
94
|
+
|| claims["https://api.openai.com/auth"]?.organization_id
|
|
95
|
+
|| pickWorkspaceLikeLabel(claims.workspace)
|
|
96
|
+
|| pickWorkspaceLikeLabel(claims.organization)
|
|
97
|
+
|| pickWorkspaceLikeLabel(claims.organizations?.[0]));
|
|
98
|
+
}
|
|
99
|
+
export function extractWorkspaceName(tokens) {
|
|
100
|
+
if (tokens.id_token) {
|
|
101
|
+
const claims = parseJwtClaims(tokens.id_token);
|
|
102
|
+
const workspaceName = claims && extractWorkspaceNameFromClaims(claims);
|
|
103
|
+
if (workspaceName)
|
|
104
|
+
return workspaceName;
|
|
105
|
+
}
|
|
106
|
+
if (!tokens.access_token)
|
|
107
|
+
return undefined;
|
|
108
|
+
const claims = parseJwtClaims(tokens.access_token);
|
|
109
|
+
return claims ? extractWorkspaceNameFromClaims(claims) : undefined;
|
|
110
|
+
}
|
|
85
111
|
function extractEmail(tokens) {
|
|
86
112
|
if (tokens.id_token) {
|
|
87
113
|
const claims = parseJwtClaims(tokens.id_token);
|
|
@@ -291,12 +317,14 @@ function normalizeTokens(tokens, now) {
|
|
|
291
317
|
const access = tokens.access_token;
|
|
292
318
|
if (!refresh && !access)
|
|
293
319
|
return undefined;
|
|
320
|
+
const workspaceName = extractWorkspaceName(tokens);
|
|
294
321
|
return {
|
|
295
322
|
refresh,
|
|
296
323
|
access,
|
|
297
324
|
expires: now() + (tokens.expires_in ?? 3600) * 1000,
|
|
298
325
|
accountId: extractAccountId(tokens),
|
|
299
326
|
email: extractEmail(tokens),
|
|
327
|
+
...(workspaceName ? { workspaceName } : {}),
|
|
300
328
|
};
|
|
301
329
|
}
|
|
302
330
|
export async function runCodexOAuth(input = {}) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolveCodexAuthSource } from "./codex-auth-source.js";
|
|
2
2
|
import { fetchCodexStatus } from "./codex-status-fetcher.js";
|
|
3
3
|
import { getActiveCodexAccount, normalizeCodexStore, readCodexStore, writeCodexStore, } from "./codex-store.js";
|
|
4
|
+
import { getCodexDisplayName, recoverInvalidCodexAccount } from "./codex-invalid-account.js";
|
|
4
5
|
import { readAuth } from "./store.js";
|
|
5
6
|
export class CodexStatusCommandHandledError extends Error {
|
|
6
7
|
constructor() {
|
|
@@ -44,6 +45,12 @@ function ratio(remaining, entitlement) {
|
|
|
44
45
|
function value(value) {
|
|
45
46
|
return value === undefined ? "n/a" : String(value);
|
|
46
47
|
}
|
|
48
|
+
function pickWorkspaceLabel(input) {
|
|
49
|
+
return input.workspaceName
|
|
50
|
+
?? input.name
|
|
51
|
+
?? input.email
|
|
52
|
+
?? input.accountId;
|
|
53
|
+
}
|
|
47
54
|
function renderWindow(label, window) {
|
|
48
55
|
if (window.entitlement === 100 && window.remaining !== undefined) {
|
|
49
56
|
return `${label}: ${window.remaining}% left`;
|
|
@@ -51,16 +58,16 @@ function renderWindow(label, window) {
|
|
|
51
58
|
return `${label}: ${ratio(window.remaining, window.entitlement)}`;
|
|
52
59
|
}
|
|
53
60
|
function renderStatus(status) {
|
|
61
|
+
const identity = status.identity;
|
|
54
62
|
return [
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
63
|
+
`账号: ${value(identity.accountId ?? identity.email)}`,
|
|
64
|
+
`Workspace: ${value(pickWorkspaceLabel({
|
|
65
|
+
workspaceName: identity.workspaceName,
|
|
66
|
+
email: identity.email,
|
|
67
|
+
accountId: identity.accountId,
|
|
68
|
+
}))}`,
|
|
61
69
|
renderWindow("5h", status.windows.primary),
|
|
62
70
|
renderWindow("week", status.windows.secondary),
|
|
63
|
-
`credits: ${ratio(status.credits.remaining, status.credits.total)}`,
|
|
64
71
|
].join("\n");
|
|
65
72
|
}
|
|
66
73
|
function renderCachedStatus(store) {
|
|
@@ -68,14 +75,15 @@ function renderCachedStatus(store) {
|
|
|
68
75
|
const entry = active?.entry;
|
|
69
76
|
const snapshot = entry?.snapshot;
|
|
70
77
|
return [
|
|
71
|
-
|
|
72
|
-
`
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
78
|
+
`账号: ${value(entry?.accountId ?? active?.name ?? entry?.email)}`,
|
|
79
|
+
`Workspace: ${value(pickWorkspaceLabel({
|
|
80
|
+
workspaceName: entry?.workspaceName,
|
|
81
|
+
name: entry?.name ?? active?.name,
|
|
82
|
+
email: entry?.email,
|
|
83
|
+
accountId: entry?.accountId,
|
|
84
|
+
}))}`,
|
|
76
85
|
renderWindow("5h", snapshot?.usage5h ?? {}),
|
|
77
|
-
"week
|
|
78
|
-
"credits: n/a",
|
|
86
|
+
renderWindow("week", snapshot?.usageWeek ?? {}),
|
|
79
87
|
].join("\n");
|
|
80
88
|
}
|
|
81
89
|
function getCachedAccountForSource(store, input) {
|
|
@@ -96,14 +104,15 @@ function renderCachedStatusForAccount(store, input) {
|
|
|
96
104
|
const entry = active?.entry;
|
|
97
105
|
const snapshot = entry?.snapshot;
|
|
98
106
|
return [
|
|
99
|
-
|
|
100
|
-
`
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
107
|
+
`账号: ${value(entry?.accountId ?? active?.name ?? entry?.email)}`,
|
|
108
|
+
`Workspace: ${value(pickWorkspaceLabel({
|
|
109
|
+
workspaceName: entry?.workspaceName,
|
|
110
|
+
name: entry?.name ?? active?.name,
|
|
111
|
+
email: entry?.email,
|
|
112
|
+
accountId: entry?.accountId,
|
|
113
|
+
}))}`,
|
|
104
114
|
renderWindow("5h", snapshot?.usage5h ?? {}),
|
|
105
|
-
"week
|
|
106
|
-
"credits: n/a",
|
|
115
|
+
renderWindow("week", snapshot?.usageWeek ?? {}),
|
|
107
116
|
].join("\n");
|
|
108
117
|
}
|
|
109
118
|
function hasCachedStore(store) {
|
|
@@ -230,12 +239,55 @@ export async function handleCodexStatusCommand(input) {
|
|
|
230
239
|
},
|
|
231
240
|
}));
|
|
232
241
|
if (!fetched.ok) {
|
|
242
|
+
if (fetched.error.kind === "invalid_account") {
|
|
243
|
+
const currentRaw = await readStore().catch(() => ({}));
|
|
244
|
+
const currentStore = normalizeCodexStore(currentRaw);
|
|
245
|
+
const invalid = getCachedAccountForSource(currentStore, { accountId: source.accountId });
|
|
246
|
+
const invalidName = invalid?.name ?? currentStore.active;
|
|
247
|
+
if (invalidName && currentStore.accounts[invalidName]) {
|
|
248
|
+
const recovered = await recoverInvalidCodexAccount({
|
|
249
|
+
store: currentStore,
|
|
250
|
+
invalidAccountName: invalidName,
|
|
251
|
+
setAuth: input.client?.auth?.set
|
|
252
|
+
? async (next) => {
|
|
253
|
+
const authClient = input.client?.auth;
|
|
254
|
+
const setAuth = input.client?.auth?.set;
|
|
255
|
+
if (!setAuth)
|
|
256
|
+
return;
|
|
257
|
+
await setAuth.call(authClient, next);
|
|
258
|
+
}
|
|
259
|
+
: undefined,
|
|
260
|
+
});
|
|
261
|
+
await writeStore(recovered.store);
|
|
262
|
+
const removedDisplay = getCodexDisplayName(invalid?.entry, recovered.removed);
|
|
263
|
+
const messageLines = [`无效账号${removedDisplay}已移除,请及时检查核对`];
|
|
264
|
+
if (recovered.replacement) {
|
|
265
|
+
const replacementEntry = recovered.store.accounts[recovered.replacement];
|
|
266
|
+
const replacementDisplay = getCodexDisplayName(replacementEntry, recovered.replacement);
|
|
267
|
+
messageLines.push(`已切换到${replacementDisplay}`);
|
|
268
|
+
if (recovered.weekRecoveryOnly) {
|
|
269
|
+
messageLines.push("请检查账号状态");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
await showToast({
|
|
273
|
+
client: input.client,
|
|
274
|
+
message: messageLines.join("\n"),
|
|
275
|
+
variant: "warning",
|
|
276
|
+
});
|
|
277
|
+
throw new CodexStatusCommandHandledError();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
233
280
|
const cachedRaw = await readStore().catch(() => ({}));
|
|
234
281
|
const cached = normalizeCodexStore(cachedRaw);
|
|
235
282
|
if (hasCachedStore(cached)) {
|
|
236
283
|
await showToast({
|
|
237
284
|
client: input.client,
|
|
238
|
-
message: `Codex status fetch failed
|
|
285
|
+
message: `Codex status fetch failed: ${fetched.error.message}`,
|
|
286
|
+
variant: "warning",
|
|
287
|
+
});
|
|
288
|
+
await showToast({
|
|
289
|
+
client: input.client,
|
|
290
|
+
message: renderCachedStatusForAccount(cached, { accountId: source.accountId }),
|
|
239
291
|
variant: "warning",
|
|
240
292
|
});
|
|
241
293
|
}
|
|
@@ -279,6 +331,7 @@ export async function handleCodexStatusCommand(input) {
|
|
|
279
331
|
providerId: previousEntry.providerId ?? "codex",
|
|
280
332
|
accountId: fetched.status.identity.accountId ?? previousEntry.accountId ?? source.accountId,
|
|
281
333
|
email: fetched.status.identity.email ?? previousEntry.email,
|
|
334
|
+
workspaceName: fetched.status.identity.workspaceName ?? previousEntry.workspaceName,
|
|
282
335
|
lastUsed: fetched.status.updatedAt,
|
|
283
336
|
snapshot: {
|
|
284
337
|
...(previousEntry.snapshot ?? {}),
|
|
@@ -1,4 +1,49 @@
|
|
|
1
1
|
const CODEX_USAGE_URL = "https://chatgpt.com/backend-api/codex/usage";
|
|
2
|
+
function isInvalidAccountRefreshError(error) {
|
|
3
|
+
if (!error || typeof error !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
const value = error;
|
|
6
|
+
return value.kind === "invalid_account"
|
|
7
|
+
&& value.status === 400
|
|
8
|
+
&& typeof value.message === "string"
|
|
9
|
+
&& value.message.length > 0;
|
|
10
|
+
}
|
|
11
|
+
function readErrorStatus(error) {
|
|
12
|
+
if (!error || typeof error !== "object")
|
|
13
|
+
return undefined;
|
|
14
|
+
const value = error.status;
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
16
|
+
}
|
|
17
|
+
function readErrorMessage(error) {
|
|
18
|
+
if (error instanceof Error)
|
|
19
|
+
return error.message;
|
|
20
|
+
if (typeof error === "string")
|
|
21
|
+
return error;
|
|
22
|
+
if (error && typeof error === "object") {
|
|
23
|
+
const message = error.message;
|
|
24
|
+
if (typeof message === "string" && message.length > 0)
|
|
25
|
+
return message;
|
|
26
|
+
}
|
|
27
|
+
return String(error);
|
|
28
|
+
}
|
|
29
|
+
function mapRefreshTokenError(error) {
|
|
30
|
+
if (isInvalidAccountRefreshError(error))
|
|
31
|
+
return error;
|
|
32
|
+
const message = readErrorMessage(error);
|
|
33
|
+
const status = readErrorStatus(error);
|
|
34
|
+
const hasRefresh400Message = /refresh/i.test(message) && /\b400\b/.test(message);
|
|
35
|
+
if (status === 400 || hasRefresh400Message) {
|
|
36
|
+
return {
|
|
37
|
+
kind: "invalid_account",
|
|
38
|
+
status: 400,
|
|
39
|
+
message,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
kind: "network_error",
|
|
44
|
+
message,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
2
47
|
function asRecord(input) {
|
|
3
48
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
4
49
|
return undefined;
|
|
@@ -154,12 +199,10 @@ export async function fetchCodexStatus(input) {
|
|
|
154
199
|
refreshed = await input.refreshTokens(oauth);
|
|
155
200
|
}
|
|
156
201
|
catch (error) {
|
|
202
|
+
const mappedError = mapRefreshTokenError(error);
|
|
157
203
|
return {
|
|
158
204
|
ok: false,
|
|
159
|
-
error:
|
|
160
|
-
kind: "network_error",
|
|
161
|
-
message: error instanceof Error ? error.message : String(error),
|
|
162
|
-
},
|
|
205
|
+
error: mappedError,
|
|
163
206
|
};
|
|
164
207
|
}
|
|
165
208
|
if (!refreshed || !refreshed.access) {
|
package/dist/codex-store.d.ts
CHANGED
package/dist/codex-store.js
CHANGED
|
@@ -60,6 +60,8 @@ function pickEntry(input) {
|
|
|
60
60
|
next.name = pickString(source.name);
|
|
61
61
|
if (pickString(source.providerId))
|
|
62
62
|
next.providerId = pickString(source.providerId);
|
|
63
|
+
if (pickString(source.workspaceName))
|
|
64
|
+
next.workspaceName = pickString(source.workspaceName);
|
|
63
65
|
if (pickString(source.refresh))
|
|
64
66
|
next.refresh = pickString(source.refresh);
|
|
65
67
|
if (pickString(source.access))
|
|
@@ -3,6 +3,7 @@ import { stdin as input, stdout as output } from "node:process";
|
|
|
3
3
|
import { fetchCodexStatus } from "../codex-status-fetcher.js";
|
|
4
4
|
import { runCodexOAuth } from "../codex-oauth.js";
|
|
5
5
|
import { getActiveCodexAccount, readCodexStore, writeCodexStore, } from "../codex-store.js";
|
|
6
|
+
import { recoverInvalidCodexAccount } from "../codex-invalid-account.js";
|
|
6
7
|
import { readAuth } from "../store.js";
|
|
7
8
|
function pickName(input) {
|
|
8
9
|
const accountId = input.accountId?.trim();
|
|
@@ -44,6 +45,13 @@ function toMenuQuota(entry) {
|
|
|
44
45
|
completions: undefined,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
48
|
+
function withoutRecoveryWarning(snapshot) {
|
|
49
|
+
if (!snapshot)
|
|
50
|
+
return undefined;
|
|
51
|
+
const next = { ...snapshot };
|
|
52
|
+
delete next.recoveryWarning;
|
|
53
|
+
return next;
|
|
54
|
+
}
|
|
47
55
|
async function promptText(message) {
|
|
48
56
|
const rl = createInterface({ input, output });
|
|
49
57
|
try {
|
|
@@ -66,8 +74,11 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
66
74
|
const authorizeOpenAIOAuth = inputDeps.runCodexOAuth ?? runCodexOAuth;
|
|
67
75
|
const refreshSnapshots = async (store) => {
|
|
68
76
|
const names = Object.keys(store.accounts);
|
|
77
|
+
const pendingRecoveryWarnings = new Map();
|
|
69
78
|
for (const name of names) {
|
|
70
79
|
const entry = store.accounts[name];
|
|
80
|
+
if (!entry)
|
|
81
|
+
continue;
|
|
71
82
|
const oauth = toOAuth(entry);
|
|
72
83
|
if (!oauth.access && !oauth.refresh) {
|
|
73
84
|
store.accounts[name] = {
|
|
@@ -88,6 +99,30 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
88
99
|
},
|
|
89
100
|
}));
|
|
90
101
|
if (!result.ok) {
|
|
102
|
+
if (result.error.kind === "invalid_account") {
|
|
103
|
+
const recovered = await recoverInvalidCodexAccount({
|
|
104
|
+
store,
|
|
105
|
+
invalidAccountName: name,
|
|
106
|
+
setAuth: async (next) => {
|
|
107
|
+
await inputDeps.client.auth.set(next);
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
store.accounts = recovered.store.accounts;
|
|
111
|
+
store.active = recovered.store.active;
|
|
112
|
+
if (recovered.replacement) {
|
|
113
|
+
const replacement = store.accounts[recovered.replacement];
|
|
114
|
+
const weekRemaining = replacement?.snapshot?.usageWeek?.remaining ?? 0;
|
|
115
|
+
const fiveHourRemaining = replacement?.snapshot?.usage5h?.remaining ?? 0;
|
|
116
|
+
if (weekRemaining > 0 && fiveHourRemaining <= 0) {
|
|
117
|
+
pendingRecoveryWarnings.set(recovered.replacement, {
|
|
118
|
+
code: "week_recovery_only",
|
|
119
|
+
removed: recovered.removed,
|
|
120
|
+
replacement: recovered.replacement,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
91
126
|
store.accounts[name] = {
|
|
92
127
|
...entry,
|
|
93
128
|
snapshot: {
|
|
@@ -113,10 +148,11 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
113
148
|
...(result.authPatch?.accountId !== undefined ? { accountId: result.authPatch.accountId } : {}),
|
|
114
149
|
name: nextName,
|
|
115
150
|
providerId: "openai",
|
|
151
|
+
workspaceName: result.status.identity.workspaceName ?? entry.workspaceName,
|
|
116
152
|
accountId: result.status.identity.accountId ?? result.authPatch?.accountId ?? entry.accountId,
|
|
117
153
|
email: result.status.identity.email ?? entry.email,
|
|
118
154
|
snapshot: {
|
|
119
|
-
...(entry.snapshot ?? {}),
|
|
155
|
+
...(withoutRecoveryWarning(entry.snapshot) ?? {}),
|
|
120
156
|
plan: result.status.identity.plan ?? entry.snapshot?.plan,
|
|
121
157
|
usage5h: {
|
|
122
158
|
entitlement: result.status.windows.primary.entitlement,
|
|
@@ -141,6 +177,24 @@ export function createCodexMenuAdapter(inputDeps) {
|
|
|
141
177
|
if (store.active === name || !store.active)
|
|
142
178
|
store.active = nextName;
|
|
143
179
|
}
|
|
180
|
+
for (const [name, entry] of Object.entries(store.accounts)) {
|
|
181
|
+
const warning = pendingRecoveryWarnings.get(name);
|
|
182
|
+
const snapshot = withoutRecoveryWarning(entry.snapshot);
|
|
183
|
+
store.accounts[name] = {
|
|
184
|
+
...entry,
|
|
185
|
+
...(snapshot ? { snapshot } : {}),
|
|
186
|
+
};
|
|
187
|
+
if (!warning)
|
|
188
|
+
continue;
|
|
189
|
+
const nextSnapshot = {
|
|
190
|
+
...(store.accounts[name].snapshot ?? {}),
|
|
191
|
+
};
|
|
192
|
+
nextSnapshot.recoveryWarning = warning;
|
|
193
|
+
store.accounts[name] = {
|
|
194
|
+
...store.accounts[name],
|
|
195
|
+
snapshot: nextSnapshot,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
144
198
|
};
|
|
145
199
|
return {
|
|
146
200
|
key: "codex",
|