codex-multi-auth 2.2.2 → 2.3.0-beta.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.
Files changed (34) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/README.md +1 -0
  3. package/dist/lib/accounts.d.ts +26 -0
  4. package/dist/lib/accounts.d.ts.map +1 -1
  5. package/dist/lib/accounts.js +71 -0
  6. package/dist/lib/accounts.js.map +1 -1
  7. package/dist/lib/codex-manager/account-pool-write.d.ts +95 -0
  8. package/dist/lib/codex-manager/account-pool-write.d.ts.map +1 -0
  9. package/dist/lib/codex-manager/account-pool-write.js +166 -0
  10. package/dist/lib/codex-manager/account-pool-write.js.map +1 -0
  11. package/dist/lib/codex-manager/manual-callback.d.ts +33 -0
  12. package/dist/lib/codex-manager/manual-callback.d.ts.map +1 -0
  13. package/dist/lib/codex-manager/manual-callback.js +31 -0
  14. package/dist/lib/codex-manager/manual-callback.js.map +1 -0
  15. package/dist/lib/codex-manager.d.ts.map +1 -1
  16. package/dist/lib/codex-manager.js +77 -85
  17. package/dist/lib/codex-manager.js.map +1 -1
  18. package/dist/lib/config.d.ts +18 -0
  19. package/dist/lib/config.d.ts.map +1 -1
  20. package/dist/lib/config.js +27 -0
  21. package/dist/lib/config.js.map +1 -1
  22. package/dist/lib/runtime-rotation-proxy.d.ts +1 -0
  23. package/dist/lib/runtime-rotation-proxy.d.ts.map +1 -1
  24. package/dist/lib/runtime-rotation-proxy.js +81 -6
  25. package/dist/lib/runtime-rotation-proxy.js.map +1 -1
  26. package/dist/lib/schemas.d.ts +4 -0
  27. package/dist/lib/schemas.d.ts.map +1 -1
  28. package/dist/lib/schemas.js +1 -0
  29. package/dist/lib/schemas.js.map +1 -1
  30. package/dist/lib/ui/copy.d.ts +2 -0
  31. package/dist/lib/ui/copy.d.ts.map +1 -1
  32. package/dist/lib/ui/copy.js +2 -0
  33. package/dist/lib/ui/copy.js.map +1 -1
  34. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Choose the workspace a freshly-saved account should start on: prefer the
