codex-multi-auth 2.8.7 → 2.9.1
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 +14 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +34 -0
- package/dist/lib/account-policy.d.ts +3 -1
- package/dist/lib/account-policy.d.ts.map +1 -1
- package/dist/lib/account-policy.js +14 -1
- package/dist/lib/budget-guard.d.ts +1 -0
- package/dist/lib/budget-guard.d.ts.map +1 -1
- package/dist/lib/budget-guard.js +13 -3
- package/dist/lib/codex-manager/account-pool-write.d.ts +8 -0
- package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -1
- package/dist/lib/codex-manager/account-pool-write.js +1 -1
- package/dist/lib/codex-manager/help.d.ts +2 -0
- package/dist/lib/codex-manager/help.d.ts.map +1 -1
- package/dist/lib/codex-manager/help.js +38 -1
- package/dist/lib/codex-manager/login-flow.d.ts +3 -0
- package/dist/lib/codex-manager/login-flow.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-flow.js +80 -6
- package/dist/lib/codex-manager/login-menu-actions.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-menu-actions.js +41 -5
- package/dist/lib/codex-manager/login-oauth.d.ts +38 -2
- package/dist/lib/codex-manager/login-oauth.d.ts.map +1 -1
- package/dist/lib/codex-manager/login-oauth.js +205 -23
- package/dist/lib/policy/runtime-policy.d.ts +7 -0
- package/dist/lib/policy/runtime-policy.d.ts.map +1 -1
- package/dist/lib/policy/runtime-policy.js +1 -0
- package/dist/lib/request/fetch-helpers.d.ts +7 -0
- package/dist/lib/request/fetch-helpers.d.ts.map +1 -1
- package/dist/lib/request/fetch-helpers.js +1 -1
- package/dist/lib/request/response-handler.d.ts +3 -1
- package/dist/lib/request/response-handler.d.ts.map +1 -1
- package/dist/lib/request/response-handler.js +26 -5
- package/dist/lib/request/stream-failover-runtime.d.ts +8 -1
- package/dist/lib/request/stream-failover-runtime.d.ts.map +1 -1
- package/dist/lib/request/stream-failover-runtime.js +9 -1
- package/dist/lib/request/stream-failover.d.ts.map +1 -1
- package/dist/lib/request/stream-failover.js +41 -0
- package/dist/lib/runtime/account-check.d.ts.map +1 -1
- package/dist/lib/runtime/account-check.js +8 -1
- package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
- package/dist/lib/runtime-rotation-proxy.js +46 -3
- package/dist/lib/storage.d.ts +19 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/storage.js +32 -0
- package/dist/lib/usage/index.d.ts +2 -0
- package/dist/lib/usage/index.d.ts.map +1 -1
- package/dist/lib/usage/index.js +2 -0
- package/dist/lib/usage/ledger.d.ts.map +1 -1
- package/dist/lib/usage/ledger.js +7 -0
- package/dist/lib/usage/pricing.d.ts +15 -0
- package/dist/lib/usage/pricing.d.ts.map +1 -1
- package/dist/lib/usage/pricing.js +24 -0
- package/dist/lib/usage/stream-usage-deferral.d.ts +39 -0
- package/dist/lib/usage/stream-usage-deferral.d.ts.map +1 -0
- package/dist/lib/usage/stream-usage-deferral.js +54 -0
- package/dist/lib/usage/types.d.ts +6 -0
- package/dist/lib/usage/types.d.ts.map +1 -1
- package/dist/lib/usage/usage-extraction.d.ts +42 -0
- package/dist/lib/usage/usage-extraction.d.ts.map +1 -0
- package/dist/lib/usage/usage-extraction.js +178 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-multi-auth",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.1",
|
|
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
|
@@ -161,6 +161,19 @@ codex-multi-auth forecast --live
|
|
|
161
161
|
|
|
162
162
|
If browser launch is blocked, use the alternate login paths in [docs/getting-started.md](docs/getting-started.md#alternate-login-paths).
|
|
163
163
|
For remote or headless shells, prefer `codex-multi-auth login --device-auth`.
|
|
164
|
+
To refresh one saved account without changing the active selection or manual
|
|
165
|
+
pin, run:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
codex-multi-auth login --account <index|email|account_id> --preserve-selection
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Add `--device-auth` on a remote shell. The refreshed OAuth identity must match
|
|
172
|
+
the requested saved account or nothing is written. Refreshing the account Codex
|
|
173
|
+
is currently using still publishes its new tokens to the native CLI; refreshing
|
|
174
|
+
any other account leaves the active selection and the manual pin untouched. A
|
|
175
|
+
disabled account stays disabled. `--account` cannot be combined with `--org`,
|
|
176
|
+
which would rebind the row to a different workspace.
|
|
164
177
|
|
|
165
178
|
---
|
|
166
179
|
|
|
@@ -392,7 +405,7 @@ codex-multi-auth doctor --json
|
|
|
392
405
|
|
|
393
406
|
## Release Notes
|
|
394
407
|
|
|
395
|
-
- Current stable: [docs/releases/v2.
|
|
408
|
+
- Current stable: [docs/releases/v2.9.1.md](docs/releases/v2.9.1.md) — install via `npm i -g codex-multi-auth`
|
|
396
409
|
- Previous stable: [docs/releases/v2.8.3.md](docs/releases/v2.8.3.md)
|
|
397
410
|
- Previous stable: [docs/releases/v2.8.2.md](docs/releases/v2.8.2.md)
|
|
398
411
|
- Previous stable: [docs/releases/v2.7.1.md](docs/releases/v2.7.1.md)
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,kBAAkB,CAAC;AAoW5D;;;;;;;;;;;;;;;GAeG;AAEH,eAAO,MAAM,iBAAiB,EAAE,MAuwI/B,CAAC;AAEF,eAAO,MAAM,gBAAgB,KAAoB,CAAC;AAElD,eAAe,iBAAiB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,14 @@ import { LiveAccountSync } from "./lib/live-account-sync.js";
|
|
|
38
38
|
import { clearCorrelationId, initLogger, logDebug, logError, logInfo, logRequest, logWarn, setCorrelationId, } from "./lib/logger.js";
|
|
39
39
|
import { PreemptiveQuotaScheduler, readQuotaSchedulerSnapshot, } from "./lib/preemptive-quota-scheduler.js";
|
|
40
40
|
import { createRuntimeUsageRecorder, evaluateRuntimePolicy, loadRuntimePolicyState, } from "./lib/policy/runtime-policy.js";
|
|
41
|
+
import { createStreamUsageDeferral } from "./lib/usage/stream-usage-deferral.js";
|
|
42
|
+
/**
|
|
43
|
+
* How long a streaming request's usage-ledger row may wait for the upstream
|
|
44
|
+
* `usage` event before it is written without token counts. Long enough not to
|
|
45
|
+
* truncate a genuinely slow completion; the timer is unref'd so it never holds
|
|
46
|
+
* the process open. See createStreamUsageDeferral.
|
|
47
|
+
*/
|
|
48
|
+
const USAGE_STREAM_RECORD_FALLBACK_MS = 15 * 60_000;
|
|
41
49
|
import { getModelFamily, MODEL_FAMILIES, prewarmCodexInstructions, } from "./lib/prompts/codex.js";
|
|
42
50
|
import { prewarmHostCodexPrompt } from "./lib/prompts/host-codex-prompt.js";
|
|
43
51
|
import { fetchCodexQuotaSnapshot, formatQuotaSnapshotLine, } from "./lib/quota-probe.js";
|
|
@@ -546,6 +554,18 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
546
554
|
async fetch(input, init) {
|
|
547
555
|
let usageRecorder = null;
|
|
548
556
|
let usageCompletion = null;
|
|
557
|
+
// A non-streaming response reports its token counts while
|
|
558
|
+
// handleSuccessResponse is still assembling the body, so they
|
|
559
|
+
// are folded straight into usageCompletion below. A streaming
|
|
560
|
+
// one reports them only as the client drains the body, long
|
|
561
|
+
// after the `finally` that writes the row — so that row is
|
|
562
|
+
// handed to the deferral instead of written empty.
|
|
563
|
+
const usageDeferral = createStreamUsageDeferral({
|
|
564
|
+
record: (completion) => {
|
|
565
|
+
void usageRecorder?.record(completion);
|
|
566
|
+
},
|
|
567
|
+
fallbackMs: USAGE_STREAM_RECORD_FALLBACK_MS,
|
|
568
|
+
});
|
|
549
569
|
try {
|
|
550
570
|
if (cachedAccountManager &&
|
|
551
571
|
cachedAccountManager !== accountManager) {
|
|
@@ -1594,6 +1614,7 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1594
1614
|
sessionAffinityStore?.updateLastResponseId(sessionAffinityKey, responseId, Date.now(), sessionAffinityVersion);
|
|
1595
1615
|
storedResponseIdForSuccess = true;
|
|
1596
1616
|
},
|
|
1617
|
+
onUsage: usageDeferral.onUsage,
|
|
1597
1618
|
streamStallTimeoutMs,
|
|
1598
1619
|
});
|
|
1599
1620
|
if (!isStreaming && emptyResponseMaxRetries > 0) {
|
|
@@ -1679,7 +1700,20 @@ export const OpenAIOAuthPlugin = async ({ client }) => {
|
|
|
1679
1700
|
accountId: successAccountForResponse.accountId ?? null,
|
|
1680
1701
|
email: successAccountForResponse.email ?? null,
|
|
1681
1702
|
},
|
|
1703
|
+
// Already known for a non-streaming response, which
|
|
1704
|
+
// reported its usage while the body was being
|
|
1705
|
+
// assembled above.
|
|
1706
|
+
...(usageDeferral.captured() ?? {}),
|
|
1682
1707
|
};
|
|
1708
|
+
if (isStreaming && !usageDeferral.captured()) {
|
|
1709
|
+
// Writing the row in the `finally` below would stamp
|
|
1710
|
+
// zero tokens — the client has not read a byte yet —
|
|
1711
|
+
// and the ledger is append-only, so the real counts
|
|
1712
|
+
// could never correct it. Zero tokens is exactly what
|
|
1713
|
+
// left maxTokens / maxCostUsd unenforceable here.
|
|
1714
|
+
usageDeferral.defer(usageCompletion);
|
|
1715
|
+
usageCompletion = null;
|
|
1716
|
+
}
|
|
1683
1717
|
if (lastCodexCliActiveSyncIndex !==
|
|
1684
1718
|
successAccountForResponse.index) {
|
|
1685
1719
|
void accountManager.syncCodexCliActiveSelectionForIndex(successAccountForResponse.index);
|
|
@@ -13,7 +13,9 @@ export interface AccountPolicyStore {
|
|
|
13
13
|
accounts: Record<string, AccountPolicy>;
|
|
14
14
|
}
|
|
15
15
|
export declare function getAccountPolicyPath(): string;
|
|
16
|
-
export declare function getAccountPolicyKey(account: Pick<AccountMetadataV3, "accountId" | "email"
|
|
16
|
+
export declare function getAccountPolicyKey(account: Pick<AccountMetadataV3, "accountId" | "email"> & {
|
|
17
|
+
refreshToken?: string | null;
|
|
18
|
+
}, _index?: number): string;
|
|
17
19
|
export declare function loadAccountPolicyStore(): Promise<AccountPolicyStore>;
|
|
18
20
|
export declare function saveAccountPolicyStore(store: AccountPolicyStore): Promise<void>;
|
|
19
21
|
export declare function upsertAccountPolicy(store: AccountPolicyStore, accountKey: string, mutate: (policy: AccountPolicy) => void, now?: number): AccountPolicy;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-policy.d.ts","sourceRoot":"","sources":["../../lib/account-policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAkFD,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"account-policy.d.ts","sourceRoot":"","sources":["../../lib/account-policy.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAItD,MAAM,WAAW,aAAa;IAC7B,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IAClC,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAkFD,wBAAgB,oBAAoB,IAAI,MAAM,CAE7C;AAED,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,IAAI,CAAC,iBAAiB,EAAE,WAAW,GAAG,OAAO,CAAC,GAAG;IACzD,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B,EACD,MAAM,CAAC,EAAE,MAAM,GACb,MAAM,CAmBR;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,kBAAkB,CAAC,CAa1E;AAED,wBAAsB,sBAAsB,CAC3C,KAAK,EAAE,kBAAkB,GACvB,OAAO,CAAC,IAAI,CAAC,CAsCf;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,kBAAkB,EACzB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,EACvC,GAAG,SAAa,GACd,aAAa,CAgBf;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAEtE;AAED,wBAAgB,oCAAoC,IAAI,IAAI,CAE3D"}
|
|
@@ -81,9 +81,22 @@ export function getAccountPolicyPath() {
|
|
|
81
81
|
return join(getCodexMultiAuthDir(), ACCOUNT_POLICY_FILE_NAME);
|
|
82
82
|
}
|
|
83
83
|
export function getAccountPolicyKey(account, _index) {
|
|
84
|
+
// The key must be stable across processes and independent of the account's
|
|
85
|
+
// slot (policy state is persisted and survives reordering), so it is derived
|
|
86
|
+
// from identity, never from `_index`.
|
|
87
|
+
//
|
|
88
|
+
// An account whose accountId AND email are both missing — freshly imported,
|
|
89
|
+
// or not yet hydrated from its token — used to hash the literal "unknown",
|
|
90
|
+
// which gave EVERY such account the same key: pausing, draining or tagging
|
|
91
|
+
// one silently applied to all of them. Fall back to the refresh token
|
|
92
|
+
// instead, which every account has and no two accounts share. It is
|
|
93
|
+
// namespaced before hashing so a token value can never collide with an
|
|
94
|
+
// email or accountId of the same text, and only its digest is persisted.
|
|
95
|
+
// Mirrors getAccountIdentityKey's `allowRefreshFallback`.
|
|
96
|
+
const refreshFallback = account.refreshToken?.trim();
|
|
84
97
|
const identity = account.accountId?.trim() ||
|
|
85
98
|
account.email?.trim().toLowerCase() ||
|
|
86
|
-
"unknown";
|
|
99
|
+
(refreshFallback ? `refresh:${refreshFallback}` : "unknown");
|
|
87
100
|
return `sha256:${createHash("sha256").update(identity).digest("hex")}`;
|
|
88
101
|
}
|
|
89
102
|
export async function loadAccountPolicyStore() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"budget-guard.d.ts","sourceRoot":"","sources":["../../lib/budget-guard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"budget-guard.d.ts","sourceRoot":"","sources":["../../lib/budget-guard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAIrD,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7D,MAAM,WAAW,WAAW;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAChC,OAAO,EAAE,CAAC,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,qBAAqB;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,YAAY,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;QAChB,gBAAgB,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,MAAM,EAAE;QACP,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;QAC3B,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1B,CAAC;CACF;AA6ED,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAE/D;AAED,wBAAsB,oBAAoB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAatE;AAED,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAsCjF;AAED,wBAAgB,iBAAiB,CAChC,KAAK,EAAE,gBAAgB,EACvB,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,EACrC,GAAG,SAAa,GACd,WAAW,CAOb;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,SAAa,GAAG,MAAM,CAoBnF;AAED,wBAAgB,mBAAmB,CAClC,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,YAAY,GACnB,qBAAqB,CA+CvB;AAED,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD"}
|
package/dist/lib/budget-guard.js
CHANGED
|
@@ -175,9 +175,18 @@ export function evaluateBudgetGuard(limit, summary) {
|
|
|
175
175
|
summary.totals.totalTokens >= limit.maxTokens) {
|
|
176
176
|
reasons.push(`token limit reached (${summary.totals.totalTokens}/${limit.maxTokens})`);
|
|
177
177
|
}
|
|
178
|
-
if (typeof limit.maxCostUsd === "number"
|
|
179
|
-
summary.totals.costUsd >= limit.maxCostUsd) {
|
|
180
|
-
|
|
178
|
+
if (typeof limit.maxCostUsd === "number") {
|
|
179
|
+
if (summary.totals.costUsd >= limit.maxCostUsd) {
|
|
180
|
+
reasons.push(`cost limit reached (${summary.totals.costUsd.toFixed(6)}/${limit.maxCostUsd.toFixed(6)})`);
|
|
181
|
+
}
|
|
182
|
+
else if ((summary.totals.unpricedRequests ?? 0) > 0) {
|
|
183
|
+
// The window contains token usage we have no rate for, so `costUsd` is
|
|
184
|
+
// an under-count by an unknown amount and this limit cannot be
|
|
185
|
+
// evaluated. Counting unknown cost as zero is what made a cost budget
|
|
186
|
+
// silently unenforceable for every unpriced model; an unevaluable spend
|
|
187
|
+
// limit fails closed instead. Priced models are unaffected.
|
|
188
|
+
reasons.push(`cost limit cannot be evaluated: ${summary.totals.unpricedRequests} request(s) in this window used a model with no known price, so recorded cost (${summary.totals.costUsd.toFixed(6)}) is incomplete. Price the model in lib/usage/pricing.ts, or budget on --requests/--tokens instead of --cost.`);
|
|
189
|
+
}
|
|
181
190
|
}
|
|
182
191
|
return {
|
|
183
192
|
key: limit.key,
|
|
@@ -188,6 +197,7 @@ export function evaluateBudgetGuard(limit, summary) {
|
|
|
188
197
|
requests: summary.totals.requests,
|
|
189
198
|
totalTokens: summary.totals.totalTokens,
|
|
190
199
|
costUsd: summary.totals.costUsd,
|
|
200
|
+
unpricedRequests: summary.totals.unpricedRequests,
|
|
191
201
|
},
|
|
192
202
|
limits: {
|
|
193
203
|
maxRequests: limit.maxRequests ?? null,
|
|
@@ -30,6 +30,14 @@ export interface ResolvedAccountWrite {
|
|
|
30
30
|
accessToken?: string;
|
|
31
31
|
expiresAt?: number;
|
|
32
32
|
workspaces?: Workspace[];
|
|
33
|
+
/**
|
|
34
|
+
* Keep the saved row's `enabled` flag instead of re-enabling it. A targeted
|
|
35
|
+
* re-authentication only tops up credentials for a row the user named, so it
|
|
36
|
+
* must not pull a deliberately disabled account back into rotation behind
|
|
37
|
+
* their back. A plain login still re-enables: signing in is the user asking
|
|
38
|
+
* for that account to be usable again.
|
|
39
|
+
*/
|
|
40
|
+
preserveEnabledState?: boolean;
|
|
33
41
|
now: number;
|
|
34
42
|
}
|
|
35
43
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account-pool-write.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/account-pool-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,SAAS,EAAE,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,EAC/C,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,GAC/B,SAAS,EAAE,GAAG,SAAS,CAYzB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,uBAAuB,CAAC,EACzE,gBAAgB,EAAE,SAAS,EAAE,GAAG,SAAS,GACvC,MAAM,GAAG,SAAS,CAwBpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAwBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,CA+ChE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wBAAwB,EAAE,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,QAAQ,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,MAAM,CAAC;KACtB,EACD,OAAO,EAAE;QAAE,wCAAwC,EAAE,OAAO,CAAA;KAAE,KAC1D,MAAM,GAAG,SAAS,CAAC;CACxB,GAAG;IACH,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACxC,CA6CA"}
|
|
1
|
+
{"version":3,"file":"account-pool-write.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/account-pool-write.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAChD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,uBAAuB,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAEzE;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;IACvD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,GAAG,EAAE,MAAM,CAAC;CACZ;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,CACxC,UAAU,EAAE,SAAS,EAAE,EACvB,SAAS,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAWR;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACrC,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,CAAC,EAC/C,QAAQ,EAAE,SAAS,EAAE,GAAG,SAAS,GAC/B,SAAS,EAAE,GAAG,SAAS,CAYzB;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC3C,QAAQ,EAAE,IAAI,CAAC,iBAAiB,EAAE,YAAY,GAAG,uBAAuB,CAAC,EACzE,gBAAgB,EAAE,SAAS,EAAE,GAAG,SAAS,GACvC,MAAM,GAAG,SAAS,CAwBpB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CACnC,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,UAAU,CAAA;CAAE,CAwBrD;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAClC,QAAQ,EAAE,iBAAiB,EAC3B,KAAK,EAAE,oBAAoB,GACzB;IAAE,OAAO,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAA;CAAE,CA+ChE;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,MAAM,EAAE,oBAAoB,EAAE,CAAC;IAC/B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wBAAwB,EAAE,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,QAAQ,EAAE;QACT,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,YAAY,CAAC,EAAE,MAAM,CAAC;KACtB,EACD,OAAO,EAAE;QAAE,wCAAwC,EAAE,OAAO,CAAA;KAAE,KAC1D,MAAM,GAAG,SAAS,CAAC;CACxB,GAAG;IACH,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,uBAAuB,GAAG,IAAI,CAAC;CACxC,CA6CA"}
|
|
@@ -115,7 +115,7 @@ export function buildUpdatedAccount(existing, write) {
|
|
|
115
115
|
refreshToken: write.refreshToken,
|
|
116
116
|
accessToken: write.accessToken,
|
|
117
117
|
expiresAt: write.expiresAt,
|
|
118
|
-
enabled: true,
|
|
118
|
+
enabled: write.preserveEnabledState ? existing.enabled : true,
|
|
119
119
|
authInvalidatedAt: undefined,
|
|
120
120
|
authInvalidationErrorCode: undefined,
|
|
121
121
|
lastUsed: write.now,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/help.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,IAAI,IAAI,CAsDjC;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,GAAG,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"help.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/help.ts"],"names":[],"mappings":"AAIA,wBAAgB,UAAU,IAAI,IAAI,CAsDjC;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,OAAO,CAAC;IACpB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAC5B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,gBAAgB,CAAA;CAAE,GACvC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,mBAAmB,CAuGtE;AAED,UAAU,cAAc;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GACrC;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEnD,wBAAgB,cAAc,IAAI,IAAI,CAgBrC;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,cAAc,CAyD5D"}
|
|
@@ -5,7 +5,7 @@ export function printUsage() {
|
|
|
5
5
|
"Codex Multi-Auth CLI",
|
|
6
6
|
"",
|
|
7
7
|
"Start here:",
|
|
8
|
-
" codex-multi-auth login [--device-auth|--manual|--no-browser] [--org <org_id>]",
|
|
8
|
+
" codex-multi-auth login [--device-auth|--manual|--no-browser] [--org <org_id>] [--preserve-selection] [--account <index|email|account_id>]",
|
|
9
9
|
" codex-multi-auth status [--json] (list is the same command)",
|
|
10
10
|
" codex-multi-auth check (always live-probes)",
|
|
11
11
|
"",
|
|
@@ -72,6 +72,33 @@ export function parseAuthLoginArgs(args) {
|
|
|
72
72
|
options.deviceAuth = true;
|
|
73
73
|
continue;
|
|
74
74
|
}
|
|
75
|
+
if (arg === "--preserve-selection") {
|
|
76
|
+
options.preserveSelection = true;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
if (arg === "--account" || arg?.startsWith("--account=")) {
|
|
80
|
+
let value;
|
|
81
|
+
if (arg === "--account") {
|
|
82
|
+
value = args[i + 1];
|
|
83
|
+
i += 1;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
value = arg.slice("--account=".length);
|
|
87
|
+
}
|
|
88
|
+
const trimmed = value?.trim();
|
|
89
|
+
if (!trimmed || trimmed.startsWith("--")) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
reason: "error",
|
|
93
|
+
message: "Missing value for --account. Usage: codex-multi-auth login --account <index|email|account_id>",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
options.account = trimmed;
|
|
97
|
+
// A targeted re-auth must never select the refreshed account as a
|
|
98
|
+
// side effect, even if the caller omits the redundant safety flag.
|
|
99
|
+
options.preserveSelection = true;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
75
102
|
if (arg === "--org" || arg?.startsWith("--org=")) {
|
|
76
103
|
// Bind this login to a specific workspace/org id (issue #491). Reuses
|
|
77
104
|
// the CODEX_AUTH_ACCOUNT_ID override mechanism so the same email's
|
|
@@ -112,6 +139,16 @@ export function parseAuthLoginArgs(args) {
|
|
|
112
139
|
message: `Cannot combine --device-auth with ${conflict}`,
|
|
113
140
|
};
|
|
114
141
|
}
|
|
142
|
+
// --org rebinds a login to a different workspace, which is exactly the
|
|
143
|
+
// identity change a targeted re-auth is built to refuse. Rejecting the pair
|
|
144
|
+
// up front beats failing after the user has already completed a sign-in.
|
|
145
|
+
if (options.account && options.org) {
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
reason: "error",
|
|
149
|
+
message: "Cannot combine --account with --org. Re-authenticate the saved account on its own workspace, or register the other workspace with --org on its own.",
|
|
150
|
+
};
|
|
151
|
+
}
|
|
115
152
|
return { ok: true, options };
|
|
116
153
|
}
|
|
117
154
|
export function printBestUsage() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AccountMetadataV3, type AccountStorageV3 } from "../storage.js";
|
|
1
2
|
import { type RepairCommandDeps } from "./repair-commands.js";
|
|
2
3
|
/**
|
|
3
4
|
* Interactive control loop for the `login` command: the account dashboard menu
|
|
@@ -13,5 +14,7 @@ export interface LoginFlowDeps {
|
|
|
13
14
|
createRepairCommandDeps(): RepairCommandDeps;
|
|
14
15
|
}
|
|
15
16
|
/** @internal */
|
|
17
|
+
export declare function resolveLoginAccountTarget(storage: AccountStorageV3, identifier: string): AccountMetadataV3 | null;
|
|
18
|
+
/** @internal */
|
|
16
19
|
export declare function runAuthLogin(args: string[], deps: LoginFlowDeps): Promise<number>;
|
|
17
20
|
//# sourceMappingURL=login-flow.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-flow.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-flow.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"login-flow.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-flow.ts"],"names":[],"mappings":"AAQA,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EAUrB,MAAM,eAAe,CAAC;AAmCvB,OAAO,EACN,KAAK,iBAAiB,EAGtB,MAAM,sBAAsB,CAAC;AAM9B;;;;;;;GAOG;AAEH,gBAAgB;AAChB,MAAM,WAAW,aAAa;IAC7B,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC7C,uBAAuB,IAAI,iBAAiB,CAAC;CAC7C;AAkCD,gBAAgB;AAChB,wBAAgB,yBAAyB,CACxC,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,MAAM,GAChB,iBAAiB,GAAG,IAAI,CAkB1B;AAED,gBAAgB;AAChB,wBAAsB,YAAY,CACjC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,EAAE,aAAa,GACjB,OAAO,CAAC,MAAM,CAAC,CAqBjB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { sanitizeEmail } from "../accounts.js";
|
|
1
2
|
import { isBrowserLaunchSuppressed } from "../auth/browser.js";
|
|
2
3
|
import { promptAddAnotherAccount, promptLoginMode } from "../cli.js";
|
|
3
4
|
import { ACCOUNT_LIMITS } from "../constants.js";
|
|
@@ -6,6 +7,7 @@ import { createLogger } from "../logger.js";
|
|
|
6
7
|
import { loadQuotaCache } from "../quota-cache.js";
|
|
7
8
|
import { resolveActiveIndex } from "../runtime/account-status.js";
|
|
8
9
|
import { clearAccounts, formatStorageErrorHint, getNamedBackups, loadAccounts, loadFlaggedAccounts, restoreAccountsFromBackup, setStoragePath, StorageError, } from "../storage.js";
|
|
10
|
+
import { CodexValidationError } from "../errors.js";
|
|
9
11
|
import { UI_COPY } from "../ui/ui-copy.js";
|
|
10
12
|
import { confirm } from "../ui/confirm.js";
|
|
11
13
|
import { stylePromptText } from "./formatters/index.js";
|
|
@@ -38,6 +40,23 @@ async function clearAccountsAndReset() {
|
|
|
38
40
|
await clearAccounts();
|
|
39
41
|
}
|
|
40
42
|
/** @internal */
|
|
43
|
+
export function resolveLoginAccountTarget(storage, identifier) {
|
|
44
|
+
if (/^[1-9]\d*$/.test(identifier)) {
|
|
45
|
+
const byIndex = storage.accounts[Number(identifier) - 1];
|
|
46
|
+
return byIndex ?? null;
|
|
47
|
+
}
|
|
48
|
+
const byAccountId = storage.accounts.filter((account) => account.accountId === identifier);
|
|
49
|
+
if (byAccountId.length === 1)
|
|
50
|
+
return byAccountId[0] ?? null;
|
|
51
|
+
if (byAccountId.length > 1)
|
|
52
|
+
return null;
|
|
53
|
+
const email = sanitizeEmail(identifier);
|
|
54
|
+
if (!email)
|
|
55
|
+
return null;
|
|
56
|
+
const byEmail = storage.accounts.filter((account) => sanitizeEmail(account.email) === email);
|
|
57
|
+
return byEmail.length === 1 ? (byEmail[0] ?? null) : null;
|
|
58
|
+
}
|
|
59
|
+
/** @internal */
|
|
41
60
|
export async function runAuthLogin(args, deps) {
|
|
42
61
|
const parsedArgs = parseAuthLoginArgs(args);
|
|
43
62
|
if (!parsedArgs.ok) {
|
|
@@ -197,7 +216,10 @@ async function runAuthLoginFlow(loginOptions, deps) {
|
|
|
197
216
|
// (--device-auth, --manual, --no-browser), they want to add a new account
|
|
198
217
|
// directly. Skipping the dashboard menu keeps `login --device-auth`
|
|
199
218
|
// usable from scripts and matches the documented behavior of the help text.
|
|
200
|
-
const explicitSignInMode = loginOptions.deviceAuth ||
|
|
219
|
+
const explicitSignInMode = loginOptions.deviceAuth ||
|
|
220
|
+
loginOptions.manual ||
|
|
221
|
+
loginOptions.preserveSelection ||
|
|
222
|
+
Boolean(loginOptions.account);
|
|
201
223
|
loginFlow: while (true) {
|
|
202
224
|
const existingStorage = await loadAccounts();
|
|
203
225
|
if (!explicitSignInMode &&
|
|
@@ -210,6 +232,23 @@ async function runAuthLoginFlow(loginOptions, deps) {
|
|
|
210
232
|
}
|
|
211
233
|
const refreshedStorage = await loadAccounts();
|
|
212
234
|
let existingCount = refreshedStorage?.accounts.length ?? 0;
|
|
235
|
+
const expectedAccount = loginOptions.account
|
|
236
|
+
? refreshedStorage
|
|
237
|
+
? resolveLoginAccountTarget(refreshedStorage, loginOptions.account)
|
|
238
|
+
: null
|
|
239
|
+
: undefined;
|
|
240
|
+
if (loginOptions.account && !expectedAccount) {
|
|
241
|
+
console.error("The requested Codex account is missing or ambiguous. No login was started.");
|
|
242
|
+
return 1;
|
|
243
|
+
}
|
|
244
|
+
// Positional hint for the write, re-verified (never trusted) inside the
|
|
245
|
+
// storage transaction. resolveLoginAccountTarget hands back a row out of
|
|
246
|
+
// this very array, so indexOf is an exact identity lookup.
|
|
247
|
+
const expectedAccountIndex = expectedAccount && refreshedStorage
|
|
248
|
+
? refreshedStorage.accounts.indexOf(expectedAccount)
|
|
249
|
+
: -1;
|
|
250
|
+
// A non-empty pool already forces a fresh sign-in, and a resolved target
|
|
251
|
+
// implies the pool was non-empty, so it adds no condition of its own.
|
|
213
252
|
let forceNewLogin = existingCount > 0;
|
|
214
253
|
let onboardingBackupDiscoveryWarning = null;
|
|
215
254
|
const loadNamedBackupsForOnboarding = async () => {
|
|
@@ -290,6 +329,13 @@ async function runAuthLoginFlow(loginOptions, deps) {
|
|
|
290
329
|
parsed: targetIndex + 1,
|
|
291
330
|
switchReason: "restore",
|
|
292
331
|
preserveActiveIndexByFamily: true,
|
|
332
|
+
// A restore replaces the whole account list, so every
|
|
333
|
+
// index moves. Session affinity remembers accounts BY
|
|
334
|
+
// INDEX, so without this a live proxy keeps routing
|
|
335
|
+
// in-flight sessions to whatever now occupies the old
|
|
336
|
+
// slot. `switch`/`best` already bump for a single
|
|
337
|
+
// move; a restore moves everything. See issue #474.
|
|
338
|
+
bumpAffinityGeneration: true,
|
|
293
339
|
});
|
|
294
340
|
console.log(UI_COPY.oauth.restoreBackupLoaded(selectedBackup.fileName, restoredStorage.accounts.length));
|
|
295
341
|
if (!synced) {
|
|
@@ -339,9 +385,31 @@ async function runAuthLoginFlow(loginOptions, deps) {
|
|
|
339
385
|
console.error(`Login failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
|
|
340
386
|
return 1;
|
|
341
387
|
}
|
|
342
|
-
const resolved = resolveAccountSelection(tokenResult, loginOptions.org);
|
|
343
|
-
|
|
344
|
-
|
|
388
|
+
const resolved = resolveAccountSelection(tokenResult, loginOptions.org, expectedAccount?.accountId);
|
|
389
|
+
let persistResult;
|
|
390
|
+
try {
|
|
391
|
+
persistResult = await persistAccountPool([resolved], false, {
|
|
392
|
+
preserveSelection: loginOptions.preserveSelection,
|
|
393
|
+
expectedAccount: expectedAccount ?? undefined,
|
|
394
|
+
expectedAccountIndex: expectedAccountIndex >= 0 ? expectedAccountIndex : undefined,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
catch (error) {
|
|
398
|
+
if (error instanceof CodexValidationError) {
|
|
399
|
+
console.error(`Re-authentication failed: ${error.message}`);
|
|
400
|
+
return 1;
|
|
401
|
+
}
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
// Publish to ~/.codex/auth.json exactly when this write owns the
|
|
405
|
+
// selection. Skipping the sync whenever --preserve-selection was passed
|
|
406
|
+
// stranded the native CLI on the expired tokens of the very account the
|
|
407
|
+
// user had just refreshed, and nothing downstream repaired it: the
|
|
408
|
+
// drift check compares identity, not tokens, so a same-identity refresh
|
|
409
|
+
// never looks like drift.
|
|
410
|
+
if (!persistResult || persistResult.isActiveAccount) {
|
|
411
|
+
await syncSelectionToCodex(resolved);
|
|
412
|
+
}
|
|
345
413
|
const latestStorage = await loadAccounts();
|
|
346
414
|
const count = latestStorage?.accounts.length ?? 1;
|
|
347
415
|
existingCount = count;
|
|
@@ -350,16 +418,22 @@ async function runAuthLoginFlow(loginOptions, deps) {
|
|
|
350
418
|
// Only claim a new saved slot when one was actually appended. A
|
|
351
419
|
// same-email login that maps onto an existing entry updates or
|
|
352
420
|
// rebinds it instead of growing the pool (issue #512).
|
|
353
|
-
const outcomeMessage =
|
|
421
|
+
const outcomeMessage = persistResult?.outcome === "rebound"
|
|
354
422
|
? `Rebound workspace for existing account. Total: ${count}`
|
|
355
|
-
:
|
|
423
|
+
: persistResult?.outcome === "updated"
|
|
356
424
|
? `Updated existing account. Total: ${count}`
|
|
357
425
|
: `Added account. Total: ${count}`;
|
|
358
426
|
console.log(outcomeMessage);
|
|
427
|
+
if (persistResult && !persistResult.accountEnabled) {
|
|
428
|
+
console.log(`Account ${persistResult.accountIndex + 1} is still disabled and stays out of rotation. Re-enable it from the manage menu in "codex-multi-auth login".`);
|
|
429
|
+
}
|
|
359
430
|
console.log("Next steps:");
|
|
360
431
|
console.log(" codex-multi-auth status Check that the wrapper is active.");
|
|
361
432
|
console.log(" codex-multi-auth check Confirm your saved accounts look healthy.");
|
|
362
433
|
console.log(" codex-multi-auth list Review saved accounts before switching.");
|
|
434
|
+
if (loginOptions.preserveSelection) {
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
363
437
|
if (count >= ACCOUNT_LIMITS.MAX_ACCOUNTS) {
|
|
364
438
|
console.log(`Reached maximum account limit (${ACCOUNT_LIMITS.MAX_ACCOUNTS}).`);
|
|
365
439
|
// Same reasoning as the addAnother=false branch below: in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-menu-actions.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-menu-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"login-menu-actions.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-menu-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,WAAW,CAAC;AAGjD,OAAO,EAEN,KAAK,gBAAgB,EAKrB,KAAK,kBAAkB,EAGvB,MAAM,eAAe,CAAC;AASvB,OAAO,EACN,KAAK,eAAe,EAKpB,MAAM,kBAAkB,CAAC;AAG1B;;;;;GAKG;AAEH,gBAAgB;AAChB,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE7D,gBAAgB;AAChB,wBAAsB,qBAAqB,CAC1C,YAAY,EAAE,kBAAkB,GAAG,IAAI,EACvC,sBAAsB,GAAE,MAAM,GAAG,IAAW,GAC1C,OAAO,CAAC,eAAe,CAAC,CA6D1B;AAED,gBAAgB;AAChB,wBAAsB,uBAAuB,CAC5C,YAAY,EAAE,kBAAkB,GAC9B,OAAO,CAAC,iBAAiB,CAAC,CA0C5B;AAED,gBAAgB;AAChB,wBAAsB,2BAA2B,CAChD,OAAO,EAAE,kBAAkB,EAAE,GAC3B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAkCpC;AA8GD,gBAAgB;AAChB,wBAAsB,kBAAkB,CACvC,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,OAAO,CAAC,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CAkKf"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { stdin as input, stdout as output } from "node:process";
|
|
2
2
|
import { sanitizeEmail } from "../accounts.js";
|
|
3
|
+
import { CodexValidationError } from "../errors.js";
|
|
3
4
|
import { MODEL_FAMILIES } from "../prompts/codex.js";
|
|
4
|
-
import { findMatchingAccountIndex, loadAccounts, reconcilePinnedAccountIndex, setStoragePath, withAccountStorageTransaction, } from "../storage.js";
|
|
5
|
+
import { bumpStorageAffinityGeneration, findMatchingAccountIndex, loadAccounts, reconcilePinnedAccountIndex, setStoragePath, withAccountStorageTransaction, } from "../storage.js";
|
|
5
6
|
import { UI_COPY } from "../ui/ui-copy.js";
|
|
6
7
|
import { getUiRuntimeOptions } from "../ui/runtime.js";
|
|
7
8
|
import { select } from "../ui/select.js";
|
|
@@ -249,6 +250,12 @@ export async function handleManageAction(storage, menuResult) {
|
|
|
249
250
|
nextStorage.accounts.splice(nextIndex, 1);
|
|
250
251
|
resetManageActionSelection(nextStorage, nextIndex);
|
|
251
252
|
nextStorage.pinnedAccountIndex = reconcilePinnedAccountIndex(pinnedAccount, nextStorage.accounts);
|
|
253
|
+
// The splice shifts every index above the removed slot, and session
|
|
254
|
+
// affinity remembers accounts BY INDEX. Without a generation bump a
|
|
255
|
+
// live proxy keeps routing in-flight sessions to the account that
|
|
256
|
+
// slid into the old slot. Reconciling the pin is not enough — the
|
|
257
|
+
// affinity map is separate state. See issue #474.
|
|
258
|
+
bumpStorageAffinityGeneration(nextStorage);
|
|
252
259
|
await persist(nextStorage);
|
|
253
260
|
replaceManageActionStorage(storage, nextStorage);
|
|
254
261
|
deleted = true;
|
|
@@ -306,9 +313,38 @@ export async function handleManageAction(storage, menuResult) {
|
|
|
306
313
|
console.error(`Refresh failed: ${tokenResult.message ?? tokenResult.reason ?? "unknown error"}`);
|
|
307
314
|
return;
|
|
308
315
|
}
|
|
309
|
-
const resolved = resolveAccountSelection(tokenResult);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
316
|
+
const resolved = resolveAccountSelection(tokenResult, undefined, existing.accountId);
|
|
317
|
+
let persistResult;
|
|
318
|
+
try {
|
|
319
|
+
persistResult = await persistAccountPool([resolved], false, {
|
|
320
|
+
preserveSelection: true,
|
|
321
|
+
expectedAccount: existing,
|
|
322
|
+
expectedAccountIndex: idx,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// A mismatched sign-in (the user picked a different ChatGPT account in
|
|
327
|
+
// the browser) is a refusal, not a crash. Nothing between here and the
|
|
328
|
+
// process entrypoint catches this, so an uncaught throw would take the
|
|
329
|
+
// whole interactive dashboard down with a raw stack trace.
|
|
330
|
+
if (error instanceof CodexValidationError) {
|
|
331
|
+
console.error(`Refresh failed: ${error.message}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
throw error;
|
|
335
|
+
}
|
|
336
|
+
// Refreshing the account Codex is currently using must publish its new
|
|
337
|
+
// tokens; refreshing any other row must leave the selection alone.
|
|
338
|
+
if (!persistResult || persistResult.isActiveAccount) {
|
|
339
|
+
await syncSelectionToCodex(resolved);
|
|
340
|
+
}
|
|
341
|
+
// Report the row that was actually written. The pool can shift between the
|
|
342
|
+
// dashboard render and this transaction, so the menu index may name a
|
|
343
|
+
// different account by now.
|
|
344
|
+
const refreshedIndex = persistResult?.accountIndex ?? idx;
|
|
345
|
+
console.log(`Refreshed account ${refreshedIndex + 1}.`);
|
|
346
|
+
if (persistResult && !persistResult.accountEnabled) {
|
|
347
|
+
console.log(`Account ${refreshedIndex + 1} is still disabled and stays out of rotation. Toggle it from this menu to bring it back.`);
|
|
348
|
+
}
|
|
313
349
|
}
|
|
314
350
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type Workspace } from "../accounts.js";
|
|
2
|
+
import { type AccountMetadataV3 } from "../storage.js";
|
|
2
3
|
import type { AccountIdSource, TokenResult } from "../types.js";
|
|
3
4
|
import { type AccountPoolWriteOutcome } from "./account-pool-write.js";
|
|
4
5
|
/**
|
|
@@ -35,7 +36,7 @@ export declare function isAbortError(error: unknown): boolean;
|
|
|
35
36
|
*
|
|
36
37
|
* @internal
|
|
37
38
|
*/
|
|
38
|
-
export declare function resolveAccountSelection(tokens: TokenSuccess, orgOverride?: string): TokenSuccessWithAccount;
|
|
39
|
+
export declare function resolveAccountSelection(tokens: TokenSuccess, orgOverride?: string, targetAccountId?: string): TokenSuccessWithAccount;
|
|
39
40
|
/** @internal */
|
|
40
41
|
export type OAuthSignInMode = "browser" | "manual" | "device" | "restore-backup" | "cancel";
|
|
41
42
|
/** @internal */
|
|
@@ -49,7 +50,42 @@ export declare function runSignInFlow(forceNewLogin: boolean, signInMode: Extrac
|
|
|
49
50
|
/** @internal */
|
|
50
51
|
export type PersistAccountPoolOutcome = AccountPoolWriteOutcome;
|
|
51
52
|
/** @internal */
|
|
52
|
-
export
|
|
53
|
+
export interface PersistAccountPoolResult {
|
|
54
|
+
/** How the pool absorbed this login (appended, refreshed, or rebound). */
|
|
55
|
+
outcome: PersistAccountPoolOutcome;
|
|
56
|
+
/** Index of the row the login actually wrote. */
|
|
57
|
+
accountIndex: number;
|
|
58
|
+
/** Global selection index after the write. */
|
|
59
|
+
activeIndex: number;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the written row is the one plain `codex` will use. Callers gate the
|
|
62
|
+
* ~/.codex/auth.json sync on this: refreshing the active account must publish
|
|
63
|
+
* its new tokens, and refreshing any other row must not steal the selection.
|
|
64
|
+
*/
|
|
65
|
+
isActiveAccount: boolean;
|
|
66
|
+
/** Whether the written row is enabled for rotation after the write. */
|
|
67
|
+
accountEnabled: boolean;
|
|
68
|
+
}
|
|
69
|
+
/** @internal */
|
|
70
|
+
export type ExpectedLoginAccount = Pick<AccountMetadataV3, "recordId" | "accountId" | "email" | "refreshToken">;
|
|
71
|
+
/** @internal */
|
|
72
|
+
export interface PersistAccountPoolOptions {
|
|
73
|
+
/** Keep every global/model-family selection on its pre-login identity. */
|
|
74
|
+
preserveSelection?: boolean;
|
|
75
|
+
/** Refuse the write unless the OAuth result is this saved account. */
|
|
76
|
+
expectedAccount?: ExpectedLoginAccount;
|
|
77
|
+
/**
|
|
78
|
+
* Position {@link expectedAccount} occupied when the caller resolved it.
|
|
79
|
+
* Treated as a hint that is verified against the row actually sitting there,
|
|
80
|
+
* never trusted: the pool can change between the caller's read and this
|
|
81
|
+
* transaction. It exists so an unambiguous positional request such as
|
|
82
|
+
* `login --account 2` stays deterministic when several saved rows share an
|
|
83
|
+
* identity, which identity matching alone cannot tell apart.
|
|
84
|
+
*/
|
|
85
|
+
expectedAccountIndex?: number;
|
|
86
|
+
}
|
|
87
|
+
/** @internal */
|
|
88
|
+
export declare function persistAccountPool(results: TokenSuccessWithAccount[], replaceAll: boolean, options?: PersistAccountPoolOptions): Promise<PersistAccountPoolResult | null>;
|
|
53
89
|
/** @internal */
|
|
54
90
|
export declare function syncSelectionToCodex(tokens: TokenSuccessWithAccount): Promise<void>;
|
|
55
91
|
//# sourceMappingURL=login-oauth.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login-oauth.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-oauth.ts"],"names":[],"mappings":"AAEA,OAAO,EAON,KAAK,SAAS,EACd,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"login-oauth.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/login-oauth.ts"],"names":[],"mappings":"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"}
|