theclawbay 0.1.13 → 0.1.15

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.
@@ -231,9 +231,13 @@ class AgentCommand extends base_command_1.BaseCommand {
231
231
  const filePath = node_path_1.default.join(paths_1.accountsDir, `${account.name}.json`);
232
232
  try {
233
233
  const verification = (0, sync_accounts_1.buildVerificationRequestConfig)(managed, account);
234
+ const requiresLocalOtp = verification === null;
235
+ if (requiresLocalOtp) {
236
+ this.log(`Banned-signal check for "${accountName}" will use local browser OTP flow (Outlook mailbox detected).`);
237
+ }
234
238
  await (0, account_login_1.createAuthSnapshotForAccount)(account, filePath, {
235
- oauthShowBrowser: flags.showBrowser,
236
- verification,
239
+ oauthShowBrowser: flags.showBrowser || requiresLocalOtp,
240
+ verification: verification !== null && verification !== void 0 ? verification : undefined,
237
241
  log: (message) => this.log(message),
238
242
  });
239
243
  banVerificationState.set(accountName, { checkedAtMs: nowMs, confirmed: false });
@@ -16,6 +16,7 @@ const ZENDRIVER_IMPORT_ERROR_TOKEN = "THECLAWBAY_ZENDRIVER_IMPORT_ERROR";
16
16
  const ZENDRIVER_ERROR_TOKEN = "THECLAWBAY_ZENDRIVER_ERROR";
17
17
  const ZENDRIVER_LAUNCH_TIMEOUT_MS = 15000;
18
18
  const ACCOUNT_DEACTIVATED_ERROR_MARKER = "THECLAWBAY_ACCOUNT_DEACTIVATED";
19
+ const CAPTCHA_CHALLENGE_ERROR_MARKER = "THECLAWBAY_CAPTCHA_CHALLENGE";
19
20
  const DEFAULT_WIN_PATH_EXTENSIONS = [".EXE", ".CMD", ".BAT", ".COM"];
20
21
  function trimWrappingQuotes(value) {
21
22
  const trimmed = value.trim();
@@ -229,6 +230,8 @@ LOG_TOKEN = "THECLAWBAY_ZENDRIVER_LOG"
229
230
  IMPORT_ERROR_TOKEN = "THECLAWBAY_ZENDRIVER_IMPORT_ERROR"
230
231
  ERROR_TOKEN = "${ZENDRIVER_ERROR_TOKEN}"
231
232
  ACCOUNT_DEACTIVATED_TOKEN = "${ACCOUNT_DEACTIVATED_ERROR_MARKER}"
233
+ CAPTCHA_CHALLENGE_TOKEN = "${CAPTCHA_CHALLENGE_ERROR_MARKER}"
234
+ ALLOW_MANUAL_OTP = os.environ.get("THECLAWBAY_ALLOW_MANUAL_OTP", "").strip() == "1"
232
235
 
233
236
  try:
234
237
  import zendriver as zd
@@ -297,6 +300,15 @@ OTP_PROMPT_SNIPPETS = [
297
300
  "código de verificação",
298
301
  "código de confirmação",
299
302
  ]
303
+ CAPTCHA_PROMPT_SNIPPETS = [
304
+ "captcha",
305
+ "verify you are human",
306
+ "prove you're human",
307
+ "human verification",
308
+ "security challenge",
309
+ "security check",
310
+ "i'm not a robot",
311
+ ]
300
312
  CONSENT_PROMPT_SNIPPETS = [
301
313
  "allow access",
302
314
  "authorize",
@@ -316,6 +328,32 @@ ACCOUNT_DEACTIVATED_SNIPPETS = [
316
328
  ]
317
329
 
318
330
 
331
+ def _extract_email_domain(email):
332
+ value = str(email or "").strip().lower()
333
+ at = value.rfind("@")
334
+ if at < 0 or at == len(value) - 1:
335
+ return ""
336
+ return value[at + 1 :]
337
+
338
+
339
+ def _is_outlook_domain(domain):
340
+ if not domain:
341
+ return False
342
+ if domain in ("outlook.com", "hotmail.com", "live.com", "msn.com"):
343
+ return True
344
+ return (
345
+ domain.startswith("outlook.")
346
+ or domain.startswith("hotmail.")
347
+ or domain.startswith("live.")
348
+ or domain.startswith("msn.")
349
+ )
350
+
351
+
352
+ PREFER_OUTLOOK_LOCAL = _is_outlook_domain(
353
+ _extract_email_domain(VERIFICATION_ACCOUNT_EMAIL or LOGIN_EMAIL)
354
+ )
355
+
356
+
319
357
  def _log(message):
320
358
  print(LOG_TOKEN + ":" + str(message), flush=True)
321
359
 
@@ -554,6 +592,14 @@ async def _fill_otp(tab, code):
554
592
  return "missing"
555
593
 
556
594
 
595
+ async def _open_outlook_mail_tab(tab):
596
+ try:
597
+ opened = await tab.evaluate("(() => { window.open('https://outlook.live.com/mail/0/', '_blank'); return true; })()")
598
+ return bool(opened)
599
+ except Exception:
600
+ return False
601
+
602
+
557
603
  def _contains_snippet(html, snippets):
558
604
  return any(snippet in html for snippet in snippets)
559
605
 
@@ -574,6 +620,10 @@ def _is_account_deactivated_state(html):
574
620
  return _contains_snippet(html, ACCOUNT_DEACTIVATED_SNIPPETS)
575
621
 
576
622
 
623
+ def _is_captcha_state(html):
624
+ return _contains_snippet(html, CAPTCHA_PROMPT_SNIPPETS)
625
+
626
+
577
627
  def _request_verification_code_sync():
578
628
  if not VERIFICATION_ENDPOINT or not VERIFICATION_AUTH:
579
629
  raise RuntimeError("verification endpoint/auth missing for OTP retrieval")
@@ -660,6 +710,8 @@ async def _automation_loop(tab):
660
710
  otp_submit_attempts = 0
661
711
  otp_total_submit_attempts = 0
662
712
  otp_needs_submit = False
713
+ manual_otp_mode = False
714
+ outlook_tab_opened = False
663
715
  transient_error_hits = 0
664
716
  success_seen_at = 0.0
665
717
 
@@ -673,6 +725,15 @@ async def _automation_loop(tab):
673
725
  ACCOUNT_DEACTIVATED_TOKEN + ": OpenAI reported this account is deleted or deactivated."
674
726
  )
675
727
 
728
+ if _is_captcha_state(html):
729
+ if show:
730
+ _log("captcha/security challenge detected; complete it manually in this browser")
731
+ await asyncio.sleep(0.9)
732
+ continue
733
+ raise RuntimeError(
734
+ CAPTCHA_CHALLENGE_TOKEN + ": captcha/security challenge detected and browser is headless"
735
+ )
736
+
676
737
  if _is_success_state(html, current_url):
677
738
  if success_seen_at <= 0.0:
678
739
  success_seen_at = now
@@ -740,9 +801,41 @@ async def _automation_loop(tab):
740
801
  continue
741
802
 
742
803
  if _contains_snippet(html, OTP_PROMPT_SNIPPETS):
804
+ if manual_otp_mode:
805
+ await asyncio.sleep(0.9)
806
+ continue
743
807
  if not otp_code:
808
+ if not VERIFICATION_ENDPOINT or not VERIFICATION_AUTH:
809
+ if show and ALLOW_MANUAL_OTP:
810
+ manual_otp_mode = True
811
+ if PREFER_OUTLOOK_LOCAL and not outlook_tab_opened:
812
+ outlook_tab_opened = await _open_outlook_mail_tab(tab)
813
+ if outlook_tab_opened:
814
+ _log("opened Outlook mailbox tab for local OTP retrieval")
815
+ _log("backend OTP API disabled for this account; enter OTP manually in this browser to continue")
816
+ await asyncio.sleep(0.9)
817
+ continue
818
+ raise RuntimeError("verification endpoint/auth missing for OTP retrieval")
819
+
744
820
  _log("requesting OTP code from backend")
745
- otp_code = await asyncio.to_thread(_request_verification_code_sync)
821
+ try:
822
+ otp_code = await asyncio.to_thread(_request_verification_code_sync)
823
+ except Exception as exc:
824
+ reason = _compact_error_text(exc)
825
+ if show and ALLOW_MANUAL_OTP:
826
+ manual_otp_mode = True
827
+ if PREFER_OUTLOOK_LOCAL and not outlook_tab_opened:
828
+ outlook_tab_opened = await _open_outlook_mail_tab(tab)
829
+ if outlook_tab_opened:
830
+ _log("opened Outlook mailbox tab for local OTP retrieval")
831
+ _log(
832
+ "backend OTP retrieval failed"
833
+ + (f" ({reason})" if reason else "")
834
+ + "; enter OTP manually in this browser to continue"
835
+ )
836
+ await asyncio.sleep(0.9)
837
+ continue
838
+ raise
746
839
  _log("received OTP code from backend")
747
840
  otp_submit_attempts = 0
748
841
  otp_needs_submit = True
@@ -883,6 +976,21 @@ function extractZendriverFailureReason(output) {
883
976
  return summarizeErrorDetails(runtimeError, 700);
884
977
  return null;
885
978
  }
979
+ function isCaptchaChallengeAuthError(error) {
980
+ if (!(error instanceof Error))
981
+ return false;
982
+ const message = error.message.toLowerCase();
983
+ return (message.includes(CAPTCHA_CHALLENGE_ERROR_MARKER.toLowerCase()) ||
984
+ message.includes("captcha/security challenge detected"));
985
+ }
986
+ function isBackendOtpRetrievalAuthError(error) {
987
+ if (!(error instanceof Error))
988
+ return false;
989
+ const message = error.message.toLowerCase();
990
+ return (message.includes("backend verification job failed") ||
991
+ message.includes("backend verification request failed") ||
992
+ message.includes("timed out waiting for verification code"));
993
+ }
886
994
  function extractOauthAuthorizeUrl(output) {
887
995
  var _a, _b, _c;
888
996
  const cleaned = stripAnsi(output);
@@ -922,6 +1030,7 @@ async function launchWithZendriverPython(pythonBin, oauthUrl, showBrowser, autom
922
1030
  THECLAWBAY_VERIFICATION_AUTH: (_d = (_c = automation.verification) === null || _c === void 0 ? void 0 : _c.authorization) !== null && _d !== void 0 ? _d : "",
923
1031
  THECLAWBAY_VERIFICATION_ACCOUNT_EMAIL: (_f = (_e = automation.verification) === null || _e === void 0 ? void 0 : _e.accountEmail) !== null && _f !== void 0 ? _f : automation.loginEmail,
924
1032
  THECLAWBAY_VERIFICATION_ACCOUNT_NAME: (_h = (_g = automation.verification) === null || _g === void 0 ? void 0 : _g.accountName) !== null && _h !== void 0 ? _h : "",
1033
+ THECLAWBAY_ALLOW_MANUAL_OTP: automation.allowManualOtpFallback ? "1" : "",
925
1034
  },
926
1035
  });
927
1036
  return new Promise((resolve) => {
@@ -1090,7 +1199,10 @@ async function runCodexBrowserOAuth(codexHome, codexSpawnPlan, timeoutMs, automa
1090
1199
  if (!oauthUrl)
1091
1200
  return;
1092
1201
  oauthUrlDiscovered = true;
1093
- browserSessionTask = launchOauthUrlInZendriver(oauthUrl, options.showBrowser === true, automation, options.log)
1202
+ browserSessionTask = launchOauthUrlInZendriver(oauthUrl, options.showBrowser === true, {
1203
+ ...automation,
1204
+ allowManualOtpFallback: options.allowManualOtpFallback === true,
1205
+ }, options.log)
1094
1206
  .then((session) => {
1095
1207
  oauthBrowserStarted = true;
1096
1208
  const runtimeTask = session
@@ -1240,15 +1352,41 @@ async function createAuthSnapshotForAccount(account, destinationPath, options) {
1240
1352
  const authPath = node_path_1.default.join(tempCodexHome, "auth.json");
1241
1353
  try {
1242
1354
  log === null || log === void 0 ? void 0 : log(`[${account.name}] starting Codex OAuth login flow`);
1243
- await runCodexBrowserOAuth(tempCodexHome, codexSpawnPlan, timeoutMs, {
1244
- accountName: account.name,
1245
- loginEmail: account.loginEmail,
1246
- loginPassword: account.loginPassword,
1247
- verification: options.verification,
1248
- }, {
1249
- log,
1250
- showBrowser: oauthShowBrowser,
1251
- });
1355
+ try {
1356
+ await runCodexBrowserOAuth(tempCodexHome, codexSpawnPlan, timeoutMs, {
1357
+ accountName: account.name,
1358
+ loginEmail: account.loginEmail,
1359
+ loginPassword: account.loginPassword,
1360
+ verification: options.verification,
1361
+ }, {
1362
+ log,
1363
+ showBrowser: oauthShowBrowser,
1364
+ allowManualOtpFallback: oauthShowBrowser,
1365
+ });
1366
+ }
1367
+ catch (error) {
1368
+ const shouldRetryInVisibleBrowser = !oauthShowBrowser &&
1369
+ (isCaptchaChallengeAuthError(error) || isBackendOtpRetrievalAuthError(error));
1370
+ if (!shouldRetryInVisibleBrowser) {
1371
+ throw error;
1372
+ }
1373
+ if (isCaptchaChallengeAuthError(error)) {
1374
+ log === null || log === void 0 ? void 0 : log(`[${account.name}] captcha/security challenge detected; reopening visible browser for manual completion`);
1375
+ }
1376
+ else {
1377
+ log === null || log === void 0 ? void 0 : log(`[${account.name}] backend OTP fetch failed; reopening visible browser so OTP can be completed manually`);
1378
+ }
1379
+ await runCodexBrowserOAuth(tempCodexHome, codexSpawnPlan, timeoutMs, {
1380
+ accountName: account.name,
1381
+ loginEmail: account.loginEmail,
1382
+ loginPassword: account.loginPassword,
1383
+ verification: options.verification,
1384
+ }, {
1385
+ log,
1386
+ showBrowser: true,
1387
+ allowManualOtpFallback: true,
1388
+ });
1389
+ }
1252
1390
  const authReady = await waitForFile(authPath, 2500);
1253
1391
  if (!authReady) {
1254
1392
  throw new Error("Codex OAuth login finished without auth.json (login not completed).");
@@ -6,7 +6,8 @@ export type ManagedAccountPayload = {
6
6
  loginPassword?: string | null;
7
7
  };
8
8
  export declare function normalizeManagedCredentialAccount(item: ManagedAccountPayload): ManagedCredentialAccount | null;
9
- export declare function buildVerificationRequestConfig(managedConfig: ManagedConfig, account: ManagedCredentialAccount): VerificationRequestConfig;
9
+ export declare function requiresLocalOtpAutomation(account: ManagedCredentialAccount): boolean;
10
+ export declare function buildVerificationRequestConfig(managedConfig: ManagedConfig, account: ManagedCredentialAccount): VerificationRequestConfig | null;
10
11
  export declare function syncManagedAccounts(accounts: ManagedAccountPayload[], options: {
11
12
  managedConfig: ManagedConfig;
12
13
  log?: (message: string) => void;
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.normalizeManagedCredentialAccount = normalizeManagedCredentialAccount;
7
+ exports.requiresLocalOtpAutomation = requiresLocalOtpAutomation;
7
8
  exports.buildVerificationRequestConfig = buildVerificationRequestConfig;
8
9
  exports.syncManagedAccounts = syncManagedAccounts;
9
10
  const node_crypto_1 = require("node:crypto");
@@ -13,6 +14,8 @@ const account_service_1 = require("../accounts/account-service");
13
14
  const paths_1 = require("../config/paths");
14
15
  const account_login_1 = require("./account-login");
15
16
  const ACCOUNT_NAME_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
17
+ const OUTLOOK_EXACT_DOMAINS = new Set(["outlook.com", "hotmail.com", "live.com", "msn.com"]);
18
+ const OUTLOOK_DOMAIN_PREFIXES = ["outlook.", "hotmail.", "live.", "msn."];
16
19
  const EMPTY_STATE = {
17
20
  version: 1,
18
21
  accounts: {},
@@ -45,8 +48,32 @@ function joinUrl(base, pathname) {
45
48
  url.pathname = `${url.pathname.replace(/\/+$/g, "")}${pathname.startsWith("/") ? "" : "/"}${pathname}`;
46
49
  return url.toString();
47
50
  }
51
+ function extractEmailDomain(email) {
52
+ const normalized = email.trim().toLowerCase();
53
+ const atIndex = normalized.lastIndexOf("@");
54
+ if (atIndex < 0 || atIndex === normalized.length - 1)
55
+ return "";
56
+ return normalized.slice(atIndex + 1);
57
+ }
58
+ function isOutlookDomain(domain) {
59
+ if (!domain)
60
+ return false;
61
+ if (OUTLOOK_EXACT_DOMAINS.has(domain))
62
+ return true;
63
+ for (const prefix of OUTLOOK_DOMAIN_PREFIXES) {
64
+ if (domain.startsWith(prefix))
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ function requiresLocalOtpAutomation(account) {
70
+ return isOutlookDomain(extractEmailDomain(account.loginEmail));
71
+ }
48
72
  function buildVerificationRequestConfig(managedConfig, account) {
49
73
  var _a, _b, _c;
74
+ if (requiresLocalOtpAutomation(account)) {
75
+ return null;
76
+ }
50
77
  if (managedConfig.authType === "apiKey") {
51
78
  return {
52
79
  endpoint: joinUrl(managedConfig.backendUrl, "/api/codex-auth/v1/verification-code"),
@@ -129,10 +156,14 @@ async function syncManagedAccounts(accounts, options) {
129
156
  log === null || log === void 0 ? void 0 : log(`[${account.name}] refreshing account session via Codex OAuth login`);
130
157
  try {
131
158
  const verification = buildVerificationRequestConfig(options.managedConfig, account);
159
+ const requiresLocalOtp = verification === null;
160
+ if (requiresLocalOtp) {
161
+ log === null || log === void 0 ? void 0 : log(`[${account.name}] outlook mailbox detected; skipping backend OTP API and using local browser OTP flow`);
162
+ }
132
163
  await (0, account_login_1.createAuthSnapshotForAccount)(account, filePath, {
133
164
  log,
134
- oauthShowBrowser: options.oauthShowBrowser,
135
- verification,
165
+ oauthShowBrowser: options.oauthShowBrowser || requiresLocalOtp,
166
+ verification: verification !== null && verification !== void 0 ? verification : undefined,
136
167
  });
137
168
  log === null || log === void 0 ? void 0 : log(`[${account.name}] account session refreshed`);
138
169
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "The Claw Bay CLI: manage and auto-switch between Codex accounts.",
5
5
  "license": "MIT",
6
6
  "bin": {