3
+ * workspace whose id matches the resolved accountId, otherwise the first
4
+ * enabled workspace, otherwise index 0.
5
+ */
6
+ export function pickInitialWorkspaceIndex(workspaces, accountId) {
7
+ if (accountId) {
8
+ const matching = workspaces.findIndex((workspace) => workspace.id === accountId);
9
+ if (matching >= 0)
10
+ return matching;
11
+ }
12
+ const firstEnabled = workspaces.findIndex((workspace) => workspace.enabled !== false);
13
+ return firstEnabled >= 0 ? firstEnabled : 0;
14
+ }
15
+ /**
16
+ * Merge incoming workspaces with the saved set, preserving any per-workspace
17
+ * enabled/disabled state the user already had. Workspaces the login did not
18
+ * surface are dropped only when the login returned a workspace list at all;
19
+ * otherwise the existing list is kept untouched.
20
+ */
21
+ export function mergeAccountWorkspaces(existing, incoming) {
22
+ if (!incoming)
23
+ return existing.workspaces;
24
+ return incoming.map((newWs) => {
25
+ const existingWs = existing.workspaces?.find((w) => w.id === newWs.id);
26
+ return existingWs
27
+ ? {
28
+ ...newWs,
29
+ enabled: existingWs.enabled,
30
+ disabledAt: existingWs.disabledAt,
31
+ }
32
+ : newWs;
33
+ });
34
+ }
35
+ /**
36
+ * Resolve the workspace index a refreshed account should point at after a
37
+ * merge: keep the user on their current workspace if it survived, else the
38
+ * default workspace, else the first enabled one, else index 0.
39
+ */
40
+ export function resolveCurrentWorkspaceIndex(existing, mergedWorkspaces) {
41
+ if (!mergedWorkspaces || mergedWorkspaces.length === 0) {
42
+ return existing.currentWorkspaceIndex;
43
+ }
44
+ const currentWorkspaceId = existing.workspaces?.[typeof existing.currentWorkspaceIndex === "number"
45
+ ? existing.currentWorkspaceIndex
46
+ : 0]?.id;
47
+ if (currentWorkspaceId) {
48
+ const matching = mergedWorkspaces.findIndex((workspace) => workspace.id === currentWorkspaceId);
49
+ if (matching >= 0)
50
+ return matching;
51
+ }
52
+ const defaultIndex = mergedWorkspaces.findIndex((workspace) => workspace.isDefault === true);
53
+ if (defaultIndex >= 0)
54
+ return defaultIndex;
55
+ const firstEnabled = mergedWorkspaces.findIndex((workspace) => workspace.enabled !== false);
56
+ return firstEnabled >= 0 ? firstEnabled : 0;
57
+ }
58
+ /**
59
+ * Build the saved record for a login result that did not match any existing
60
+ * entry, seeding its workspace tracking from the token-derived workspaces.
61
+ */
62
+ export function buildInsertedAccount(write) {
63
+ const initialWorkspaceIndex = write.workspaces && write.workspaces.length > 0
64
+ ? pickInitialWorkspaceIndex(write.workspaces, write.accountId)
65
+ : undefined;
66
+ return {
67
+ outcome: "inserted",
68
+ account: {
69
+ accountId: write.accountId,
70
+ accountIdSource: write.accountIdSource,
71
+ accountLabel: write.accountLabel,
72
+ email: write.email,
73
+ refreshToken: write.refreshToken,
74
+ accessToken: write.accessToken,
75
+ expiresAt: write.expiresAt,
76
+ enabled: true,
77
+ addedAt: write.now,
78
+ lastUsed: write.now,
79
+ workspaces: write.workspaces,
80
+ currentWorkspaceIndex: initialWorkspaceIndex,
81
+ },
82
+ };
83
+ }
84
+ /**
85
+ * Fold a login result onto an existing saved entry: refresh tokens/identity,
86
+ * merge workspace tracking, and classify whether this was a plain `updated`
87
+ * refresh or a `rebound` (a previously-unknown workspace appeared).
88
+ */
89
+ export function buildUpdatedAccount(existing, write) {
90
+ const nextEmail = write.email ?? existing.email;
91
+ const nextAccountId = write.accountId ?? existing.accountId;
92
+ const nextAccountIdSource = write.accountId
93
+ ? (write.accountIdSource ?? existing.accountIdSource)
94
+ : existing.accountIdSource;
95
+ const previousWorkspaceIds = new Set((existing.workspaces ?? []).map((workspace) => workspace.id));
96
+ const mergedWorkspaces = mergeAccountWorkspaces(existing, write.workspaces);
97
+ // Only a genuine *rebind* counts as "rebound": the account already tracked
98
+ // workspaces and the login surfaced one it had never seen. First-time
99
+ // enrichment of a pre-#491 row (no prior workspaces) is a plain `updated`,
100
+ // so the user is not told "Rebound workspace" on their first workspace-aware
101
+ // re-login (#512 follow-up).
102
+ const introducedNewWorkspace = previousWorkspaceIds.size > 0 &&
103
+ Boolean(write.workspaces?.some((workspace) => !previousWorkspaceIds.has(workspace.id)));
104
+ const nextCurrentWorkspaceIndex = resolveCurrentWorkspaceIndex(existing, mergedWorkspaces);
105
+ return {
106
+ outcome: introducedNewWorkspace ? "rebound" : "updated",
107
+ account: {
108
+ ...existing,
109
+ accountId: nextAccountId,
110
+ accountIdSource: nextAccountIdSource,
111
+ accountLabel: write.accountLabel ?? existing.accountLabel,
112
+ email: nextEmail,
113
+ refreshToken: write.refreshToken,
114
+ accessToken: write.accessToken,
115
+ expiresAt: write.expiresAt,
116
+ enabled: true,
117
+ lastUsed: write.now,
118
+ workspaces: mergedWorkspaces,
119
+ currentWorkspaceIndex: nextCurrentWorkspaceIndex,
120
+ },
121
+ };
122
+ }
123
+ /**
124
+ * Fold a batch of resolved login writes into the saved account list, returning
125
+ * the next account array, the selected index, and the outcome of the last
126
+ * write. This is the pure core of `persistAccountPool`: it owns the dedup →
127
+ * insert/update → workspace-tracking → active-index decisions that issue #512
128
+ * depends on, with the matching strategy injected so the production path and
129
+ * tests share the exact same behaviour.
130
+ */
131
+ export function applyAccountPoolResults(params) {
132
+ const accounts = [...params.existing];
133
+ let selectedAccountIndex = null;
134
+ let selectedOutcome = null;
135
+ for (const write of params.writes) {
136
+ const existingIndex = params.findMatchingAccountIndex(accounts, {
137
+ accountId: write.accountId,
138
+ email: write.email,
139
+ refreshToken: write.refreshToken,
140
+ }, { allowUniqueAccountIdFallbackWithoutEmail: true });
141
+ if (existingIndex === undefined) {
142
+ const { account, outcome } = buildInsertedAccount(write);
143
+ selectedAccountIndex = accounts.length;
144
+ accounts.push(account);
145
+ selectedOutcome = outcome;
146
+ continue;
147
+ }
148
+ const existing = accounts[existingIndex];
149
+ if (!existing)
150
+ continue;
151
+ const { account, outcome } = buildUpdatedAccount(existing, write);
152
+ accounts[existingIndex] = account;
153
+ selectedAccountIndex = existingIndex;
154
+ selectedOutcome = outcome;
155
+ }
156
+ const fallbackActiveIndex = accounts.length === 0
157
+ ? 0
158
+ : Math.max(0, Math.min(params.priorActiveIndex ?? 0, accounts.length - 1));
159
+ const activeIndex = accounts.length === 0
160
+ ? 0
161
+ : selectedAccountIndex === null
162
+ ? fallbackActiveIndex
163
+ : Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
164
+ return { accounts, activeIndex, outcome: selectedOutcome };
165
+ }
166
+ //# sourceMappingURL=account-pool-write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account-pool-write.js","sourceRoot":"","sources":["../../../lib/codex-manager/account-pool-write.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Classification of a manual OAuth callback entry.
3
+ *
4
+ * Splitting `invalid` and `state-mismatch` out from `cancelled` is the fix for
5
+ * the issue #512 follow-up: `login --manual` previously collapsed a malformed
6
+ * or wrong-attempt callback URL into the same `null` it used for a genuine user
7
+ * abort, so the CLI printed `Cancelled.` and hid the real validation error.
8
+ *
9
+ * - `code`: a valid authorization code + matching state were extracted.
10
+ * - `cancelled`: the user aborted (empty input, an Esc character, or one of the
11
+ * cancel keywords `q`/`quit`/`cancel`/`back`).
12
+ * - `invalid`: the pasted value was missing the code and/or state parameter.
13
+ * - `state-mismatch`: the state did not match the one minted for this attempt.
14
+ */
15
+ export type ManualCallbackClassification = {
16
+ type: "code";
17
+ code: string;
18
+ } | {
19
+ type: "cancelled";
20
+ } | {
21
+ type: "invalid";
22
+ } | {
23
+ type: "state-mismatch";
24
+ };
25
+ /**
26
+ * Classify raw manual-callback input against the expected OAuth state. Pure and
27
+ * I/O-free so the CLI prompt and unit tests share identical behaviour.
28
+ *
29
+ * `answer` is `null` when the input stream closed before any line was read,
30
+ * which is treated as a cancellation.
31
+ */
32
+ export declare function classifyManualCallbackInput(answer: string | null, expectedState: string): ManualCallbackClassification;
33
+ //# sourceMappingURL=manual-callback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manual-callback.d.ts","sourceRoot":"","sources":["../../../lib/codex-manager/manual-callback.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,4BAA4B,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC9B;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,GACnB;IAAE,IAAI,EAAE,gBAAgB,CAAA;CAAE,CAAC;AAK9B;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CAC1C,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,aAAa,EAAE,MAAM,GACnB,4BAA4B,CAmB9B"}
@@ -0,0 +1,31 @@
1
+ import { parseAuthorizationInput } from "../auth/auth.js";
2
+ const CANCEL_KEYWORDS = new Set(["", "q", "quit", "cancel", "back"]);
3
+ const ESC_CHARACTER = "\u001b";
4
+ /**
5
+ * Classify raw manual-callback input against the expected OAuth state. Pure and
6
+ * I/O-free so the CLI prompt and unit tests share identical behaviour.
7
+ *
8
+ * `answer` is `null` when the input stream closed before any line was read,
9
+ * which is treated as a cancellation.
10
+ */
11
+ export function classifyManualCallbackInput(answer, expectedState) {
12
+ if (answer === null) {
13
+ return { type: "cancelled" };
14
+ }
15
+ if (answer.includes(ESC_CHARACTER)) {
16
+ return { type: "cancelled" };
17
+ }
18
+ const normalized = answer.trim().toLowerCase();
19
+ if (CANCEL_KEYWORDS.has(normalized)) {
20
+ return { type: "cancelled" };
21
+ }
22
+ const parsed = parseAuthorizationInput(answer);
23
+ if (!parsed.code || !parsed.state) {
24
+ return { type: "invalid" };
25
+ }
26
+ if (parsed.state !== expectedState) {
27
+ return { type: "state-mismatch" };
28
+ }
29
+ return { type: "code", code: parsed.code };
30
+ }
31
+ //# sourceMappingURL=manual-callback.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manual-callback.js","sourceRoot":"","sources":["../../../lib/codex-manager/manual-callback.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAsB1D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;AACrE,MAAM,aAAa,GAAG,QAAQ,CAAC;AAE/B;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B,CAC1C,MAAqB,EACrB,aAAqB;IAErB,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;QACrB,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC9B,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;QACpC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC9B,CAAC;IACD,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAC/C,IAAI,eAAe,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QACrC,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAC9B,CAAC;IACD,MAAM,MAAM,GAAG,uBAAuB,CAAC,MAAM,CAAC,CAAC;IAC/C,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;QACnC,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAC5B,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,KAAK,aAAa,EAAE,CAAC;QACpC,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,CAAC;IACnC,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AAC5C,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"codex-manager.d.ts","sourceRoot":"","sources":["../../lib/codex-manager.ts"],"names":[],"mappings":"AA8MA,KAAK,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AA8MxE,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,MAAM,EACd,YAAY,GAAE,UAAoB,GAChC,MAAM,CA4CR;AA0gCD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ3D;AA+3DD,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,OAAO,CAAC,CAuGrE;AAuCD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA+O7E"}
1
+ {"version":3,"file":"codex-manager.d.ts","sourceRoot":"","sources":["../../lib/codex-manager.ts"],"names":[],"mappings":"AAwNA,KAAK,UAAU,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,GAAG,OAAO,CAAC;AA8MxE,wBAAgB,sBAAsB,CACrC,MAAM,EAAE,MAAM,EACd,YAAY,GAAE,UAAoB,GAChC,MAAM,CA4CR;AAuhCD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAQ3D;AAg3DD,wBAAsB,4BAA4B,IAAI,OAAO,CAAC,OAAO,CAAC,CAuGrE;AAuCD,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA+O7E"}
@@ -1,7 +1,7 @@
1
1
  import { stdin as input, stdout as output } from "node:process";
