Skip to content

Commit 1ad09d4

Browse files
committed
merge: sync workspace routing and fix proxy cleanup race
2 parents efbb34e + a8aec23 commit 1ad09d4

13 files changed

Lines changed: 380 additions & 115 deletions

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@ codex auth fix --dry-run
109109
codex auth doctor --fix
110110
```
111111

112+
If the shell should not launch a browser, use the manual callback flow:
113+
114+
```bash
115+
codex auth login --manual
116+
CODEX_AUTH_NO_BROWSER=1 codex auth login
117+
```
118+
119+
In non-TTY/manual shells, provide the full redirect URL on stdin instead of waiting for a browser callback:
120+
121+
```bash
122+
echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual
123+
```
124+
125+
No new npm scripts or storage migration steps are required for this login-flow update.
126+
112127
---
113128

114129
## Command Toolkit
@@ -234,6 +249,7 @@ codex auth login
234249
- `codex auth` unrecognized: run `where codex`, then follow `docs/troubleshooting.md` for routing fallback commands
235250
- Switch succeeds but wrong account appears active: run `codex auth switch <index>`, then restart session
236251
- OAuth callback on port `1455` fails: free the port and re-run `codex auth login`
252+
- Browser launch is blocked or you are in a headless shell: re-run `codex auth login --manual` or set `CODEX_AUTH_NO_BROWSER=1`
237253
- `missing field id_token` / `token_expired` / `refresh_token_reused`: re-login affected account
238254

239255
</details>

docs/features.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ User-facing capability map for `codex-multi-auth`.
5454
| Quick switch and search hotkeys | Faster navigation in the dashboard |
5555
| Account action hotkeys | Per-account set, refresh, toggle, and delete shortcuts |
5656
| In-dashboard settings hub | Runtime and display tuning without editing files directly |
57-
| Browser-first OAuth with manual fallback | Works in normal and constrained terminal environments |
57+
| Browser-first OAuth with manual fallback | `codex auth login` stays browser-first, while `--manual`, `--no-browser`, and `CODEX_AUTH_NO_BROWSER=1` keep login usable in browser-restricted shells |
58+
59+
Manual/non-TTY login accepts the full callback URL on stdin, so automation and host-managed shells can complete auth without relying on a local browser handoff.
5860

5961
---
6062

docs/getting-started.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ codex auth list
5656
codex auth check
5757
```
5858

59+
If browser launch is blocked or you want to handle the callback manually:
60+
61+
```bash
62+
codex auth login --manual
63+
CODEX_AUTH_NO_BROWSER=1 codex auth login
64+
```
65+
66+
In non-TTY/manual shells, provide the full redirect URL on stdin:
67+
68+
```bash
69+
echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual
70+
```
71+
72+
`codex auth login` remains browser-first by default. No new npm scripts or storage migration steps are required for this auth-flow update.
73+
5974
---
6075

6176
## Add More Accounts
@@ -111,6 +126,7 @@ If the OAuth callback on port `1455` fails:
111126

112127
- stop the process using port `1455`
113128
- rerun `codex auth login`
129+
- if browser launch is unavailable, rerun `codex auth login --manual`
114130

115131
If account state looks stale:
116132

docs/reference/commands.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ Compatibility aliases are supported:
5656

5757
---
5858

59+
## Upgrade Notes
60+
61+
- `codex auth login` remains browser-first by default.
62+
- `codex auth login --manual` and `codex auth login --no-browser` force the manual callback flow instead of launching a browser.
63+
- `CODEX_AUTH_NO_BROWSER=1` suppresses browser launch for automation/headless sessions. False-like values such as `0` and `false` do not disable browser launch by themselves.
64+
- In non-TTY/manual shells, pass the full redirect URL on stdin, for example: `echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual`.
65+
- No new npm scripts or storage migration steps were introduced for this auth-flow update.
66+
67+
---
68+
5969
## Compatibility and Non-TTY Behavior
6070

6171
- `codex` remains the primary wrapper entrypoint. It routes `codex auth ...` and the compatibility aliases to the multi-auth runtime, and forwards every other command to the official `@openai/codex` CLI.

