@bjesuiter/codex-switcher 1.7.3 → 1.7.4

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 (3) hide show
  1. package/README.md +3 -6
  2. package/cdx.mjs +57 -40
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,15 +6,12 @@ Switch the coding-agents [pi](https://pi.dev/), [codex](https://developers.opena
6
6
 
7
7
  ## Latest Changes
8
8
 
9
- ### 1.7.3
9
+ ### 1.7.4
10
10
 
11
11
  #### Fixes
12
12
 
13
- - Add recovery for OAuth callback port conflicts (`127.0.0.1:1455`) during login/relogin:
14
- - detect `EADDRINUSE` as a port-in-use condition
15
- - prompt to kill the existing listener and retry, switch to device flow, or cancel
16
- - show detected PID/command details when available before confirmation
17
- - Improve callback server startup diagnostics by exposing startup error reason/code details, making port/listen failures easier to troubleshoot.
13
+ - Detect Cloudflare/bot-protection HTML challenge responses during OAuth device flow startup and token polling, and report an explicit `cloudflare_challenge` reason instead of only a generic HTTP 403.
14
+ - When that challenge is detected, show a clear workaround: retry without `--device-flow` to use browser/manual callback flow.
18
15
 
19
16
  see full changelog here: https://github.com/bjesuiter/codex-switcher/blob/main/CHANGELOG.md
20
17
 
package/cdx.mjs CHANGED
@@ -15,7 +15,7 @@ import { generatePKCE } from "@openauthjs/openauth/pkce";
15
15
  import http from "node:http";
16
16
 
17
17
  //#region package.json
18
- var version = "1.7.3";
18
+ var version = "1.7.4";
19
19
 
20
20
  //#endregion
21
21
  //#region lib/platform/path-resolver.ts
@@ -1036,6 +1036,46 @@ const truncateForLog = (value, maxLength = 300) => {
1036
1036
  if (value.length <= maxLength) return value;
1037
1037
  return `${value.slice(0, maxLength)}…`;
1038
1038
  };
1039
+ const isCloudflareChallengeResponse = (headers, bodyText) => {
1040
+ if (headers.get("cf-mitigated")?.toLowerCase() === "challenge") return true;
1041
+ if (!bodyText) return false;
1042
+ return [
1043
+ /<title>Just a moment\.\.\.<\/title>/i,
1044
+ /Enable JavaScript and cookies to continue/i,
1045
+ /challenge-platform/i,
1046
+ /_cf_chl_opt/i
1047
+ ].some((pattern) => pattern.test(bodyText));
1048
+ };
1049
+ const parseOAuthErrorResponse = async (res) => {
1050
+ let rawBody;
1051
+ try {
1052
+ rawBody = await res.text();
1053
+ } catch {
1054
+ rawBody = void 0;
1055
+ }
1056
+ const failureReason = isCloudflareChallengeResponse(res.headers, rawBody) ? "cloudflare_challenge" : void 0;
1057
+ if (!rawBody) return { ...failureReason ? { failureReason } : {} };
1058
+ const trimmed = rawBody.trim();
1059
+ if (!trimmed) return { ...failureReason ? { failureReason } : {} };
1060
+ try {
1061
+ const json = JSON.parse(trimmed);
1062
+ return {
1063
+ ...json.error ? { oauthError: json.error } : {},
1064
+ ...typeof json.interval === "number" ? { interval: json.interval } : {},
1065
+ responseBody: truncateForLog(JSON.stringify({
1066
+ ...json.error ? { error: json.error } : {},
1067
+ ...json.error_description ? { error_description: json.error_description } : {},
1068
+ ...typeof json.interval === "number" ? { interval: json.interval } : {}
1069
+ })),
1070
+ ...failureReason ? { failureReason } : {}
1071
+ };
1072
+ } catch {
1073
+ return {
1074
+ responseBody: failureReason === "cloudflare_challenge" ? "Cloudflare challenge response detected (HTML page)" : truncateForLog(trimmed),
1075
+ ...failureReason ? { failureReason } : {}
1076
+ };
1077
+ }
1078
+ };
1039
1079
  const startDeviceAuthorizationFlow = async () => {
1040
1080
  try {
1041
1081
  const res = await fetch(DEVICE_CODE_URL, {
@@ -1047,28 +1087,14 @@ const startDeviceAuthorizationFlow = async () => {
1047
1087
  })
1048
1088
  });
1049
1089
  if (!res.ok) {
1050
- let oauthError;
1051
- let responseBody;
1052
- try {
1053
- const json = await res.json();
1054
- oauthError = json.error;
1055
- responseBody = truncateForLog(JSON.stringify({
1056
- ...json.error ? { error: json.error } : {},
1057
- ...json.error_description ? { error_description: json.error_description } : {}
1058
- }));
1059
- } catch {
1060
- try {
1061
- responseBody = truncateForLog(await res.text());
1062
- } catch {
1063
- responseBody = void 0;
1064
- }
1065
- }
1090
+ const { oauthError, responseBody, failureReason } = await parseOAuthErrorResponse(res);
1066
1091
  return {
1067
1092
  type: "failed",
1068
- error: `Device code request failed with HTTP ${res.status} ${res.statusText}`,
1093
+ error: failureReason === "cloudflare_challenge" ? "Device code request was blocked by a Cloudflare challenge response." : `Device code request failed with HTTP ${res.status} ${res.statusText}`,
1069
1094
  status: res.status,
1070
1095
  ...oauthError ? { oauthError } : {},
1071
- ...responseBody ? { responseBody } : {}
1096
+ ...responseBody ? { responseBody } : {},
1097
+ ...failureReason ? { failureReason } : {}
1072
1098
  };
1073
1099
  }
1074
1100
  const json = await res.json();
@@ -1121,25 +1147,7 @@ const pollDeviceAuthorizationToken = async (deviceCode) => {
1121
1147
  idToken: json.id_token
1122
1148
  };
1123
1149
  }
1124
- let errorCode;
1125
- let interval;
1126
- let responseBody;
1127
- try {
1128
- const json = await res.json();
1129
- errorCode = json.error;
1130
- interval = json.interval;
1131
- responseBody = truncateForLog(JSON.stringify({
1132
- ...json.error ? { error: json.error } : {},
1133
- ...json.error_description ? { error_description: json.error_description } : {},
1134
- ...typeof json.interval === "number" ? { interval: json.interval } : {}
1135
- }));
1136
- } catch {
1137
- try {
1138
- responseBody = truncateForLog(await res.text());
1139
- } catch {
1140
- responseBody = void 0;
1141
- }
1142
- }
1150
+ const { oauthError: errorCode, interval, responseBody, failureReason } = await parseOAuthErrorResponse(res);
1143
1151
  if (errorCode === "authorization_pending") return {
1144
1152
  type: "pending",
1145
1153
  interval: typeof interval === "number" && interval > 0 ? interval : 5
@@ -1152,10 +1160,11 @@ const pollDeviceAuthorizationToken = async (deviceCode) => {
1152
1160
  if (errorCode === "expired_token") return { type: "expired" };
1153
1161
  return {
1154
1162
  type: "failed",
1155
- error: `Device token polling failed with HTTP ${res.status} ${res.statusText}`,
1163
+ error: failureReason === "cloudflare_challenge" ? "Device token polling was blocked by a Cloudflare challenge response." : `Device token polling failed with HTTP ${res.status} ${res.statusText}`,
1156
1164
  status: res.status,
1157
1165
  ...errorCode ? { oauthError: errorCode } : {},
1158
- ...responseBody ? { responseBody } : {}
1166
+ ...responseBody ? { responseBody } : {},
1167
+ ...failureReason ? { failureReason } : {}
1159
1168
  };
1160
1169
  } catch (error) {
1161
1170
  return {
@@ -1561,6 +1570,10 @@ const runDeviceOAuthFlow = async (useSpinner) => {
1561
1570
  if (typeof deviceFlowResult.status === "number") p.log.error(`HTTP status: ${deviceFlowResult.status}`);
1562
1571
  if (deviceFlowResult.oauthError) p.log.error(`OAuth error: ${deviceFlowResult.oauthError}`);
1563
1572
  if (deviceFlowResult.responseBody) p.log.error(`Response: ${deviceFlowResult.responseBody}`);
1573
+ if (deviceFlowResult.failureReason === "cloudflare_challenge") {
1574
+ p.log.warning("Detected Cloudflare challenge on auth.openai.com.");
1575
+ p.log.info("Retry without --device-flow to use browser/manual callback flow.");
1576
+ }
1564
1577
  return null;
1565
1578
  }
1566
1579
  const deviceFlow = deviceFlowResult.flow;
@@ -1610,6 +1623,10 @@ const runDeviceOAuthFlow = async (useSpinner) => {
1610
1623
  if (typeof pollResult.status === "number") p.log.error(`HTTP status: ${pollResult.status}`);
1611
1624
  if (pollResult.oauthError) p.log.error(`OAuth error: ${pollResult.oauthError}`);
1612
1625
  if (pollResult.responseBody) p.log.error(`Response: ${pollResult.responseBody}`);
1626
+ if (pollResult.failureReason === "cloudflare_challenge") {
1627
+ p.log.warning("Detected Cloudflare challenge on auth.openai.com.");
1628
+ p.log.info("Retry without --device-flow to use browser/manual callback flow.");
1629
+ }
1613
1630
  }
1614
1631
  return null;
1615
1632
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bjesuiter/codex-switcher",
3
- "version": "1.7.3",
3
+ "version": "1.7.4",
4
4
  "type": "module",
5
5
  "description": "CLI tool to switch between multiple OpenAI accounts for OpenCode",
6
6
  "bin": {