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.
- package/README.md +97 -10
- package/bin/unbrowse-wrapper.mjs +14 -30
- package/dist/cli.js +2832 -766
- package/dist/mcp.js +20392 -0
- package/package.json +5 -2
- package/runtime-src/agent-outcome.ts +166 -0
- package/runtime-src/analytics-session.ts +27 -5
- package/runtime-src/api/browse-index.ts +254 -0
- package/runtime-src/api/browse-session.ts +177 -0
- package/runtime-src/api/browse-submit.ts +462 -0
- package/runtime-src/api/routes.ts +348 -281
- package/runtime-src/auth/index.ts +68 -23
- package/runtime-src/cli.ts +574 -165
- package/runtime-src/client/index.ts +171 -18
- package/runtime-src/execution/index.ts +60 -10
- package/runtime-src/execution/robots.ts +167 -0
- package/runtime-src/kuri/client.ts +281 -89
- package/runtime-src/mcp.ts +1065 -0
- package/runtime-src/orchestrator/index.ts +66 -38
- package/runtime-src/orchestrator/timing-economics.ts +80 -0
- package/runtime-src/reverse-engineer/index.ts +33 -3
- package/runtime-src/runtime/setup.ts +8 -8
- package/runtime-src/single-binary.ts +20 -2
- package/runtime-src/types/skill.ts +2 -0
|
@@ -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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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`);
|