2
2
  import { createInterface } from "node:readline/promises";
3
3
  import { extractAccountEmail, extractAccountId, formatAccountLabel, formatWaitTime, getAccountIdCandidates, resolveRequestAccountId, sanitizeEmail, selectBestAccountCandidate, shouldUpdateAccountIdFromToken, } from "./accounts.js";
4
- import { createAuthorizationFlow, exchangeAuthorizationCode, parseAuthorizationInput, redactOAuthUrlForLog, REDIRECT_URI, } from "./auth/auth.js";
4
+ import { createAuthorizationFlow, exchangeAuthorizationCode, redactOAuthUrlForLog, REDIRECT_URI, } from "./auth/auth.js";
5
5
  import { runDeviceAuthFlow } from "./auth/device-auth.js";
6
6
  import { resolveOrgOverride } from "./auth/org-override.js";
7
7
  import { copyTextToClipboard, isBrowserLaunchSuppressed, openBrowserUrl, } from "./auth/browser.js";
@@ -12,6 +12,8 @@ import { setCodexCliActiveSelection } from "./codex-cli/writer.js";
12
12
  import { runBestCommand, } from "./codex-manager/commands/best.js";
13
13
  import { runAccountCommand } from "./codex-manager/commands/account.js";
14
14
  import { ACCOUNT_MANAGER_COMMANDS } from "./codex-manager/account-manager-commands.js";
