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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unbrowse",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "Reverse-engineer any website into reusable API skills. npm CLI + local engine.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,5 +1,4 @@
1
- import { BrowserManager } from "agent-browser/dist/browser.js";
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; // Always wait at least 15s so user has time to log in
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
- * Waits up to 120s for navigation back to the target domain, then captures cookies.
33
- * Uses an isolated persistent profile per domain.
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
- await browser.launch({
48
- action: "launch",
49
- id: nanoid(),
50
- headless: false,
51
- profile: profileDir,
52
- userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
53
- });
54
- await executeCommand({ action: "navigate", id: nanoid(), url }, browser);
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 so we can detect new ones after login
60
- const context = browser.getContext();
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 = page.url();
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
- // Check if new cookies appeared (the real signal that login happened)
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 finalContext = browser.getContext();
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
- try {
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; // session cookie
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);