unbrowse 2.10.2 → 2.12.0

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.
@@ -11,6 +11,8 @@ import { getDefaultLoginConfig } from "../runtime/supervisor.js";
11
11
  const LOGIN_TIMEOUT_MS = 300_000;
12
12
  const POLL_INTERVAL_MS = 2_000;
13
13
  const MIN_WAIT_MS = 15_000;
14
+ const LOGIN_PATHS = /\/(login|signin|sign-in|sso|auth|oauth|uas\/login|checkpoint)/i;
15
+ const CLOUDFLARE_TEXT = /just a moment|attention required|verify you are human|cloudflare/i;
14
16
 
15
17
  /**
16
18
  * Returns the persistent profile directory for a given domain.
@@ -40,6 +42,45 @@ export interface StoredAuthBundle {
40
42
  source_meta?: BrowserAuthSourceMeta | null;
41
43
  }
42
44
 
45
+ export type InteractiveLoginAssessment =
46
+ | { status: "pending"; reason: string }
47
+ | { status: "authenticated"; reason: string }
48
+ | { status: "blocked"; reason: string };
49
+
50
+ export function assessInteractiveLoginState(input: {
51
+ currentUrl: string;
52
+ targetDomain: string;
53
+ initialCookieCount: number;
54
+ currentCookieCount: number;
55
+ hasCloudflareChallenge?: boolean;
56
+ pageText?: string;
57
+ }): InteractiveLoginAssessment {
58
+ let parsed: URL;
59
+ try {
60
+ parsed = new URL(input.currentUrl);
61
+ } catch {
62
+ return { status: "pending", reason: "invalid_url" };
63
+ }
64
+
65
+ const currentDomain = parsed.hostname.toLowerCase();
66
+ const targetNorm = input.targetDomain.toLowerCase();
67
+ const isOnTarget = currentDomain === targetNorm || currentDomain.endsWith(`.${targetNorm}`);
68
+ if (!isOnTarget) return { status: "pending", reason: "off_target_domain" };
69
+
70
+ if (input.hasCloudflareChallenge) return { status: "blocked", reason: "cloudflare_challenge" };
71
+ if (input.pageText && CLOUDFLARE_TEXT.test(input.pageText)) return { status: "blocked", reason: "cloudflare_text" };
72
+ if (LOGIN_PATHS.test(parsed.pathname)) return { status: "pending", reason: "still_on_login_path" };
73
+
74
+ if (input.currentCookieCount > input.initialCookieCount) {
75
+ return { status: "authenticated", reason: "new_cookies_on_target" };
76
+ }
77
+ if (input.currentCookieCount > 0) {
78
+ return { status: "authenticated", reason: "cookies_present_on_target" };
79
+ }
80
+
81
+ return { status: "pending", reason: "no_session_cookies" };
82
+ }
83
+
43
84
  export function storedAuthNeedsBrowserRefresh(bundle: StoredAuthBundle | null | undefined): boolean {
44
85
  if (!bundle) return true;
45
86
  if (bundle.cookies.length === 0 && Object.keys(bundle.headers).length === 0) return true;
@@ -95,6 +136,7 @@ export async function interactiveLogin(
95
136
 
96
137
  // Wait for user to complete login — detect via cookie changes + URL change
97
138
  let loggedIn = false;
139
+ let blockedReason: string | null = null;
98
140
  let lastLoggedUrl = "";
99
141
  const effectiveTimeout = loginConfig.interactive ? LOGIN_TIMEOUT_MS : loginConfig.timeout_ms;
100
142
  while (Date.now() - startTime < effectiveTimeout) {
@@ -103,9 +145,6 @@ export async function interactiveLogin(
103
145
 
104
146
  try {
105
147
  const currentUrl = await kuri.getCurrentUrl(tabId);
106
- const currentDomain = new URL(currentUrl).hostname.toLowerCase();
107
- const targetNorm = targetDomain.toLowerCase();
108
-
109
148
  if (currentUrl !== lastLoggedUrl) {
110
149
  log("auth", `navigated to: ${currentUrl}`);
111
150
  lastLoggedUrl = currentUrl;
@@ -113,25 +152,28 @@ export async function interactiveLogin(
113
152
 
114
153
  if (elapsed < MIN_WAIT_MS) continue;
115
154
 
116
- const isOnTarget = currentDomain === targetNorm || currentDomain.endsWith("." + targetNorm);
117
- if (isOnTarget) {
118
- const isStillLogin = /\/(login|signin|sign-in|sso|auth|oauth|uas\/login|checkpoint)/.test(new URL(currentUrl).pathname);
119
-
120
- const currentCookies = await kuri.getCookies(tabId);
121
- const currentCookieCount = currentCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
122
- const gotNewCookies = currentCookieCount > initialCookieCount;
123
-
124
- if (!isStillLogin && gotNewCookies) {
125
- loggedIn = true;
126
- log("auth", `login complete — ${currentUrl} (cookies: ${initialCookieCount} → ${currentCookieCount})`);
127
- break;
128
- }
129
-
130
- if (!isStillLogin && currentCookieCount > 0) {
131
- loggedIn = true;
132
- log("auth", `already logged in — ${currentUrl} (${currentCookieCount} cookies present)`);
133
- break;
134
- }
155
+ const currentCookies = await kuri.getCookies(tabId);
156
+ const currentCookieCount = currentCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
157
+ const hasCloudflareChallenge = await kuri.hasCloudflareChallenge(tabId).catch(() => false);
158
+ const pageText = hasCloudflareChallenge ? await kuri.getText(tabId).catch(() => "") : "";
159
+ const assessment = assessInteractiveLoginState({
160
+ currentUrl,
161
+ targetDomain,
162
+ initialCookieCount,
163
+ currentCookieCount,
164
+ hasCloudflareChallenge,
165
+ pageText,
166
+ });
167
+
168
+ if (assessment.status === "authenticated") {
169
+ loggedIn = true;
170
+ log("auth", `login complete — ${currentUrl} (cookies: ${initialCookieCount} → ${currentCookieCount}; ${assessment.reason})`);
171
+ break;
172
+ }
173
+
174
+ if (assessment.status === "blocked") {
175
+ blockedReason = assessment.reason;
176
+ log("auth", `login blocked — ${currentUrl} (${assessment.reason})`);
135
177
  }
136
178
  } catch { /* page navigating */ }
137
179
  }
@@ -139,7 +181,10 @@ export async function interactiveLogin(
139
181
  if (!loggedIn) {
140
182
  log("auth", `login wait ended after ${Math.round((Date.now() - startTime) / 1000)}s — fallback: ${loginConfig.fallback_strategy}`);
141
183
  if (loginConfig.fallback_strategy === "fail") {
142
- return { success: false, domain: targetDomain, cookies_stored: 0, error: "Login timed out (fallback: fail)" };
184
+ const error = blockedReason
185
+ ? `Login blocked (${blockedReason})`
186
+ : "Login timed out (fallback: fail)";
187
+ return { success: false, domain: targetDomain, cookies_stored: 0, error };
143
188
  }
144
189
  if (loginConfig.fallback_strategy === "skip") {
145
190
  log("auth", `skipping cookie capture per fallback_strategy`);