15
+ import { applyAccountPoolResults, } from "./codex-manager/account-pool-write.js";
16
+ import { classifyManualCallbackInput, } from "./codex-manager/manual-callback.js";
15
17
  import { runBudgetCommand } from "./codex-manager/commands/budget.js";
16
18
  import { runBridgeCommand } from "./codex-manager/commands/bridge.js";
17
19
  import { runCheckCommand } from "./codex-manager/commands/check.js";
@@ -856,15 +858,33 @@ async function syncCodexCliActiveSelectionIfDrifted(storage) {
856
858
  * process.env for the duration of a login, which raced on concurrent re-entry.
857
859
  */
858
860
  function resolveAccountSelection(tokens, orgOverride) {
861
+ const candidates = getAccountIdCandidates(tokens.access, tokens.idToken);
862
+ // Surface every workspace/organization exposed by the token so the saved
863
+ // account can track them (issue #491/#512). Without this, same-email
864
+ // multi-workspace logins persisted rows with `workspaces: null` and
865
+ // `workspace <account>` was unusable. Built before the `--org` override
866
+ // branch so the explicit-binding flow persists workspaces too (#512).
867
+ const workspaces = candidates.length > 0
868
+ ? candidates.map((candidate) => ({
869
+ id: candidate.accountId,
870
+ name: candidate.label,
871
+ enabled: true,
872
+ isDefault: candidate.isDefault,
873
+ }))
874
+ : undefined;
859
875
  const override = resolveOrgOverride(orgOverride);
860
876
  if (override) {
877
+ // Prefer the token candidate's human label for the chosen org so the
878
+ // saved row is identifiable, falling back to a bare manual binding.
879
+ const matched = candidates.find((candidate) => candidate.accountId === override);
861
880
  return {
862
881
  ...tokens,
863
882
  accountIdOverride: override,
864
883
  accountIdSource: "manual",
884
+ accountLabel: matched?.label,
885
+ workspaces,
865
886
  };
866
887
  }
867
- const candidates = getAccountIdCandidates(tokens.access, tokens.idToken);
868
888
  if (candidates.length === 0) {
869
889
  return tokens;
870
890
  }
@@ -876,6 +896,7 @@ function resolveAccountSelection(tokens, orgOverride) {
876
896
  accountIdOverride: candidate.accountId,
877
897
  accountIdSource: candidate.source,
878
898
  accountLabel: candidate.label,
899
+ workspaces,
879
900
  };
880
901
  }
881
902
  }