docs/upgrade.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,18 @@ codex auth forecast --live --model gpt-5-codex
4949

5050
---
5151

52+
## Login Flow Upgrade Notes
53+
54+
- `codex auth login` remains the default browser-first path.
55+
- `codex auth login --manual` and `codex auth login --no-browser` force manual callback handling for browser-restricted shells.
56+
- `CODEX_AUTH_NO_BROWSER=1` suppresses browser launch for automation/headless sessions. False-like values such as `0` and `false` no longer force manual mode.
57+
- In non-TTY/manual shells, provide the full redirect URL on stdin, for example: `echo "http://127.0.0.1:1455/auth/callback?code=..." | codex auth login --manual`.
58+
- No new npm scripts, storage migrations, or extra upgrade steps were introduced for this auth-flow change.
59+
60+
For the full command/behavior reference, see [reference/commands.md](reference/commands.md).
61+
62+
---
63+
5264
## Configuration Upgrade Notes
5365

5466
During upgrades, runtime config source precedence is:

lib/codex-manager.ts

Lines changed: 83 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,7 +1144,50 @@ async function promptManualCallback(
11441144
console.log("");
11451145
console.log(stylePromptText(UI_COPY.oauth.pastePrompt, "accent"));
11461146
}
1147-
const answer = await rl.question(useInteractivePrompt ? "◆ " : "");
1147+
const answer = useInteractivePrompt
1148+
? await rl.question("◆ ")
1149+
: await new Promise<string | null>((resolve, reject) => {
1150+
if (input.readableEnded || input.destroyed) {
1151+
resolve(null);
1152+
return;
1153+
}
1154+
let settled = false;
1155+
const handleInputClosed = () => {
1156+
if (settled) return;
1157+
settled = true;
1158+
input.off("end", handleInputClosed);
1159+
input.off("close", handleInputClosed);
1160+
resolve(null);
1161+
};
1162+
const finish = (value: string) => {
1163+
if (settled) return;
1164+
settled = true;
1165+
input.off("end", handleInputClosed);
1166+
input.off("close", handleInputClosed);
1167+
resolve(value);
1168+
};
1169+
const fail = (error: unknown) => {
1170+
if (settled) return;
1171+
settled = true;
1172+
input.off("end", handleInputClosed);
1173+
input.off("close", handleInputClosed);
1174+
reject(error);
1175+
};
1176+
rl.question("")
1177+
.then((value) => finish(value))
1178+
.catch((error) => {
1179+
if (isAbortError(error) || isReadlineClosedError(error)) {
1180+
handleInputClosed();
1181+
return;
1182+
}
1183+
fail(error);
1184+
});
1185+
input.once("end", handleInputClosed);
1186+
input.once("close", handleInputClosed);
1187+
});
1188+
if (answer === null) {
1189+
return null;
1190+
}
11481191
if (answer.includes("\u001b")) {
11491192
return null;
11501193
}
@@ -1163,7 +1206,7 @@ async function promptManualCallback(
11631206
if (parsed.state && parsed.state !== state) return null;
11641207
return parsed.code;
11651208
} catch (error) {
1166-
if (isAbortError(error)) {
1209+
if (isAbortError(error) || isReadlineClosedError(error)) {
11671210
return null;
11681211
}
11691212
throw error;
@@ -1172,6 +1215,17 @@ async function promptManualCallback(
11721215
}
11731216
}
11741217

1218+
function isReadlineClosedError(error: unknown): boolean {
1219+
if (!(error instanceof Error)) {
1220+
return false;
1221+
}
1222+
const errorCode =
1223+
typeof error === "object" && error !== null && "code" in error
1224+
? String((error as { code?: unknown }).code)
1225+
: "";
1226+
return errorCode === "ERR_USE_AFTER_CLOSE" || /readline was closed/i.test(error.message);
1227+
}
1228+
11751229
type OAuthSignInMode = "browser" | "manual" | "cancel";
11761230

11771231
async function promptOAuthSignInMode(): Promise<OAuthSignInMode> {
@@ -1471,28 +1525,8 @@ async function runOAuthFlow(
14711525
): Promise<TokenResult> {
14721526
const { pkce, state, url } = await createAuthorizationFlow({ forceNewLogin });
14731527
const preferManualMode = options.manual || isBrowserLaunchSuppressed();
1474-
let oauthServer: Awaited<ReturnType<typeof startLocalOAuthServer>> | null = null;
1475-
try {
1476-
oauthServer = await startLocalOAuthServer({ state });
1477-
} catch (serverError) {
1478-
log.warn(
1479-
"Local OAuth callback server unavailable; falling back to manual callback entry.",
1480-
serverError instanceof Error
1481-
? {
1482-
message: serverError.message,
1483-
stack: serverError.stack,
1484-
code:
1485-
typeof serverError === "object" &&
1486-
serverError !== null &&
1487-
"code" in serverError
1488-
? String(serverError.code)
1489-
: undefined,
1490-
}
1491-
: { error: String(serverError) },
1492-
);
1493-
oauthServer = null;
1494-
}
14951528
let code: string | null = null;
1529+
let oauthServer: Awaited<ReturnType<typeof startLocalOAuthServer>> | null = null;
14961530
try {
14971531
const signInMode = preferManualMode ? "manual" : await promptOAuthSignInMode();
14981532
if (signInMode === "cancel") {
@@ -1503,6 +1537,29 @@ async function runOAuthFlow(
15031537
};
15041538
}
15051539

1540+
if (signInMode === "browser") {
1541+
try {
1542+
oauthServer = await startLocalOAuthServer({ state });
1543+
} catch (serverError) {
1544+
log.warn(
1545+
"Local OAuth callback server unavailable; falling back to manual callback entry.",
1546+
serverError instanceof Error
1547+
? {
1548+
message: serverError.message,
1549+
stack: serverError.stack,
1550+
code:
1551+
typeof serverError === "object" &&
1552+
serverError !== null &&
1553+
"code" in serverError
1554+
? String(serverError.code)
1555+
: undefined,
1556+
}
1557+
: { error: String(serverError) },
1558+
);
1559+
oauthServer = null;
1560+
}
1561+
}
1562+
15061563
if (signInMode === "browser") {
15071564
const opened = openBrowserUrl(url);
15081565
if (opened) {
@@ -1541,7 +1598,9 @@ async function runOAuthFlow(
15411598
stylePromptText(
15421599
waitingForCallback
15431600
? UI_COPY.oauth.callbackMissed
1544-
: UI_COPY.oauth.callbackUnavailable,
1601+
: signInMode === "manual"
1602+
? UI_COPY.oauth.callbackBypassed
1603+
: UI_COPY.oauth.callbackUnavailable,
15451604
"warning",
15461605
),
15471606
);

lib/request/fetch-helpers.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -531,16 +531,18 @@ function getSharedProxyDispatcher(proxyUrl: string): ProxyDispatcher {
531531
}
532532

533533
export async function closeSharedProxyDispatchers(): Promise<void> {
534-
const dispatchers = [...sharedProxyDispatchers.values()] as ClosableDispatcher[];
535-
sharedProxyDispatchers.clear();
536-
537-
await Promise.allSettled(
538-
dispatchers.map(async (dispatcher) => {
539-
if (typeof dispatcher.close === "function") {
540-
await dispatcher.close();
541-
}
542-
}),
543-
);
534+
while (sharedProxyDispatchers.size > 0) {
535+
const dispatchers = [...sharedProxyDispatchers.values()] as ClosableDispatcher[];
536+
sharedProxyDispatchers.clear();
537+
538+
await Promise.allSettled(
539+
dispatchers.map(async (dispatcher) => {
540+
if (typeof dispatcher.close === "function") {
541+
await dispatcher.close();
542+
}
543+
}),
544+
);
545+
}
544546
}
545547

546548
registerCleanup(closeSharedProxyDispatchers);

0 commit comments

Comments
 (0)