codex-multi-auth 2.21.0 → 2.23.0
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/.codex-plugin/plugin.json +1 -1
- package/README.md +8 -5
- package/config/schema/config.schema.json +8 -0
- package/dist/lib/accounts.d.ts +5 -0
- package/dist/lib/accounts.js +33 -4
- package/dist/lib/api-route-store.d.ts +7 -0
- package/dist/lib/api-route-store.js +100 -31
- package/dist/lib/auth/account-access.js +2 -1
- package/dist/lib/auth/auth.js +3 -2
- package/dist/lib/auth/device-auth.js +2 -1
- package/dist/lib/auth/http.d.ts +1 -0
- package/dist/lib/auth/http.js +1 -0
- package/dist/lib/codex-manager/account-manager-commands.js +1 -0
- package/dist/lib/codex-manager/api-login-menu.d.ts +1 -1
- package/dist/lib/codex-manager/api-login-menu.js +4 -6
- package/dist/lib/codex-manager/commands/locale.d.ts +12 -0
- package/dist/lib/codex-manager/commands/locale.js +25 -0
- package/dist/lib/codex-manager/commands/status.d.ts +5 -1
- package/dist/lib/codex-manager/commands/status.js +501 -13
- package/dist/lib/codex-manager/help.js +3 -1
- package/dist/lib/codex-manager.js +98 -23
- package/dist/lib/config.js +3 -0
- package/dist/lib/constants.d.ts +1 -0
- package/dist/lib/constants.js +1 -0
- package/dist/lib/locale.d.ts +7 -0
- package/dist/lib/locale.js +35 -0
- package/dist/lib/model-route-policy.d.ts +1 -0
- package/dist/lib/model-route-policy.js +2 -1
- package/dist/lib/quota-probe.js +2 -1
- package/dist/lib/request/proxy-fetch.d.ts +5 -0
- package/dist/lib/request/proxy-fetch.js +37 -0
- package/dist/lib/request/url-rewriting.js +17 -0
- package/dist/lib/runtime/account-model-catalog.d.ts +3 -0
- package/dist/lib/runtime/account-model-catalog.js +7 -2
- package/dist/lib/runtime/api-model-capabilities.d.ts +11 -2
- package/dist/lib/runtime/api-model-capabilities.js +155 -24
- package/dist/lib/runtime/app-bind.d.ts +7 -0
- package/dist/lib/runtime/app-bind.js +92 -1
- package/dist/lib/runtime/automatic-api-checks.d.ts +15 -0
- package/dist/lib/runtime/automatic-api-checks.js +188 -0
- package/dist/lib/runtime/model-discovery-status.d.ts +11 -1
- package/dist/lib/runtime/model-discovery-status.js +64 -49
- package/dist/lib/runtime/persisted-api-capabilities.d.ts +20 -0
- package/dist/lib/runtime/persisted-api-capabilities.js +387 -0
- package/dist/lib/runtime/responses-websocket.d.ts +48 -5
- package/dist/lib/runtime/responses-websocket.js +275 -39
- package/dist/lib/runtime/rotation-account-selection.d.ts +25 -5
- package/dist/lib/runtime/rotation-account-selection.js +94 -4
- package/dist/lib/runtime/rotation-proxy-state.d.ts +26 -0
- package/dist/lib/runtime/rotation-proxy-state.js +376 -4
- package/dist/lib/runtime/rotation-server-types.d.ts +6 -0
- package/dist/lib/runtime/rotation-token-refresh.d.ts +23 -1
- package/dist/lib/runtime/rotation-token-refresh.js +218 -27
- package/dist/lib/runtime-rotation-proxy.js +601 -105
- package/dist/lib/schemas.d.ts +5 -0
- package/dist/lib/schemas.js +1 -0
- package/dist/lib/storage/credential-sidecars.js +34 -2
- package/dist/lib/storage/file-lock.d.ts +2 -0
- package/dist/lib/storage/file-lock.js +44 -30
- package/dist/lib/usage/ledger.js +1 -0
- package/dist/lib/usage/redaction.js +1 -0
- package/dist/lib/usage/types.d.ts +1 -1
- package/dist/lib/web-dashboard-html.d.ts +1 -0
- package/dist/lib/web-dashboard-html.js +66 -0
- package/dist/lib/web-dashboard.d.ts +36 -0
- package/dist/lib/web-dashboard.js +322 -0
- package/package.json +3 -1
- package/scripts/codex-app-router.js +86 -14
- package/scripts/codex-routing.js +1 -0
- package/scripts/codex.js +150 -13
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.23.0",
|
|
4
4
|
"description": "Install and operate codex-multi-auth for the official @openai/codex CLI with multi-account OAuth rotation, switching, health checks, and recovery tools.",
|
|
5
5
|
"interface": {
|
|
6
6
|
"composerIcon": "./assets/codex-multi-auth-icon.svg"
|
package/README.md
CHANGED
|
@@ -16,8 +16,9 @@ Using OpenCode rather than the Codex CLI? The sibling project [`oc-codex-multi-a
|
|
|
16
16
|
## What it does
|
|
17
17
|
|
|
18
18
|
- **Account pool** — OAuth login for multiple ChatGPT accounts, stored locally under `~/.codex/multi-auth` (files `0600`, directories `0700`), with per-project pools under `projects/<project-key>/`.
|
|
19
|
-
- **Switching and selection** — pin an account, pick the forecast-best one, or let health- and quota-aware hybrid selection rotate for you.
|
|
20
|
-
- **Runtime rotation** — a default-on, loopback-only proxy that forwards Responses API
|
|
19
|
+
- **Switching and selection** — pin an account, pick the forecast-best one, follow saved account order within customized priority tiers with sequential scheduling, or let health- and quota-aware hybrid selection rotate for you.
|
|
20
|
+
- **Runtime rotation** — a default-on, loopback-only proxy that forwards Responses API, model-discovery, image, thread-goal, and Codex dictation/transcription traffic through managed accounts during wrapper-launched Codex sessions.
|
|
21
|
+
- **API capability evidence** — opted-in API/ZDR routes receive daily billable checks while the router runs; status distinguishes configured access programs from verified responses. See [check behavior and controls](docs/reference/commands.md#codex-multi-auth-check).
|
|
21
22
|
- **Diagnostics and repair** — status, live health checks, quota forecasts, JSON reports, and `fix`/`doctor`/`verify` commands that recover stale or damaged local state.
|
|
22
23
|
- **Luna Reserve fallback** — treats `gpt-reserve` as a separately metered Luna allowance, automatically falls back after a genuine Luna quota 429, and exposes the Reserve percentage through `limits --json --refresh`.
|
|
23
24
|
- **Local governance** — file-backed usage ledger, budget guards, account tags/weights/pause/drain policies, routing profiles, and an optional loopback bridge with bearer tokens for local OpenAI-compatible clients.
|
|
@@ -27,7 +28,8 @@ Using OpenCode rather than the Codex CLI? The sibling project [`oc-codex-multi-a
|
|
|
27
28
|
|
|
28
29
|
| Binary | Role |
|
|
29
30
|
| --- | --- |
|
|
30
|
-
| `codex-multi-auth` | The account manager. Runs all
|
|
31
|
+
| `codex-multi-auth` | The account manager. Runs all management subcommands locally (`login`, `status`, `web`, `switch`, `forecast`, `rotation`, `fix`, ...). The `auth` prefix is optional: `codex-multi-auth login` ≡ `codex-multi-auth auth login`. |
|
|
32
|
+
| `codex-ma` | Short alias for `codex-multi-auth`, including the local real-time web dashboard. |
|
|
31
33
|
| `codex-multi-auth-codex` | Opt-in wrapper. Handles `auth ...` locally and forwards every other command to the official Codex CLI, enabling the rotation proxy for request-bearing commands. |
|
|
32
34
|
| `mcodex` | Convenience launcher over the wrapper, with optional `--monitor` and `--tmux` modes. No account logic of its own. |
|
|
33
35
|
| `codex-multi-auth-app-launcher` | User-level OS launcher routing helper (Windows shortcut, macOS wrapper app, Linux `.desktop`). |
|
|
@@ -51,6 +53,7 @@ codex-multi-auth login
|
|
|
51
53
|
|
|
52
54
|
# 2. Repeat login to add more accounts, then inspect the pool
|
|
53
55
|
codex-multi-auth status
|
|
56
|
+
codex-ma web --no-open
|
|
54
57
|
|
|
55
58
|
# 3. Probe account health and preview which account would serve next
|
|
56
59
|
codex-multi-auth check
|
|
@@ -94,7 +97,7 @@ codex-multi-auth rotation status # is live rotation enabled?
|
|
|
94
97
|
|
|
95
98
|
## Runtime rotation, in one paragraph
|
|
96
99
|
|
|
97
|
-
With rotation on (the default), request-bearing wrapper commands — `exec`, `review`, `resume`, `fork`, `app`, and the bare TUI — route through a local proxy that picks the healthiest account per request, refreshes tokens as needed, and fails over across the pool on rate limits within a bounded retry budget. The proxy listens on loopback only, authenticates its own clients with a per-process token, forwards only Responses API, model-discovery, image,
|
|
100
|
+
With rotation on (the default), request-bearing wrapper commands — `exec`, `review`, `resume`, `fork`, `app`, and the bare TUI — route through a local proxy that picks the healthiest account per request, refreshes tokens as needed, and fails over across the pool on rate limits within a bounded retry budget. The proxy listens on loopback only, authenticates its own clients with a per-process token, forwards only Responses API, model-discovery, image, thread-goal, and `POST /transcribe` calls, and never writes account emails or tokens to client headers or logs. Transcription preserves the incoming multipart audio body and boundary, uses managed ChatGPT OAuth, and does not replay ambiguous audio uploads across accounts. Plain `codex` is never touched: rotation exists only inside `codex-multi-auth-codex`/`mcodex` sessions, or inside the packaged desktop app after an opt-in, reversible bind. Turn it off with `codex-multi-auth rotation disable` or `CODEX_MULTI_AUTH_RUNTIME_ROTATION_PROXY=0`.
|
|
98
101
|
|
|
99
102
|
## Where state lives
|
|
100
103
|
|
|
@@ -138,7 +141,7 @@ Common symptoms — wrong active account, OAuth callback port `1455` already bou
|
|
|
138
141
|
|
|
139
142
|
## Release notes
|
|
140
143
|
|
|
141
|
-
- Current stable: [v2.
|
|
144
|
+
- Current stable: [v2.23.0](docs/releases/v2.23.0.md) — `npm i -g codex-multi-auth`
|
|
142
145
|
- Earlier stable lines: [v2.6.0](docs/releases/v2.6.0.md), [v2.5.0](docs/releases/v2.5.0.md)
|
|
143
146
|
- Full release archive: [docs/README.md#release-history](docs/README.md#release-history)
|
|
144
147
|
|
package/dist/lib/accounts.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export declare class AccountManager {
|
|
|
70
70
|
private readonly persistedWorkspaceSelections;
|
|
71
71
|
private readonly persistedWorkspaces;
|
|
72
72
|
private hadPersistedStorage;
|
|
73
|
+
private readonly loadedAccountStorageSnapshot;
|
|
73
74
|
private cursorByFamily;
|
|
74
75
|
private currentAccountIndexByFamily;
|
|
75
76
|
private lastToastAccountIndex;
|
|
@@ -103,6 +104,10 @@ export declare class AccountManager {
|
|
|
103
104
|
*/
|
|
104
105
|
private routingMutexMode;
|
|
105
106
|
static loadFromDisk(authFallback?: OAuthAuthDetails): Promise<AccountManager>;
|
|
107
|
+
/** @internal True when construction received a parsed/recovered storage snapshot. */
|
|
108
|
+
hasLoadedAccountStorageSnapshot(): boolean;
|
|
109
|
+
/** @internal True only when this account identity was explicitly persisted, not legacy-derived in memory. */
|
|
110
|
+
hasPersistedRecordId(account: ManagedAccount): boolean;
|
|
106
111
|
hasRefreshToken(refreshToken: string): boolean;
|
|
107
112
|
private hydrateFromCodexCli;
|
|
108
113
|
constructor(authFallback?: OAuthAuthDetails, stored?: AccountStorageV3 | null);
|
package/dist/lib/accounts.js
CHANGED
|
@@ -142,6 +142,7 @@ export class AccountManager {
|
|
|
142
142
|
persistedWorkspaceSelections = new WeakMap();
|
|
143
143
|
persistedWorkspaces = new WeakMap();
|
|
144
144
|
hadPersistedStorage = false;
|
|
145
|
+
loadedAccountStorageSnapshot;
|
|
145
146
|
cursorByFamily = initFamilyState(0);
|
|
146
147
|
currentAccountIndexByFamily = initFamilyState(-1);
|
|
147
148
|
lastToastAccountIndex = -1;
|
|
@@ -202,6 +203,18 @@ export class AccountManager {
|
|
|
202
203
|
await manager.hydrateFromCodexCli();
|
|
203
204
|
return manager;
|
|
204
205
|
}
|
|
206
|
+
/** @internal True when construction received a parsed/recovered storage snapshot. */
|
|
207
|
+
hasLoadedAccountStorageSnapshot() {
|
|
208
|
+
return this.loadedAccountStorageSnapshot;
|
|
209
|
+
}
|
|
210
|
+
/** @internal True only when this account identity was explicitly persisted, not legacy-derived in memory. */
|
|
211
|
+
hasPersistedRecordId(account) {
|
|
212
|
+
const recordId = account.recordId?.trim();
|
|
213
|
+
if (!recordId || !this.persistenceBaseline)
|
|
214
|
+
return false;
|
|
215
|
+
const baseline = this.persistenceBaseline.accounts.find((row) => resolveAccountRecordId(row) === recordId);
|
|
216
|
+
return typeof baseline?.recordId === "string" && baseline.recordId.trim().length > 0;
|
|
217
|
+
}
|
|
205
218
|
hasRefreshToken(refreshToken) {
|
|
206
219
|
return this.accounts.some((account) => account.refreshToken === refreshToken);
|
|
207
220
|
}
|
|
@@ -266,6 +279,7 @@ export class AccountManager {
|
|
|
266
279
|
}
|
|
267
280
|
constructor(authFallback, stored) {
|
|
268
281
|
this.storagePathState = { ...getStoragePathState() };
|
|
282
|
+
this.loadedAccountStorageSnapshot = stored !== undefined && stored !== null;
|
|
269
283
|
this.hadPersistedStorage = existsSync(this.resolveSelectionStoragePath());
|
|
270
284
|
const fallbackAccountId = extractAccountId(authFallback?.access)?.trim() || undefined;
|
|
271
285
|
const fallbackAccountEmail = sanitizeEmail(extractAccountEmail(authFallback?.access));
|
|
@@ -522,6 +536,7 @@ export class AccountManager {
|
|
|
522
536
|
_runtimeTrackerKey: trackerKey,
|
|
523
537
|
circuitKeyId,
|
|
524
538
|
rateLimitResetTimes: { ...account.rateLimitResetTimes },
|
|
539
|
+
workspaces: account.workspaces?.map((workspace) => ({ ...workspace })),
|
|
525
540
|
};
|
|
526
541
|
});
|
|
527
542
|
}
|
|
@@ -1377,6 +1392,14 @@ export class AccountManager {
|
|
|
1377
1392
|
if (!storedAccount) {
|
|
1378
1393
|
return null;
|
|
1379
1394
|
}
|
|
1395
|
+
const liveAccount = this.getAccountByIdentity(source, auth);
|
|
1396
|
+
// Credential rotation proves auth health only. It must not erase an
|
|
1397
|
+
// unrelated inference network/server/rate-limit backoff that happens
|
|
1398
|
+
// to be live on the same account.
|
|
1399
|
+
const preserveNonAuthCooldown = liveAccount?.cooldownReason !== undefined &&
|
|
1400
|
+
liveAccount.cooldownReason !== "auth-failure" &&
|
|
1401
|
+
typeof liveAccount.coolingDownUntil === "number" &&
|
|
1402
|
+
liveAccount.coolingDownUntil > nowMs();
|
|
1380
1403
|
storedAccount.refreshToken = auth.refresh;
|
|
1381
1404
|
storedAccount.accessToken = auth.access;
|
|
1382
1405
|
storedAccount.expiresAt = auth.expires;
|
|
@@ -1391,9 +1414,14 @@ export class AccountManager {
|
|
|
1391
1414
|
storedAccount.enabled = undefined;
|
|
1392
1415
|
delete storedAccount.authInvalidatedAt;
|
|
1393
1416
|
delete storedAccount.authInvalidationErrorCode;
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1417
|
+
if (preserveNonAuthCooldown && liveAccount) {
|
|
1418
|
+
storedAccount.coolingDownUntil = liveAccount.coolingDownUntil;
|
|
1419
|
+
storedAccount.cooldownReason = liveAccount.cooldownReason;
|
|
1420
|
+
}
|
|
1421
|
+
else {
|
|
1422
|
+
delete storedAccount.coolingDownUntil;
|
|
1423
|
+
delete storedAccount.cooldownReason;
|
|
1424
|
+
}
|
|
1397
1425
|
if (liveAccount) {
|
|
1398
1426
|
const previousLiveAccountState = {
|
|
1399
1427
|
access: liveAccount.access,
|
|
@@ -1411,7 +1439,8 @@ export class AccountManager {
|
|
|
1411
1439
|
};
|
|
1412
1440
|
this.updateFromAuth(liveAccount, auth);
|
|
1413
1441
|
liveAccount.enabled = true;
|
|
1414
|
-
|
|
1442
|
+
if (!preserveNonAuthCooldown)
|
|
1443
|
+
this.clearAccountCooldown(liveAccount);
|
|
1415
1444
|
this.clearAuthFailures(liveAccount);
|
|
1416
1445
|
try {
|
|
1417
1446
|
await this.persistSnapshot(_current, nextStorage, persist, storedAccount);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
declare const routeSchema: z.ZodObject<{
|
|
3
|
+
capabilityEpoch: z.ZodOptional<z.ZodString>;
|
|
4
|
+
legacyCapabilityCache: z.ZodOptional<z.ZodBoolean>;
|
|
3
5
|
id: z.ZodString;
|
|
4
6
|
label: z.ZodString;
|
|
5
7
|
kind: z.ZodEnum<{
|
|
@@ -15,9 +17,14 @@ declare const routeSchema: z.ZodObject<{
|
|
|
15
17
|
accessPrograms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
16
18
|
modelAccessPrograms: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>>;
|
|
17
19
|
}, z.core.$strip>;
|
|
20
|
+
export declare const MAX_API_ROUTES = 100;
|
|
18
21
|
export type ApiRouteCredential = z.infer<typeof routeSchema>;
|
|
19
22
|
export declare function getApiRoutesPath(): string;
|
|
20
23
|
export declare function loadApiRoutes(path?: string): Promise<ApiRouteCredential[]>;
|
|
21
24
|
export declare function saveApiRoutes(routes: ApiRouteCredential[], path?: string, expected?: ApiRouteCredential[]): Promise<void>;
|
|
25
|
+
/** Return our committed snapshot, never a later writer's configuration. */
|
|
26
|
+
export declare function saveApiRoutesSnapshot(routes: ApiRouteCredential[], path?: string, expected?: ApiRouteCredential[]): Promise<ApiRouteCredential[]>;
|
|
22
27
|
export declare function updateApiModelDiscovery(route: ApiRouteCredential, models: string[], path?: string): Promise<ApiRouteCredential>;
|
|
28
|
+
/** Migrate legacy ownership before taking any capability lease or cache lock. */
|
|
29
|
+
export declare function ensureApiRouteCapabilityEpoch(route: ApiRouteCredential, signal?: AbortSignal): Promise<ApiRouteCredential | undefined>;
|
|
23
30
|
export {};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { promises as fs } from "node:fs";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -15,6 +16,9 @@ const programId = z.string().min(1).max(80).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/
|
|
|
15
16
|
const accessPrograms = z.record(programId, z.array(programId).max(32))
|
|
16
17
|
.refine(value => Object.keys(value).length <= 16);
|
|
17
18
|
const routeSchema = z.object({
|
|
19
|
+
/** Internal lifetime token. Store-owned; never trust an incoming token. */
|
|
20
|
+
capabilityEpoch: z.string().uuid().optional(),
|
|
21
|
+
legacyCapabilityCache: z.boolean().optional(),
|
|
18
22
|
id: z
|
|
19
23
|
.string()
|
|
20
24
|
.min(1)
|
|
@@ -37,9 +41,10 @@ const routeSchema = z.object({
|
|
|
37
41
|
modelAccessPrograms: z.record(modelId, accessPrograms)
|
|
38
42
|
.refine(value => Object.keys(value).length <= 1000).optional(),
|
|
39
43
|
});
|
|
44
|
+
export const MAX_API_ROUTES = 100;
|
|
40
45
|
const schema = z.object({
|
|
41
46
|
version: z.literal(1),
|
|
42
|
-
routes: z.array(routeSchema).max(
|
|
47
|
+
routes: z.array(routeSchema).max(MAX_API_ROUTES),
|
|
43
48
|
});
|
|
44
49
|
export function getApiRoutesPath() {
|
|
45
50
|
return join(getCodexMultiAuthDir(), "api-routes.json");
|
|
@@ -52,6 +57,9 @@ function validate(value) {
|
|
|
52
57
|
throw new Error("Invalid API route configuration");
|
|
53
58
|
return parsed.data.routes;
|
|
54
59
|
}
|
|
60
|
+
class InvalidApiRouteContentsError extends Error {
|
|
61
|
+
constructor() { super("Invalid API route configuration"); }
|
|
62
|
+
}
|
|
55
63
|
export async function loadApiRoutes(path = getApiRoutesPath()) {
|
|
56
64
|
let raw;
|
|
57
65
|
try {
|
|
@@ -78,39 +86,80 @@ export async function loadApiRoutes(path = getApiRoutesPath()) {
|
|
|
78
86
|
return validate(JSON.parse(raw));
|
|
79
87
|
}
|
|
80
88
|
catch {
|
|
81
|
-
throw new
|
|
89
|
+
throw new InvalidApiRouteContentsError();
|
|
82
90
|
}
|
|
83
91
|
}
|
|
84
|
-
export
|
|
92
|
+
export function saveApiRoutes(routes, path = getApiRoutesPath(), expected) {
|
|
93
|
+
return saveApiRoutesInternal(routes, path, expected);
|
|
94
|
+
}
|
|
95
|
+
/** Return our committed snapshot, never a later writer's configuration. */
|
|
96
|
+
export async function saveApiRoutesSnapshot(routes, path = getApiRoutesPath(), expected) {
|
|
97
|
+
let saved = [];
|
|
98
|
+
await saveApiRoutesInternal(routes, path, expected, false, undefined, value => { saved = value; });
|
|
99
|
+
return saved;
|
|
100
|
+
}
|
|
101
|
+
async function saveApiRoutesInternal(routes, path = getApiRoutesPath(), expected, discoveryExpansion = false, signal, onSaved) {
|
|
102
|
+
await withStorageLock(() => withFileTransactionLock(path, () => saveApiRoutesLocked(routes, path, expected, discoveryExpansion, signal, onSaved), { signal }));
|
|
103
|
+
}
|
|
104
|
+
async function saveApiRoutesLocked(routes, path = getApiRoutesPath(), expected, discoveryExpansion = false, signal, onSaved) {
|
|
85
105
|
const validated = validate({ version: 1, routes });
|
|
86
|
-
|
|
106
|
+
let serialized = JSON.stringify({ version: 1, routes: validated }) + "\n";
|
|
87
107
|
if (Buffer.byteLength(serialized) > 1024 * 1024)
|
|
88
108
|
throw new Error("API route configuration is too large");
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
}).catch((error) => {
|
|
109
|
-
if (error.code !== "ENOENT")
|
|
110
|
-
throw error;
|
|
111
|
-
});
|
|
109
|
+
signal?.throwIfAborted();
|
|
110
|
+
let current;
|
|
111
|
+
try {
|
|
112
|
+
current = await loadApiRoutes(path);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
// Only explicit replacement may repair parsed-content corruption. CAS,
|
|
116
|
+
// oversized reads and I/O failures must not be treated as an empty store.
|
|
117
|
+
if (expected !== undefined || !(error instanceof InvalidApiRouteContentsError))
|
|
118
|
+
throw error;
|
|
119
|
+
current = [];
|
|
120
|
+
}
|
|
121
|
+
await fs.mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
122
|
+
const temp = tempPathFor(path);
|
|
123
|
+
try {
|
|
124
|
+
if (expected &&
|
|
125
|
+
JSON.stringify(current) !==
|
|
126
|
+
JSON.stringify(validate({ version: 1, routes: expected }))) {
|
|
127
|
+
throw new Error("API route configuration changed; reopen the menu before saving.");
|
|
112
128
|
}
|
|
113
|
-
|
|
129
|
+
const owned = validated.map(route => {
|
|
130
|
+
const old = current.find(value => value.id === route.id);
|
|
131
|
+
const policy = (value) => JSON.stringify([
|
|
132
|
+
value.kind, value.apiKey, value.enabled, value.probeCapabilities,
|
|
133
|
+
value.accessPrograms, value.modelAccessPrograms,
|
|
134
|
+
// Visibility is a selection set; menu ordering is not a policy edit.
|
|
135
|
+
...(discoveryExpansion ? [] : [[...new Set(value.visibleModels)].sort()]),
|
|
136
|
+
]);
|
|
137
|
+
const sameLifetime = old && policy(old) === policy(route);
|
|
138
|
+
const withoutLegacy = { ...route };
|
|
139
|
+
delete withoutLegacy.legacyCapabilityCache;
|
|
140
|
+
return { ...withoutLegacy, capabilityEpoch: sameLifetime ? old.capabilityEpoch ?? randomUUID() : randomUUID(),
|
|
141
|
+
...(sameLifetime && (!old.capabilityEpoch || old.legacyCapabilityCache === true) ? { legacyCapabilityCache: true } : {}) };
|
|
142
|
+
});
|
|
143
|
+
serialized = JSON.stringify({ version: 1, routes: owned }) + "\n";
|
|
144
|
+
if (Buffer.byteLength(serialized) > 1024 * 1024)
|
|
145
|
+
throw Error("API route configuration is too large");
|
|
146
|
+
await fs.writeFile(temp, serialized, { mode: 0o600, flag: "wx" });
|
|
147
|
+
signal?.throwIfAborted();
|
|
148
|
+
await withRetry(() => fs.rename(temp, path), {
|
|
149
|
+
maxAttempts: 6,
|
|
150
|
+
backoffMs: 25,
|
|
151
|
+
});
|
|
152
|
+
onSaved?.(owned);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
await withRetry(() => fs.unlink(temp), {
|
|
156
|
+
maxAttempts: 6,
|
|
157
|
+
backoffMs: 25,
|
|
158
|
+
}).catch((error) => {
|
|
159
|
+
if (error.code !== "ENOENT")
|
|
160
|
+
throw error;
|
|
161
|
+
});
|
|
162
|
+
}
|
|
114
163
|
}
|
|
115
164
|
/** Persist a discovery baseline and new GPT text IDs without undoing menu edits.
|
|
116
165
|
* Callers supply IDs already filtered for the Responses endpoint family. */
|
|
@@ -122,7 +171,8 @@ export function updateApiModelDiscovery(route, models, path = getApiRoutesPath()
|
|
|
122
171
|
}
|
|
123
172
|
async function updateApiModelDiscoverySerial(route, models, path = getApiRoutesPath()) {
|
|
124
173
|
const routes = await loadApiRoutes(path);
|
|
125
|
-
const current = routes.find((r) => r.id === route.id && r.apiKey === route.apiKey && r.kind === route.kind
|
|
174
|
+
const current = routes.find((r) => r.id === route.id && r.apiKey === route.apiKey && r.kind === route.kind &&
|
|
175
|
+
(!route.capabilityEpoch || route.capabilityEpoch === r.capabilityEpoch));
|
|
126
176
|
if (!current)
|
|
127
177
|
return { ...route, enabled: false, visibleModels: [] };
|
|
128
178
|
if (!current.enabled)
|
|
@@ -139,7 +189,26 @@ async function updateApiModelDiscoverySerial(route, models, path = getApiRoutesP
|
|
|
139
189
|
knownModels: [...known],
|
|
140
190
|
visibleModels: [...visible],
|
|
141
191
|
};
|
|
192
|
+
let persisted = updated;
|
|
142
193
|
if (JSON.stringify(updated) !== JSON.stringify(current))
|
|
143
|
-
await
|
|
144
|
-
|
|
194
|
+
await saveApiRoutesInternal(routes.map((r) => (r.id === current.id ? updated : r)), path, routes, true, undefined, values => { const saved = values.find(value => value.id === updated.id); if (saved)
|
|
195
|
+
persisted = saved; });
|
|
196
|
+
return persisted;
|
|
197
|
+
}
|
|
198
|
+
/** Migrate legacy ownership before taking any capability lease or cache lock. */
|
|
199
|
+
export async function ensureApiRouteCapabilityEpoch(route, signal) {
|
|
200
|
+
return withStorageLock(() => withFileTransactionLock(getApiRoutesPath(), async () => {
|
|
201
|
+
signal?.throwIfAborted();
|
|
202
|
+
const current = await loadApiRoutes();
|
|
203
|
+
const match = current.find(value => value.id === route.id && value.kind === route.kind && value.apiKey === route.apiKey);
|
|
204
|
+
if (!match || (route.capabilityEpoch && route.capabilityEpoch !== match.capabilityEpoch))
|
|
205
|
+
return undefined;
|
|
206
|
+
if (match.capabilityEpoch)
|
|
207
|
+
return match;
|
|
208
|
+
let migrated;
|
|
209
|
+
await saveApiRoutesLocked(current, getApiRoutesPath(), current, false, signal, values => {
|
|
210
|
+
migrated = values.find(value => value.id === route.id);
|
|
211
|
+
});
|
|
212
|
+
return migrated;
|
|
213
|
+
}, { signal }));
|
|
145
214
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CODEX_BASE_URL } from "../constants.js";
|
|
2
2
|
import { combineSignals } from "../utils.js";
|
|
3
|
+
import { fetchAuth } from "./http.js";
|
|
3
4
|
const ACCOUNTS_CHECK_PATH = "/wham/accounts/check";
|
|
4
5
|
const ACCOUNTS_CHECK_TIMEOUT_MS = 10_000;
|
|
5
6
|
function readString(value) {
|
|
@@ -49,7 +50,7 @@ export async function fetchAuthorizedAccounts(accessToken, options = {}) {
|
|
|
49
50
|
const token = readString(accessToken);
|
|
50
51
|
if (!token)
|
|
51
52
|
return null;
|
|
52
|
-
const doFetch = options.fetch ??
|
|
53
|
+
const doFetch = options.fetch ?? fetchAuth;
|
|
53
54
|
const timeout = AbortSignal.timeout(ACCOUNTS_CHECK_TIMEOUT_MS);
|
|
54
55
|
// combineSignals wraps AbortSignal.any (available on every Node the
|
|
55
56
|
// engines floor, `>=22.19`, permits) with null-tolerance; the native composite also detaches
|
package/dist/lib/auth/auth.js
CHANGED
|
@@ -2,6 +2,7 @@ import { randomBytes, webcrypto } from "node:crypto";
|
|
|
2
2
|
import { logError } from "../logger.js";
|
|
3
3
|
import { safeParseOAuthTokenResponse } from "../schemas.js";
|
|
4
4
|
import { isAbortError } from "../utils.js";
|
|
5
|
+
import { fetchAuth } from "./http.js";
|
|
5
6
|
// OAuth constants (from openai/codex)
|
|
6
7
|
export const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
7
8
|
export const AUTHORIZE_URL = "https://auth.openai.com/oauth/authorize";
|
|
@@ -396,7 +397,7 @@ export function parseAuthorizationInput(input) {
|
|
|
396
397
|
* @returns Token result
|
|
397
398
|
*/
|
|
398
399
|
export async function exchangeAuthorizationCode(code, verifier, redirectUri = REDIRECT_URI) {
|
|
399
|
-
const res = await
|
|
400
|
+
const res = await fetchAuth(TOKEN_URL, {
|
|
400
401
|
method: "POST",
|
|
401
402
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
402
403
|
body: new URLSearchParams({
|
|
@@ -468,7 +469,7 @@ export function decodeJWT(token) {
|
|
|
468
469
|
}
|
|
469
470
|
export async function refreshAccessToken(refreshToken, options = {}) {
|
|
470
471
|
try {
|
|
471
|
-
const response = await
|
|
472
|
+
const response = await fetchAuth(TOKEN_URL, {
|
|
472
473
|
method: "POST",
|
|
473
474
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
474
475
|
signal: options?.signal,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CLIENT_ID, exchangeAuthorizationCode, sanitizeOAuthResponseBodyForLog, } from "./auth.js";
|
|
2
|
+
import { fetchAuth } from "./http.js";
|
|
2
3
|
const DEVICE_AUTH_BASE_URL = "https://auth.openai.com";
|
|
3
4
|
const DEVICE_AUTH_API_BASE_URL = `${DEVICE_AUTH_BASE_URL}/api/accounts`;
|
|
4
5
|
export const DEVICE_AUTH_VERIFICATION_URL = `${DEVICE_AUTH_BASE_URL}/codex/device`;
|
|
@@ -14,7 +15,7 @@ const DEVICE_AUTH_TRANSIENT_PENDING_STATUSES = new Set([
|
|
|
14
15
|
504,
|
|
15
16
|
]);
|
|
16
17
|
function getFetch(options) {
|
|
17
|
-
return options.fetchImpl ??
|
|
18
|
+
return options.fetchImpl ?? fetchAuth;
|
|
18
19
|
}
|
|
19
20
|
function getNow(options) {
|
|
20
21
|
return options.now ?? Date.now;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fetchWithProxy as fetchAuth } from "../request/proxy-fetch.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { fetchWithProxy as fetchAuth } from "../request/proxy-fetch.js";
|
|
@@ -2,7 +2,7 @@ import { type ApiRouteCredential } from "../api-route-store.js";
|
|
|
2
2
|
import { type MenuItem } from "../ui/select.js";
|
|
3
3
|
interface MenuDeps {
|
|
4
4
|
load: () => Promise<ApiRouteCredential[]>;
|
|
5
|
-
save: (routes: ApiRouteCredential[], expected: ApiRouteCredential[]) => Promise<
|
|
5
|
+
save: (routes: ApiRouteCredential[], expected: ApiRouteCredential[]) => Promise<ApiRouteCredential[]>;
|
|
6
6
|
select: (items: MenuItem<string>[], message: string) => Promise<string | null>;
|
|
7
7
|
text: (message: string) => Promise<string>;
|
|
8
8
|
secret: () => Promise<string>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { createInterface } from "node:readline/promises";
|
|
3
3
|
import { Writable } from "node:stream";
|
|
4
|
-
import { loadApiRoutes,
|
|
4
|
+
import { loadApiRoutes, saveApiRoutesSnapshot, } from "../api-route-store.js";
|
|
5
5
|
import { ApiModelRuntime } from "../runtime/api-model-runtime.js";
|
|
6
6
|
import { select } from "../ui/select.js";
|
|
7
7
|
async function ask(message, secret = false) {
|
|
@@ -35,7 +35,7 @@ async function ask(message, secret = false) {
|
|
|
35
35
|
}
|
|
36
36
|
const defaults = {
|
|
37
37
|
load: loadApiRoutes,
|
|
38
|
-
save: (routes, expected) =>
|
|
38
|
+
save: (routes, expected) => saveApiRoutesSnapshot(routes, undefined, expected),
|
|
39
39
|
select: (items, message) => select(items, { message, allowEscape: true }),
|
|
40
40
|
text: (message) => ask(message),
|
|
41
41
|
secret: () => ask("API key (hidden): ", true),
|
|
@@ -189,8 +189,7 @@ export async function runApiLoginMenu(overrides = {}) {
|
|
|
189
189
|
}
|
|
190
190
|
if (action !== "models") {
|
|
191
191
|
const next = routes.map((r) => (r.id === route?.id ? route : r));
|
|
192
|
-
await d.save(next, routes);
|
|
193
|
-
routes = next;
|
|
192
|
+
routes = await d.save(next, routes);
|
|
194
193
|
continue;
|
|
195
194
|
}
|
|
196
195
|
}
|
|
@@ -203,8 +202,7 @@ export async function runApiLoginMenu(overrides = {}) {
|
|
|
203
202
|
const next = routes.some((r) => r.id === edited.id)
|
|
204
203
|
? routes.map((r) => (r.id === edited.id ? edited : r))
|
|
205
204
|
: [...routes, edited];
|
|
206
|
-
await d.save(next, routes);
|
|
207
|
-
routes = next;
|
|
205
|
+
routes = await d.save(next, routes);
|
|
208
206
|
d.log("Saved. Choose an API or ZDR model explicitly in Codex. Desktop login is unchanged.");
|
|
209
207
|
}
|
|
210
208
|
catch (error) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type OutputLocaleSetting } from "../../locale.js";
|
|
2
|
+
export interface LocaleCommandDeps {
|
|
3
|
+
loadConfig: () => {
|
|
4
|
+
locale?: OutputLocaleSetting;
|
|
5
|
+
};
|
|
6
|
+
saveConfig: (patch: {
|
|
7
|
+
locale: OutputLocaleSetting;
|
|
8
|
+
}) => Promise<void>;
|
|
9
|
+
logInfo?: (message: string) => void;
|
|
10
|
+
logError?: (message: string) => void;
|
|
11
|
+
}
|
|
12
|
+
export declare function runLocaleCommand(args: string[], deps: LocaleCommandDeps): Promise<number>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { localeName, normalizeOutputLocaleSetting, resolveOutputLocale, } from "../../locale.js";
|
|
2
|
+
export async function runLocaleCommand(args, deps) {
|
|
3
|
+
const logInfo = deps.logInfo ?? console.log;
|
|
4
|
+
const logError = deps.logError ?? console.error;
|
|
5
|
+
const requested = args[0]?.trim();
|
|
6
|
+
if (args.length > 1 || requested === "--help" || requested === "-h") {
|
|
7
|
+
logInfo("Usage: codex-multi-auth config locale [auto|en|zh]");
|
|
8
|
+
return requested === "--help" || requested === "-h" ? 0 : 1;
|
|
9
|
+
}
|
|
10
|
+
if (!requested) {
|
|
11
|
+
const configured = deps.loadConfig().locale ?? "auto";
|
|
12
|
+
const effective = resolveOutputLocale(configured);
|
|
13
|
+
logInfo(`Locale: ${configured} (effective: ${localeName(effective)})`);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
const locale = normalizeOutputLocaleSetting(requested);
|
|
17
|
+
if (!locale) {
|
|
18
|
+
logError(`Unsupported locale: ${requested}. Choose auto, en, or zh.`);
|
|
19
|
+
return 1;
|
|
20
|
+
}
|
|
21
|
+
await deps.saveConfig({ locale });
|
|
22
|
+
const effective = resolveOutputLocale(locale);
|
|
23
|
+
logInfo(`Locale saved: ${locale} (effective: ${localeName(effective)})`);
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
@@ -4,9 +4,10 @@ import { loadAccountPolicyStore } from "../../account-policy.js";
|
|
|
4
4
|
import { type ModelInventory } from "../../runtime/model-discovery-status.js";
|
|
5
5
|
import type { ModelFamily } from "../../prompts/codex.js";
|
|
6
6
|
import type { QuotaCacheData } from "../../quota-cache.js";
|
|
7
|
-
import type
|
|
7
|
+
import { type AppBindRouterStatus } from "../../runtime/app-bind.js";
|
|
8
8
|
import { type RuntimeAccountSignal } from "../../runtime/runtime-current-account.js";
|
|
9
9
|
import type { RuntimeObservabilitySnapshot } from "../../runtime/runtime-observability.js";
|
|
10
|
+
import { type OutputLocale } from "../../locale.js";
|
|
10
11
|
import type { AccountStorageV3, StorageHealthSummary } from "../../storage.js";
|
|
11
12
|
type LoadedStorage = AccountStorageV3 | null;
|
|
12
13
|
export interface StatusCommandDeps {
|
|
@@ -31,6 +32,9 @@ export interface StatusCommandDeps {
|
|
|
31
32
|
logInfo?: (message: string) => void;
|
|
32
33
|
/** When true, emit a single machine-readable JSON object instead of text (cli-manager-03). */
|
|
33
34
|
json?: boolean;
|
|
35
|
+
/** Compact human-facing summary. Undefined keeps the legacy detailed view for API callers. */
|
|
36
|
+
compact?: boolean;
|
|
37
|
+
locale?: OutputLocale;
|
|
34
38
|
}
|
|
35
39
|
export declare function runStatusCommand(deps: StatusCommandDeps): Promise<number>;
|
|
36
40
|
export interface FeaturesCommandDeps {
|