@@ -888,6 +909,7 @@ function resolveAccountSelection(tokens, orgOverride) {
888
909
  accountIdOverride: best.accountId,
889
910
  accountIdSource: best.source ?? "token",
890
911
  accountLabel: best.label,
912
+ workspaces,
891
913
  };
892
914
  }
893
915
  function resolveStoredAccountIdentity(storedAccountId, storedAccountIdSource, tokenAccountId) {
@@ -922,7 +944,7 @@ function applyTokenAccountIdentity(account, tokenAccountId) {
922
944
  async function promptManualCallback(state, options = {}) {
923
945
  const useInteractivePrompt = input.isTTY && output.isTTY;
924
946
  if (!useInteractivePrompt && !options.allowNonTty) {
925
- return null;
947
+ return { type: "cancelled" };
926
948
  }
927
949
  const rl = createInterface({ input, output });
928
950
  try {
@@ -974,30 +996,11 @@ async function promptManualCallback(state, options = {}) {
974
996
  input.once("end", handleInputClosed);
975
997
  input.once("close", handleInputClosed);
976
998
  });
977
- if (answer === null) {
978
- return null;
979
- }
980
- if (answer.includes("\u001b")) {
981
- return null;
982
- }
983
- const normalized = answer.trim().toLowerCase();
984
- if (normalized.length === 0 ||
985
- normalized === "q" ||
986
- normalized === "quit" ||
987
- normalized === "cancel" ||
988
- normalized === "back") {
989
- return null;
990
- }
991
- const parsed = parseAuthorizationInput(answer);
992
- if (!parsed.code || !parsed.state)
993
- return null;
994
- if (parsed.state !== state)
995
- return null;
996
- return parsed.code;
999
+ return classifyManualCallbackInput(answer, state);
997
1000
  }
998
1001
  catch (error) {
999
1002
  if (isAbortError(error) || isReadlineClosedError(error)) {
1000
- return null;
1003
+ return { type: "cancelled" };
1001
1004
  }
1002
1005
  throw error;
1003
1006
  }
@@ -1455,9 +1458,28 @@ async function runOAuthFlow(forceNewLogin, signInMode) {
1455
1458
  : signInMode === "manual"
1456
1459
  ? UI_COPY.oauth.callbackBypassed
1457
1460
  : UI_COPY.oauth.callbackUnavailable, "warning"));
1458
- code = await promptManualCallback(state, {
1461
+ const manualResult = await promptManualCallback(state, {
1459
1462
  allowNonTty: signInMode === "manual",
1460
1463
  });
1464
+ // A parse/state failure must surface its own validation error instead
1465
+ // of being reported as `Cancelled.` like a genuine user abort
1466
+ // (issue #512 follow-up). Only an actual cancellation falls through to
1467
+ // the cancelled path below.
1468
+ if (manualResult.type === "invalid") {
1469
+ return {
1470
+ type: "failed",
1471
+ reason: "invalid_response",
1472
+ message: UI_COPY.oauth.callbackInvalid,
1473
+ };
1474
+ }
1475
+ if (manualResult.type === "state-mismatch") {
1476
+ return {
1477
+ type: "failed",
1478
+ reason: "invalid_response",
1479
+ message: UI_COPY.oauth.callbackStateMismatch,
1480
+ };
1481
+ }
1482
+ code = manualResult.type === "code" ? manualResult.code : null;
1461
1483
  }
1462
1484
  }
1463
1485
  finally {
@@ -1490,85 +1512,47 @@ async function runSignInFlow(forceNewLogin, signInMode, options = {}) {
1490
1512
  }
1491
1513
  async function persistAccountPool(results, replaceAll) {
1492
1514
  if (results.length === 0)
1493
- return;
1494
- await withAccountStorageTransaction(async (loadedStorage, persist) => {
1515
+ return null;
1516
+ return await withAccountStorageTransaction(async (loadedStorage, persist) => {
1495
1517
  const stored = replaceAll ? null : loadedStorage;
1496
1518
  const now = Date.now();
1497
- const accounts = stored?.accounts ? [...stored.accounts] : [];
1498
- let selectedAccountIndex = null;
1499
- for (const result of results) {
1519
+ const existing = stored?.accounts ? [...stored.accounts] : [];
1520
+ const writes = results.map((result) => {
1500
1521
  const tokenAccountId = extractAccountId(result.access);
1501
1522
  const accountId = resolveRequestAccountId(result.accountIdOverride, result.accountIdSource, tokenAccountId);
1502
1523
  const accountIdSource = accountId
1503
1524
  ? (result.accountIdSource ??
1504
1525
  (result.accountIdOverride ? "manual" : "token"))
1505
1526
  : undefined;
1506
- const accountLabel = result.accountLabel;
1507
- const accountEmail = sanitizeEmail(extractAccountEmail(result.access, result.idToken));
1508
- const existingIndex = findMatchingAccountIndex(accounts, {
1527
+ return {
1509
1528
  accountId,
1510
- email: accountEmail,
1511
- refreshToken: result.refresh,
1512
- }, {
1513
- allowUniqueAccountIdFallbackWithoutEmail: true,
1514
- });
1515
- if (existingIndex === undefined) {
1516
- const newIndex = accounts.length;
1517
- accounts.push({
1518
- accountId,
1519
- accountIdSource,
1520
- accountLabel,
1521
- email: accountEmail,
1522
- refreshToken: result.refresh,
1523
- accessToken: result.access,
1524
- expiresAt: result.expires,
1525
- enabled: true,
1526
- addedAt: now,
1527
- lastUsed: now,
1528
- });
1529
- selectedAccountIndex = newIndex;
1530
- continue;
1531
- }
1532
- const existing = accounts[existingIndex];
1533
- if (!existing)
1534
- continue;
1535
- const nextEmail = accountEmail ?? existing.email;
1536
- const nextAccountId = accountId ?? existing.accountId;
1537
- const nextAccountIdSource = accountId
1538
- ? (accountIdSource ?? existing.accountIdSource)
1539
- : existing.accountIdSource;
1540
- accounts[existingIndex] = {
1541
- ...existing,
1542
- accountId: nextAccountId,
1543
- accountIdSource: nextAccountIdSource,
1544
- accountLabel: accountLabel ?? existing.accountLabel,
1545
- email: nextEmail,
1529
+ accountIdSource,
1530
+ accountLabel: result.accountLabel,
1531
+ email: sanitizeEmail(extractAccountEmail(result.access, result.idToken)),
1546
1532
  refreshToken: result.refresh,
1547
1533
  accessToken: result.access,
1548
1534
  expiresAt: result.expires,
1549
- enabled: true,
1550
- lastUsed: now,
1535
+ workspaces: result.workspaces,
1536
+ now,
1551
1537
  };
1552
- selectedAccountIndex = existingIndex;
1553
- }
1554
- const fallbackActiveIndex = accounts.length === 0
1555
- ? 0
1556
- : Math.max(0, Math.min(stored?.activeIndex ?? 0, accounts.length - 1));
1557
- const nextActiveIndex = accounts.length === 0
1558
- ? 0
1559
- : selectedAccountIndex === null
1560
- ? fallbackActiveIndex
1561
- : Math.max(0, Math.min(selectedAccountIndex, accounts.length - 1));
1538
+ });
1539
+ const { accounts, activeIndex, outcome } = applyAccountPoolResults({
1540
+ existing,
1541
+ writes,
1542
+ priorActiveIndex: stored?.activeIndex,
1543
+ findMatchingAccountIndex,
1544
+ });
1562
1545
  const activeIndexByFamily = {};
1563
1546
  for (const family of MODEL_FAMILIES) {
1564
- activeIndexByFamily[family] = nextActiveIndex;
1547
+ activeIndexByFamily[family] = activeIndex;
1565
1548
  }
1566
1549
  await persist({
1567
1550
  version: 3,
1568
1551
  accounts,
1569
- activeIndex: nextActiveIndex,
1552
+ activeIndex,
1570
1553
  activeIndexByFamily,
1571
1554
  });
1555
+ return outcome;
1572
1556
  });
1573
1557
  }
1574
1558
  async function syncSelectionToCodex(tokens) {
@@ -2396,14 +2380,22 @@ async function runAuthLoginFlow(loginOptions) {
2396
2380
  return 1;
2397
2381
  }
2398
2382
  const resolved = resolveAccountSelection(tokenResult, loginOptions.org);
2399
- await persistAccountPool([resolved], false);
2383
+ const persistOutcome = await persistAccountPool([resolved], false);
2400
2384
  await syncSelectionToCodex(resolved);
2401
2385
  const latestStorage = await loadAccounts();
2402
2386
  const count = latestStorage?.accounts.length ?? 1;
2403
2387
  existingCount = count;
2404
2388
  namedBackups = [];
2405
2389
  onboardingBackupDiscoveryWarning = null;
2406
- console.log(`Added account. Total: ${count}`);
2390
+ // Only claim a new saved slot when one was actually appended. A
2391
+ // same-email login that maps onto an existing entry updates or
2392
+ // rebinds it instead of growing the pool (issue #512).
2393
+ const outcomeMessage = persistOutcome === "rebound"
2394
+ ? `Rebound workspace for existing account. Total: ${count}`
2395
+ : persistOutcome === "updated"
2396
+ ? `Updated existing account. Total: ${count}`
2397
+ : `Added account. Total: ${count}`;
2398
+ console.log(outcomeMessage);
2407
2399
  console.log("Next steps:");
2408
2400
  console.log(" codex-multi-auth status Check that the wrapper is active.");
2409
2401
  console.log(" codex-multi-auth check Confirm your saved accounts look healthy.");