unbrowse 1.2.0 → 2.0.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/package.json +1 -1
- package/runtime-src/auth/index.ts +25 -64
- package/runtime-src/capture/index.ts +543 -489
- package/runtime-src/kuri/client.ts +498 -0
- package/runtime-src/server.ts +8 -8
package/package.json
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { executeCommand } from "agent-browser/dist/actions.js";
|
|
1
|
+
import * as kuri from "../kuri/client.js";
|
|
3
2
|
import { storeCredential, getCredential, deleteCredential } from "../vault/index.js";
|
|
4
3
|
import { nanoid } from "nanoid";
|
|
5
4
|
import { isDomainMatch, getRegistrableDomain } from "../domain.js";
|
|
@@ -10,16 +9,16 @@ import fs from "node:fs";
|
|
|
10
9
|
|
|
11
10
|
const LOGIN_TIMEOUT_MS = 300_000;
|
|
12
11
|
const POLL_INTERVAL_MS = 2_000;
|
|
13
|
-
const MIN_WAIT_MS = 15_000;
|
|
12
|
+
const MIN_WAIT_MS = 15_000;
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Returns the persistent profile directory for a given domain.
|
|
17
16
|
* Stored under ~/.unbrowse/profiles/<registrableDomain>.
|
|
18
|
-
* Exporting so capture/execute can also launch with the profile if needed.
|
|
19
17
|
*/
|
|
20
18
|
export function getProfilePath(domain: string): string {
|
|
21
19
|
return path.join(os.homedir(), ".unbrowse", "profiles", getRegistrableDomain(domain));
|
|
22
20
|
}
|
|
21
|
+
|
|
23
22
|
export interface LoginResult {
|
|
24
23
|
success: boolean;
|
|
25
24
|
domain: string;
|
|
@@ -29,8 +28,10 @@ export interface LoginResult {
|
|
|
29
28
|
|
|
30
29
|
/**
|
|
31
30
|
* Open a visible browser for the user to complete login.
|
|
32
|
-
*
|
|
33
|
-
*
|
|
31
|
+
* Uses Kuri to manage the browser tab, polls for login completion via cookies.
|
|
32
|
+
*
|
|
33
|
+
* Note: Kuri manages Chrome — for interactive login, the user's Chrome
|
|
34
|
+
* needs to be visible. We navigate to the login URL and poll for cookie changes.
|
|
34
35
|
*/
|
|
35
36
|
export async function interactiveLogin(
|
|
36
37
|
url: string,
|
|
@@ -39,26 +40,23 @@ export async function interactiveLogin(
|
|
|
39
40
|
const targetDomain = domain ?? new URL(url).hostname;
|
|
40
41
|
const profileDir = getProfilePath(targetDomain);
|
|
41
42
|
|
|
42
|
-
const browser = new BrowserManager();
|
|
43
43
|
log("auth", `interactiveLogin — url: ${url}, domain: ${targetDomain}`);
|
|
44
44
|
|
|
45
45
|
try {
|
|
46
46
|
fs.mkdirSync(profileDir, { recursive: true });
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await
|
|
55
|
-
|
|
56
|
-
const page = browser.getPage();
|
|
47
|
+
|
|
48
|
+
// Start Kuri and get a tab
|
|
49
|
+
await kuri.start();
|
|
50
|
+
const tabId = await kuri.getDefaultTab();
|
|
51
|
+
await kuri.networkEnable(tabId);
|
|
52
|
+
|
|
53
|
+
// Navigate to login URL
|
|
54
|
+
await kuri.navigate(tabId, url);
|
|
55
|
+
|
|
57
56
|
const startTime = Date.now();
|
|
58
57
|
|
|
59
|
-
// Snapshot initial cookies
|
|
60
|
-
const
|
|
61
|
-
const initialCookies = context ? await context.cookies() : [];
|
|
58
|
+
// Snapshot initial cookies
|
|
59
|
+
const initialCookies = await kuri.getCookies(tabId);
|
|
62
60
|
const initialCookieCount = initialCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
|
|
63
61
|
log("auth", `initial cookies for ${targetDomain}: ${initialCookieCount}`);
|
|
64
62
|
|
|
@@ -70,7 +68,7 @@ export async function interactiveLogin(
|
|
|
70
68
|
const elapsed = Date.now() - startTime;
|
|
71
69
|
|
|
72
70
|
try {
|
|
73
|
-
const currentUrl =
|
|
71
|
+
const currentUrl = await kuri.getCurrentUrl(tabId);
|
|
74
72
|
const currentDomain = new URL(currentUrl).hostname.toLowerCase();
|
|
75
73
|
const targetNorm = targetDomain.toLowerCase();
|
|
76
74
|
|
|
@@ -79,15 +77,13 @@ export async function interactiveLogin(
|
|
|
79
77
|
lastLoggedUrl = currentUrl;
|
|
80
78
|
}
|
|
81
79
|
|
|
82
|
-
// Don't even check for login completion until MIN_WAIT_MS has passed
|
|
83
80
|
if (elapsed < MIN_WAIT_MS) continue;
|
|
84
81
|
|
|
85
82
|
const isOnTarget = currentDomain === targetNorm || currentDomain.endsWith("." + targetNorm);
|
|
86
83
|
if (isOnTarget) {
|
|
87
84
|
const isStillLogin = /\/(login|signin|sign-in|sso|auth|oauth|uas\/login|checkpoint)/.test(new URL(currentUrl).pathname);
|
|
88
85
|
|
|
89
|
-
|
|
90
|
-
const currentCookies = context ? await context.cookies() : [];
|
|
86
|
+
const currentCookies = await kuri.getCookies(tabId);
|
|
91
87
|
const currentCookieCount = currentCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
|
|
92
88
|
const gotNewCookies = currentCookieCount > initialCookieCount;
|
|
93
89
|
|
|
@@ -97,9 +93,6 @@ export async function interactiveLogin(
|
|
|
97
93
|
break;
|
|
98
94
|
}
|
|
99
95
|
|
|
100
|
-
// Handle "already logged in" — user was redirected away from login
|
|
101
|
-
// to a non-login page, and cookies already exist (even if count didn't change).
|
|
102
|
-
// This means the persistent profile already has the session.
|
|
103
96
|
if (!isStillLogin && currentCookieCount > 0) {
|
|
104
97
|
loggedIn = true;
|
|
105
98
|
log("auth", `already logged in — ${currentUrl} (${currentCookieCount} cookies present)`);
|
|
@@ -110,14 +103,11 @@ export async function interactiveLogin(
|
|
|
110
103
|
}
|
|
111
104
|
|
|
112
105
|
if (!loggedIn) {
|
|
113
|
-
// Even on timeout, grab whatever cookies we have — the user may have logged in
|
|
114
|
-
// but the detection missed it
|
|
115
106
|
log("auth", `login wait ended after ${Math.round((Date.now() - startTime) / 1000)}s — capturing cookies anyway`);
|
|
116
107
|
}
|
|
117
108
|
|
|
118
109
|
// Extract and store cookies
|
|
119
|
-
const
|
|
120
|
-
const cookies = finalContext ? await finalContext.cookies() : [];
|
|
110
|
+
const cookies = await kuri.getCookies(tabId);
|
|
121
111
|
const domainCookies = cookies.filter((c) => isDomainMatch(c.domain, targetDomain));
|
|
122
112
|
|
|
123
113
|
if (domainCookies.length === 0) {
|
|
@@ -135,33 +125,17 @@ export async function interactiveLogin(
|
|
|
135
125
|
|
|
136
126
|
return { success: true, domain: targetDomain, cookies_stored: storableCookies.length };
|
|
137
127
|
} finally {
|
|
138
|
-
|
|
139
|
-
const context = browser.getContext();
|
|
140
|
-
if (context) await Promise.race([context.close(), new Promise<void>((r) => setTimeout(r, 4000))]);
|
|
141
|
-
} catch { /* ignore */ }
|
|
128
|
+
// Cleanup handled by Kuri's tab management
|
|
142
129
|
}
|
|
143
130
|
}
|
|
144
131
|
|
|
145
132
|
/**
|
|
146
133
|
* Extract cookies directly from Chrome/Firefox SQLite databases.
|
|
147
134
|
* No browser launch needed, Chrome can stay open.
|
|
148
|
-
* Stores extracted cookies in the vault for subsequent use.
|
|
149
|
-
* Always stores under the registrable domain key for consistency.
|
|
150
135
|
*/
|
|
151
136
|
export async function extractBrowserAuth(
|
|
152
137
|
domain: string,
|
|
153
|
-
opts?: {
|
|
154
|
-
browser?: "auto" | "firefox" | "chrome" | "chromium";
|
|
155
|
-
chromeProfile?: string;
|
|
156
|
-
firefoxProfile?: string;
|
|
157
|
-
chromium?: {
|
|
158
|
-
profile?: string;
|
|
159
|
-
userDataDir?: string;
|
|
160
|
-
cookieDbPath?: string;
|
|
161
|
-
safeStorageService?: string;
|
|
162
|
-
browserName?: string;
|
|
163
|
-
};
|
|
164
|
-
}
|
|
138
|
+
opts?: { chromeProfile?: string; firefoxProfile?: string }
|
|
165
139
|
): Promise<LoginResult> {
|
|
166
140
|
const { extractBrowserCookies } = await import("./browser-cookies.js");
|
|
167
141
|
|
|
@@ -176,7 +150,6 @@ export async function extractBrowserAuth(
|
|
|
176
150
|
};
|
|
177
151
|
}
|
|
178
152
|
|
|
179
|
-
// Store in vault under same format as interactiveLogin
|
|
180
153
|
const storableCookies = result.cookies.map((c) => ({
|
|
181
154
|
name: c.name,
|
|
182
155
|
value: c.value,
|
|
@@ -188,7 +161,6 @@ export async function extractBrowserAuth(
|
|
|
188
161
|
expires: c.expires,
|
|
189
162
|
}));
|
|
190
163
|
|
|
191
|
-
// Normalize: always store under registrable domain for consistent lookups
|
|
192
164
|
const vaultKey = `auth:${getRegistrableDomain(domain)}`;
|
|
193
165
|
await storeCredential(
|
|
194
166
|
vaultKey,
|
|
@@ -214,7 +186,7 @@ type AuthCookie = {
|
|
|
214
186
|
function filterExpired(cookies: AuthCookie[]): AuthCookie[] {
|
|
215
187
|
const now = Math.floor(Date.now() / 1000);
|
|
216
188
|
return cookies.filter((c) => {
|
|
217
|
-
if (c.expires == null || c.expires <= 0) return true;
|
|
189
|
+
if (c.expires == null || c.expires <= 0) return true;
|
|
218
190
|
return c.expires > now;
|
|
219
191
|
});
|
|
220
192
|
}
|
|
@@ -222,12 +194,10 @@ function filterExpired(cookies: AuthCookie[]): AuthCookie[] {
|
|
|
222
194
|
/**
|
|
223
195
|
* Retrieve stored auth cookies for a domain from the vault.
|
|
224
196
|
* Filters out expired cookies automatically.
|
|
225
|
-
* Checks both registrable domain key and exact domain key for backward compat.
|
|
226
197
|
*/
|
|
227
198
|
export async function getStoredAuth(
|
|
228
199
|
domain: string
|
|
229
200
|
): Promise<AuthCookie[] | null> {
|
|
230
|
-
// Try registrable domain key first (new normalized format), then exact domain
|
|
231
201
|
const regDomain = getRegistrableDomain(domain);
|
|
232
202
|
const keysToTry = [`auth:${regDomain}`];
|
|
233
203
|
if (domain !== regDomain) keysToTry.push(`auth:${domain}`);
|
|
@@ -263,22 +233,13 @@ export async function getStoredAuth(
|
|
|
263
233
|
* Fallback chain:
|
|
264
234
|
* 1. Vault cookies (fast path)
|
|
265
235
|
* 2. Auto-extract from Chrome/Firefox SQLite (bird pattern — always fresh)
|
|
266
|
-
*
|
|
267
|
-
* This ensures cookies are available without requiring the user to manually
|
|
268
|
-
* call /v1/auth/steal first.
|
|
269
236
|
*/
|
|
270
237
|
export async function getAuthCookies(
|
|
271
|
-
domain: string
|
|
272
|
-
opts?: {
|
|
273
|
-
autoExtract?: boolean;
|
|
274
|
-
},
|
|
238
|
+
domain: string
|
|
275
239
|
): Promise<AuthCookie[] | null> {
|
|
276
|
-
// 1. Try vault (fast)
|
|
277
240
|
const vaultCookies = await getStoredAuth(domain);
|
|
278
241
|
if (vaultCookies && vaultCookies.length > 0) return vaultCookies;
|
|
279
|
-
if (!opts?.autoExtract) return null;
|
|
280
242
|
|
|
281
|
-
// 2. Auto-extract from browser (bird pattern)
|
|
282
243
|
log("auth", `no vault cookies for ${domain} — auto-extracting from browser`);
|
|
283
244
|
try {
|
|
284
245
|
const result = await extractBrowserAuth(domain);
|