unbrowse 3.1.0-experiments.995f8bb → 3.1.0-experiments.9cbcb13
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/dist/cli.js +79 -33
- package/dist/index.js +2 -6
- package/dist/mcp.js +73 -22
- package/dist/server.js +25994 -0
- package/package.json +1 -2
- package/vendor/kuri/manifest.json +1 -1
- package/runtime-src/agent-outcome.ts +0 -166
- package/runtime-src/analytics-session.ts +0 -55
- package/runtime-src/api/browse-index.ts +0 -343
- package/runtime-src/api/browse-session.ts +0 -572
- package/runtime-src/api/browse-submit-prereqs.ts +0 -48
- package/runtime-src/api/browse-submit.ts +0 -1184
- package/runtime-src/api/routes.ts +0 -1833
- package/runtime-src/auth/browser-cookies.ts +0 -423
- package/runtime-src/auth/index.ts +0 -535
- package/runtime-src/auth/runtime.ts +0 -116
- package/runtime-src/browser/index.ts +0 -659
- package/runtime-src/browser/types.ts +0 -41
- package/runtime-src/build-info.generated.ts +0 -6
- package/runtime-src/capture/index.ts +0 -1929
- package/runtime-src/capture/prefetch.ts +0 -95
- package/runtime-src/capture/rsc.ts +0 -45
- package/runtime-src/cli/shortcuts.ts +0 -273
- package/runtime-src/cli.ts +0 -1734
- package/runtime-src/client/graph-client.ts +0 -100
- package/runtime-src/client/index.ts +0 -1425
- package/runtime-src/debug-trace.ts +0 -18
- package/runtime-src/domain.ts +0 -38
- package/runtime-src/execution/index.ts +0 -3407
- package/runtime-src/execution/retry.ts +0 -46
- package/runtime-src/execution/robots.ts +0 -167
- package/runtime-src/execution/search-forms.ts +0 -188
- package/runtime-src/execution/token-resolver.ts +0 -135
- package/runtime-src/extraction/index.ts +0 -1507
- package/runtime-src/foundry/publish-bundle.ts +0 -392
- package/runtime-src/graph/agent-augment.ts +0 -315
- package/runtime-src/graph/index.ts +0 -1532
- package/runtime-src/graph/local-fixtures.ts +0 -393
- package/runtime-src/graph/local-harness.ts +0 -646
- package/runtime-src/graph/planner.ts +0 -411
- package/runtime-src/graph/session.ts +0 -294
- package/runtime-src/graph/trace-store.ts +0 -136
- package/runtime-src/impact-log.ts +0 -227
- package/runtime-src/index.ts +0 -24
- package/runtime-src/indexer/index.ts +0 -465
- package/runtime-src/intent-match.ts +0 -1515
- package/runtime-src/kuri/client.ts +0 -1839
- package/runtime-src/logger.ts +0 -30
- package/runtime-src/marketplace/index.ts +0 -111
- package/runtime-src/mcp.ts +0 -1911
- package/runtime-src/orchestrator/browser-agent.ts +0 -374
- package/runtime-src/orchestrator/dag-advisor.ts +0 -59
- package/runtime-src/orchestrator/dag-feedback.ts +0 -257
- package/runtime-src/orchestrator/first-pass-action.ts +0 -403
- package/runtime-src/orchestrator/index.ts +0 -4480
- package/runtime-src/orchestrator/passive-publish.ts +0 -187
- package/runtime-src/orchestrator/timing-economics.ts +0 -80
- package/runtime-src/payments/cascade.ts +0 -137
- package/runtime-src/payments/index.ts +0 -270
- package/runtime-src/payments/lobster-pay.ts +0 -182
- package/runtime-src/payments/wallet.ts +0 -98
- package/runtime-src/publish/review-context.ts +0 -93
- package/runtime-src/publish/sanitize.ts +0 -197
- package/runtime-src/publish/schema-review.ts +0 -192
- package/runtime-src/publish-admission.ts +0 -388
- package/runtime-src/ratelimit/index.ts +0 -23
- package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
- package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
- package/runtime-src/reverse-engineer/index.ts +0 -1551
- package/runtime-src/reverse-engineer/token-sources.ts +0 -379
- package/runtime-src/router.ts +0 -17
- package/runtime-src/routing-telemetry.ts +0 -395
- package/runtime-src/runtime/browser-access.ts +0 -11
- package/runtime-src/runtime/browser-auth.ts +0 -12
- package/runtime-src/runtime/browser-host.ts +0 -48
- package/runtime-src/runtime/lifecycle.ts +0 -17
- package/runtime-src/runtime/local-server.ts +0 -311
- package/runtime-src/runtime/paths.ts +0 -99
- package/runtime-src/runtime/setup.ts +0 -251
- package/runtime-src/runtime/supervisor.ts +0 -69
- package/runtime-src/runtime/update-hints.ts +0 -351
- package/runtime-src/server.ts +0 -100
- package/runtime-src/session-logs.ts +0 -142
- package/runtime-src/settings.ts +0 -221
- package/runtime-src/single-binary.ts +0 -143
- package/runtime-src/site-policy.ts +0 -54
- package/runtime-src/stale-cleanup-runner.ts +0 -144
- package/runtime-src/stale-cleanup.ts +0 -133
- package/runtime-src/telemetry-attribution.ts +0 -120
- package/runtime-src/telemetry.ts +0 -253
- package/runtime-src/template-params.ts +0 -141
- package/runtime-src/transform/drift.ts +0 -60
- package/runtime-src/transform/index.ts +0 -277
- package/runtime-src/types/index.ts +0 -1
- package/runtime-src/types/skill.ts +0 -931
- package/runtime-src/vault/index.ts +0 -196
- package/runtime-src/verification/auth-gate.ts +0 -8
- package/runtime-src/verification/candidates.ts +0 -27
- package/runtime-src/verification/index.ts +0 -120
- package/runtime-src/verification/matrix.ts +0 -30
- package/runtime-src/version.ts +0 -148
- package/runtime-src/workflow/artifact.ts +0 -161
- package/runtime-src/workflow/compile.ts +0 -808
- package/runtime-src/workflow/publish.ts +0 -225
- package/runtime-src/workflow/runtime.ts +0 -213
|
@@ -1,535 +0,0 @@
|
|
|
1
|
-
import * as kuri from "../kuri/client.js";
|
|
2
|
-
import { storeCredential, getCredential, deleteCredential } from "../vault/index.js";
|
|
3
|
-
import { nanoid } from "nanoid";
|
|
4
|
-
import { isDomainMatch, getRegistrableDomain } from "../domain.js";
|
|
5
|
-
import { log } from "../logger.js";
|
|
6
|
-
import type { ExtractBrowserCookiesOptions } from "./browser-cookies.js";
|
|
7
|
-
import path from "node:path";
|
|
8
|
-
import os from "node:os";
|
|
9
|
-
import fs from "node:fs";
|
|
10
|
-
import { getDefaultLoginConfig } from "../runtime/supervisor.js";
|
|
11
|
-
|
|
12
|
-
const LOGIN_TIMEOUT_MS = 300_000;
|
|
13
|
-
const POLL_INTERVAL_MS = 2_000;
|
|
14
|
-
const MIN_WAIT_MS = 15_000;
|
|
15
|
-
const LOGIN_PATHS = /\/(login|signin|sign-in|sso|auth|oauth|uas\/login|checkpoint)/i;
|
|
16
|
-
const CLOUDFLARE_TEXT = /just a moment|attention required|verify you are human|cloudflare/i;
|
|
17
|
-
const DISABLE_BROWSER_COOKIE_IMPORT = /^(0|false|no|off)$/i;
|
|
18
|
-
const GENERIC_AUTH_COOKIE_NAMES = /(^|[_\-.])(sess(?:ion)?(?:id)?|auth|token|csrf|xsrf|jwt|sid|sso|remember|logged[_-]?in|connect\.sid)([_\-.]|$)/i;
|
|
19
|
-
const DOMAIN_AUTH_COOKIE_NAMES: Record<string, string[]> = {
|
|
20
|
-
"linkedin.com": ["li_at"],
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type CookieLike = Pick<kuri.KuriCookie, "name" | "domain" | "httpOnly" | "secure" | "expires">;
|
|
24
|
-
|
|
25
|
-
function formatAuthError(error: unknown): string {
|
|
26
|
-
if (error instanceof Error && error.message) return error.message;
|
|
27
|
-
if (typeof error === "string") return error;
|
|
28
|
-
try {
|
|
29
|
-
return JSON.stringify(error);
|
|
30
|
-
} catch {
|
|
31
|
-
return String(error);
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function normalizeAuthDomain(domain: string): string {
|
|
36
|
-
return domain.toLowerCase().replace(/^www\./, "");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function shouldImportBrowserCookies(): boolean {
|
|
40
|
-
const raw = process.env.UNBROWSE_IMPORT_BROWSER_COOKIES?.trim();
|
|
41
|
-
if (!raw) return true;
|
|
42
|
-
return !DISABLE_BROWSER_COOKIE_IMPORT.test(raw);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function isLikelyAuthenticatedCookie(targetDomain: string, cookie: Pick<CookieLike, "name">): boolean {
|
|
46
|
-
const cookieName = cookie.name.toLowerCase();
|
|
47
|
-
const registrableDomain = getRegistrableDomain(normalizeAuthDomain(targetDomain));
|
|
48
|
-
const requiredCookieNames = DOMAIN_AUTH_COOKIE_NAMES[registrableDomain];
|
|
49
|
-
if (requiredCookieNames) return requiredCookieNames.includes(cookieName);
|
|
50
|
-
return GENERIC_AUTH_COOKIE_NAMES.test(cookieName);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function getAuthenticatedCookiesForDomain(
|
|
54
|
-
targetDomain: string,
|
|
55
|
-
cookies: CookieLike[],
|
|
56
|
-
): CookieLike[] {
|
|
57
|
-
return cookies.filter((cookie) =>
|
|
58
|
-
isDomainMatch(cookie.domain, targetDomain) && isLikelyAuthenticatedCookie(targetDomain, cookie)
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function importBrowserCookiesIntoTab(tabId: string, domain: string): Promise<number> {
|
|
63
|
-
if (!shouldImportBrowserCookies()) return 0;
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const { extractBrowserCookies } = await import("./browser-cookies.js");
|
|
67
|
-
const { cookies } = extractBrowserCookies(domain);
|
|
68
|
-
let imported = 0;
|
|
69
|
-
|
|
70
|
-
for (const cookie of cookies) {
|
|
71
|
-
try {
|
|
72
|
-
await kuri.setCookie(tabId, cookie);
|
|
73
|
-
imported += 1;
|
|
74
|
-
} catch (error) {
|
|
75
|
-
log(
|
|
76
|
-
"auth",
|
|
77
|
-
`browser_cookie_import_failed domain=${normalizeAuthDomain(domain)} tab_id=${tabId} cookie=${cookie.name} error=${formatAuthError(error)}`,
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (imported > 0) {
|
|
83
|
-
log("auth", `browser_cookie_imported domain=${normalizeAuthDomain(domain)} tab_id=${tabId} count=${imported}`);
|
|
84
|
-
}
|
|
85
|
-
return imported;
|
|
86
|
-
} catch (error) {
|
|
87
|
-
log(
|
|
88
|
-
"auth",
|
|
89
|
-
`browser_cookie_extract_failed domain=${normalizeAuthDomain(domain)} tab_id=${tabId} error=${formatAuthError(error)}`,
|
|
90
|
-
);
|
|
91
|
-
return 0;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function saveAuthProfileBestEffort(
|
|
96
|
-
tabId: string,
|
|
97
|
-
domain: string,
|
|
98
|
-
context = "auth",
|
|
99
|
-
): Promise<boolean> {
|
|
100
|
-
const profileName = normalizeAuthDomain(domain);
|
|
101
|
-
if (!profileName || profileName === "unknown") return false;
|
|
102
|
-
try {
|
|
103
|
-
await kuri.authProfileSave(tabId, profileName);
|
|
104
|
-
return true;
|
|
105
|
-
} catch (error) {
|
|
106
|
-
log(
|
|
107
|
-
"auth",
|
|
108
|
-
`${context} auth_profile_failed op=save domain=${profileName} tab_id=${tabId} error=${formatAuthError(error)}`,
|
|
109
|
-
);
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function loadAuthProfileBestEffort(
|
|
115
|
-
tabId: string,
|
|
116
|
-
domain: string,
|
|
117
|
-
context = "auth",
|
|
118
|
-
): Promise<boolean> {
|
|
119
|
-
const profileName = normalizeAuthDomain(domain);
|
|
120
|
-
if (!profileName || profileName === "unknown") return false;
|
|
121
|
-
try {
|
|
122
|
-
await kuri.authProfileLoad(tabId, profileName);
|
|
123
|
-
return true;
|
|
124
|
-
} catch (error) {
|
|
125
|
-
log(
|
|
126
|
-
"auth",
|
|
127
|
-
`${context} auth_profile_failed op=load domain=${profileName} tab_id=${tabId} error=${formatAuthError(error)}`,
|
|
128
|
-
);
|
|
129
|
-
return false;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Returns the persistent profile directory for a given domain.
|
|
135
|
-
* Stored under ~/.unbrowse/profiles/<registrableDomain>.
|
|
136
|
-
*/
|
|
137
|
-
export function getProfilePath(domain: string): string {
|
|
138
|
-
return path.join(os.homedir(), ".unbrowse", "profiles", getRegistrableDomain(domain));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface LoginResult {
|
|
142
|
-
success: boolean;
|
|
143
|
-
domain: string;
|
|
144
|
-
cookies_stored: number;
|
|
145
|
-
error?: string;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export type BrowserAuthImportOptions = ExtractBrowserCookiesOptions;
|
|
149
|
-
|
|
150
|
-
export interface BrowserAuthSourceMeta {
|
|
151
|
-
family?: string;
|
|
152
|
-
userDataDir?: string;
|
|
153
|
-
cookieDbPath?: string;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export interface StoredAuthBundle {
|
|
157
|
-
cookies: AuthCookie[];
|
|
158
|
-
headers: Record<string, string>;
|
|
159
|
-
source_keys: string[];
|
|
160
|
-
source_meta?: BrowserAuthSourceMeta | null;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export type InteractiveLoginAssessment =
|
|
164
|
-
| { status: "pending"; reason: string }
|
|
165
|
-
| { status: "authenticated"; reason: string }
|
|
166
|
-
| { status: "blocked"; reason: string };
|
|
167
|
-
|
|
168
|
-
export function assessInteractiveLoginState(input: {
|
|
169
|
-
currentUrl: string;
|
|
170
|
-
targetDomain: string;
|
|
171
|
-
initialCookieCount: number;
|
|
172
|
-
currentCookieCount: number;
|
|
173
|
-
currentCookies?: CookieLike[];
|
|
174
|
-
hasCloudflareChallenge?: boolean;
|
|
175
|
-
pageText?: string;
|
|
176
|
-
}): InteractiveLoginAssessment {
|
|
177
|
-
let parsed: URL;
|
|
178
|
-
try {
|
|
179
|
-
parsed = new URL(input.currentUrl);
|
|
180
|
-
} catch {
|
|
181
|
-
return { status: "pending", reason: "invalid_url" };
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const currentDomain = parsed.hostname.toLowerCase();
|
|
185
|
-
const targetNorm = input.targetDomain.toLowerCase();
|
|
186
|
-
const isOnTarget = currentDomain === targetNorm || currentDomain.endsWith(`.${targetNorm}`);
|
|
187
|
-
if (!isOnTarget) return { status: "pending", reason: "off_target_domain" };
|
|
188
|
-
|
|
189
|
-
if (input.hasCloudflareChallenge) return { status: "blocked", reason: "cloudflare_challenge" };
|
|
190
|
-
if (input.pageText && CLOUDFLARE_TEXT.test(input.pageText)) return { status: "blocked", reason: "cloudflare_text" };
|
|
191
|
-
if (LOGIN_PATHS.test(parsed.pathname)) return { status: "pending", reason: "still_on_login_path" };
|
|
192
|
-
|
|
193
|
-
const authCookies = getAuthenticatedCookiesForDomain(targetNorm, input.currentCookies ?? []);
|
|
194
|
-
if (authCookies.length > 0) {
|
|
195
|
-
return { status: "authenticated", reason: "auth_cookies_present_on_target" };
|
|
196
|
-
}
|
|
197
|
-
if ((input.currentCookies?.length ?? 0) > 0) {
|
|
198
|
-
return { status: "pending", reason: "non_auth_cookies_only" };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!input.currentCookies && input.currentCookieCount > input.initialCookieCount) {
|
|
202
|
-
return { status: "authenticated", reason: "new_cookies_on_target" };
|
|
203
|
-
}
|
|
204
|
-
if (!input.currentCookies && input.currentCookieCount > 0) {
|
|
205
|
-
return { status: "authenticated", reason: "cookies_present_on_target" };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return { status: "pending", reason: "no_session_cookies" };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function storedAuthNeedsBrowserRefresh(bundle: StoredAuthBundle | null | undefined): boolean {
|
|
212
|
-
if (!bundle) return true;
|
|
213
|
-
if (bundle.cookies.length === 0 && Object.keys(bundle.headers).length === 0) return true;
|
|
214
|
-
const sourceMeta = bundle.source_meta;
|
|
215
|
-
if (!sourceMeta) return true;
|
|
216
|
-
if (sourceMeta.family === "chromium" && !sourceMeta.userDataDir && !sourceMeta.cookieDbPath) {
|
|
217
|
-
return true;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Open a visible browser for the user to complete login.
|
|
223
|
-
* Uses Kuri to manage the browser tab, polls for login completion via cookies.
|
|
224
|
-
*
|
|
225
|
-
* Note: Kuri manages Chrome — for interactive login, the user's Chrome
|
|
226
|
-
* needs to be visible. We navigate to the login URL and poll for cookie changes.
|
|
227
|
-
*/
|
|
228
|
-
export async function interactiveLogin(
|
|
229
|
-
url: string,
|
|
230
|
-
domain?: string,
|
|
231
|
-
): Promise<LoginResult> {
|
|
232
|
-
const targetDomain = domain ?? new URL(url).hostname;
|
|
233
|
-
const profileDir = getProfilePath(targetDomain);
|
|
234
|
-
|
|
235
|
-
const isHeadless = process.env.HEADLESS === "true" || process.env.HEADLESS === "1";
|
|
236
|
-
const loginConfig = getDefaultLoginConfig(isHeadless);
|
|
237
|
-
log("auth", `interactiveLogin — url: ${url}, domain: ${targetDomain}, interactive: ${loginConfig.interactive}, timeout: ${loginConfig.timeout_ms}ms`);
|
|
238
|
-
|
|
239
|
-
// Login requires a visible browser — disable headless for this flow
|
|
240
|
-
const prevHeadless = process.env.HEADLESS;
|
|
241
|
-
process.env.HEADLESS = "false";
|
|
242
|
-
|
|
243
|
-
try {
|
|
244
|
-
fs.mkdirSync(profileDir, { recursive: true });
|
|
245
|
-
|
|
246
|
-
// Stop any existing headless Kuri so it restarts with HEADLESS=false
|
|
247
|
-
try { await kuri.stop(); } catch { /* may not be running */ }
|
|
248
|
-
|
|
249
|
-
// Start Kuri and get a tab
|
|
250
|
-
await kuri.start();
|
|
251
|
-
const tabId = await kuri.getDefaultTab();
|
|
252
|
-
await kuri.networkEnable(tabId);
|
|
253
|
-
|
|
254
|
-
// Navigate to login URL
|
|
255
|
-
await kuri.navigate(tabId, url);
|
|
256
|
-
|
|
257
|
-
const startTime = Date.now();
|
|
258
|
-
|
|
259
|
-
// Snapshot initial cookies
|
|
260
|
-
const initialCookies = await kuri.getCookies(tabId);
|
|
261
|
-
const initialCookieCount = initialCookies.filter((c) => isDomainMatch(c.domain, targetDomain)).length;
|
|
262
|
-
log("auth", `initial cookies for ${targetDomain}: ${initialCookieCount}`);
|
|
263
|
-
|
|
264
|
-
// Wait for user to complete login — detect via cookie changes + URL change
|
|
265
|
-
let loggedIn = false;
|
|
266
|
-
let blockedReason: string | null = null;
|
|
267
|
-
let lastLoggedUrl = "";
|
|
268
|
-
const effectiveTimeout = loginConfig.interactive ? LOGIN_TIMEOUT_MS : loginConfig.timeout_ms;
|
|
269
|
-
while (Date.now() - startTime < effectiveTimeout) {
|
|
270
|
-
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
|
271
|
-
const elapsed = Date.now() - startTime;
|
|
272
|
-
|
|
273
|
-
try {
|
|
274
|
-
const currentUrl = await kuri.getCurrentUrl(tabId);
|
|
275
|
-
if (currentUrl !== lastLoggedUrl) {
|
|
276
|
-
log("auth", `navigated to: ${currentUrl}`);
|
|
277
|
-
lastLoggedUrl = currentUrl;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (elapsed < MIN_WAIT_MS) continue;
|
|
281
|
-
|
|
282
|
-
const currentCookies = await kuri.getCookies(tabId);
|
|
283
|
-
const domainCookies = currentCookies.filter((c) => isDomainMatch(c.domain, targetDomain));
|
|
284
|
-
const currentCookieCount = domainCookies.length;
|
|
285
|
-
const hasCloudflareChallenge = await kuri.hasCloudflareChallenge(tabId).catch(() => false);
|
|
286
|
-
const pageText = hasCloudflareChallenge ? await kuri.getText(tabId).catch(() => "") : "";
|
|
287
|
-
const assessment = assessInteractiveLoginState({
|
|
288
|
-
currentUrl,
|
|
289
|
-
targetDomain,
|
|
290
|
-
initialCookieCount,
|
|
291
|
-
currentCookieCount,
|
|
292
|
-
currentCookies: domainCookies,
|
|
293
|
-
hasCloudflareChallenge,
|
|
294
|
-
pageText,
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
if (assessment.status === "authenticated") {
|
|
298
|
-
loggedIn = true;
|
|
299
|
-
log("auth", `login complete — ${currentUrl} (cookies: ${initialCookieCount} → ${currentCookieCount}; ${assessment.reason})`);
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (assessment.status === "blocked") {
|
|
304
|
-
blockedReason = assessment.reason;
|
|
305
|
-
log("auth", `login blocked — ${currentUrl} (${assessment.reason})`);
|
|
306
|
-
}
|
|
307
|
-
} catch { /* page navigating */ }
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
if (!loggedIn) {
|
|
311
|
-
log("auth", `login wait ended after ${Math.round((Date.now() - startTime) / 1000)}s — fallback: ${loginConfig.fallback_strategy}`);
|
|
312
|
-
if (loginConfig.fallback_strategy === "fail") {
|
|
313
|
-
const error = blockedReason
|
|
314
|
-
? `Login blocked (${blockedReason})`
|
|
315
|
-
: "Login timed out (fallback: fail)";
|
|
316
|
-
return { success: false, domain: targetDomain, cookies_stored: 0, error };
|
|
317
|
-
}
|
|
318
|
-
if (loginConfig.fallback_strategy === "skip") {
|
|
319
|
-
log("auth", `skipping cookie capture per fallback_strategy`);
|
|
320
|
-
return { success: false, domain: targetDomain, cookies_stored: 0, error: "Login skipped (headless)" };
|
|
321
|
-
}
|
|
322
|
-
// fallback_strategy === "prompt" — continue to capture cookies anyway
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// Extract and store cookies
|
|
326
|
-
const cookies = await kuri.getCookies(tabId);
|
|
327
|
-
const domainCookies = cookies.filter((c) => isDomainMatch(c.domain, targetDomain));
|
|
328
|
-
|
|
329
|
-
if (domainCookies.length === 0) {
|
|
330
|
-
return { success: false, domain: targetDomain, cookies_stored: 0, error: "No cookies captured for domain" };
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const storableCookies = domainCookies.map((c) => ({
|
|
334
|
-
name: c.name, value: c.value, domain: c.domain, path: c.path,
|
|
335
|
-
secure: c.secure, httpOnly: c.httpOnly, sameSite: c.sameSite, expires: c.expires,
|
|
336
|
-
}));
|
|
337
|
-
|
|
338
|
-
const vaultKey = `auth:${getRegistrableDomain(targetDomain)}`;
|
|
339
|
-
await storeCredential(vaultKey, JSON.stringify({ cookies: storableCookies }));
|
|
340
|
-
log("auth", `stored ${storableCookies.length} cookies under ${vaultKey}`);
|
|
341
|
-
|
|
342
|
-
// Also save as Kuri auth profile so browse commands (go/snap/click) have auth
|
|
343
|
-
if (await saveAuthProfileBestEffort(tabId, targetDomain, "interactive_login")) {
|
|
344
|
-
log("auth", `saved Kuri auth profile for ${targetDomain}`);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
return { success: true, domain: targetDomain, cookies_stored: storableCookies.length };
|
|
348
|
-
} finally {
|
|
349
|
-
// Restore headless setting so subsequent captures run headless
|
|
350
|
-
if (prevHeadless !== undefined) process.env.HEADLESS = prevHeadless;
|
|
351
|
-
else delete process.env.HEADLESS;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Extract cookies directly from Chrome/Firefox SQLite databases.
|
|
357
|
-
* No browser launch needed, Chrome can stay open.
|
|
358
|
-
*/
|
|
359
|
-
export async function extractBrowserAuth(
|
|
360
|
-
domain: string,
|
|
361
|
-
opts?: BrowserAuthImportOptions
|
|
362
|
-
): Promise<LoginResult> {
|
|
363
|
-
const { extractBrowserCookies } = await import("./browser-cookies.js");
|
|
364
|
-
|
|
365
|
-
const result = extractBrowserCookies(domain, opts);
|
|
366
|
-
|
|
367
|
-
if (result.cookies.length === 0) {
|
|
368
|
-
return {
|
|
369
|
-
success: false,
|
|
370
|
-
domain,
|
|
371
|
-
cookies_stored: 0,
|
|
372
|
-
error: result.warnings.join("; ") || "No cookies found in any browser",
|
|
373
|
-
};
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
const storableCookies = result.cookies.map((c) => ({
|
|
377
|
-
name: c.name,
|
|
378
|
-
value: c.value,
|
|
379
|
-
domain: c.domain,
|
|
380
|
-
path: c.path,
|
|
381
|
-
secure: c.secure,
|
|
382
|
-
httpOnly: c.httpOnly,
|
|
383
|
-
sameSite: c.sameSite,
|
|
384
|
-
expires: c.expires,
|
|
385
|
-
}));
|
|
386
|
-
|
|
387
|
-
const vaultKey = `auth:${getRegistrableDomain(domain)}`;
|
|
388
|
-
await storeCredential(
|
|
389
|
-
vaultKey,
|
|
390
|
-
JSON.stringify({ cookies: storableCookies })
|
|
391
|
-
);
|
|
392
|
-
|
|
393
|
-
log("auth", `stored ${storableCookies.length} cookies for ${domain} (key: ${vaultKey}) from ${result.source}`);
|
|
394
|
-
return { success: true, domain, cookies_stored: storableCookies.length };
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export async function loginWithBrowserFallback(
|
|
398
|
-
url: string,
|
|
399
|
-
opts?: BrowserAuthImportOptions,
|
|
400
|
-
deps: {
|
|
401
|
-
extractBrowserAuth?: typeof extractBrowserAuth;
|
|
402
|
-
interactiveLogin?: typeof interactiveLogin;
|
|
403
|
-
} = {},
|
|
404
|
-
): Promise<LoginResult> {
|
|
405
|
-
const extract = deps.extractBrowserAuth ?? extractBrowserAuth;
|
|
406
|
-
const login = deps.interactiveLogin ?? interactiveLogin;
|
|
407
|
-
const domain = new URL(url).hostname;
|
|
408
|
-
|
|
409
|
-
const extracted = await extract(domain, opts);
|
|
410
|
-
if (extracted.success && extracted.cookies_stored > 0) {
|
|
411
|
-
log("auth", `login_with_browser_fallback domain=${domain} source=browser_cookies cookies=${extracted.cookies_stored}`);
|
|
412
|
-
return extracted;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
log(
|
|
416
|
-
"auth",
|
|
417
|
-
`login_with_browser_fallback domain=${domain} source=interactive reason=${extracted.error ?? "no_browser_cookies"}`,
|
|
418
|
-
);
|
|
419
|
-
return login(url);
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
type AuthCookie = {
|
|
423
|
-
name: string;
|
|
424
|
-
value: string;
|
|
425
|
-
domain: string;
|
|
426
|
-
path?: string;
|
|
427
|
-
secure?: boolean;
|
|
428
|
-
httpOnly?: boolean;
|
|
429
|
-
sameSite?: string;
|
|
430
|
-
expires?: number;
|
|
431
|
-
};
|
|
432
|
-
|
|
433
|
-
/** Filter out expired cookies. Session cookies (expires <= 0) are kept. */
|
|
434
|
-
function filterExpired(cookies: AuthCookie[]): AuthCookie[] {
|
|
435
|
-
const now = Math.floor(Date.now() / 1000);
|
|
436
|
-
return cookies.filter((c) => {
|
|
437
|
-
if (c.expires == null || c.expires <= 0) return true;
|
|
438
|
-
return c.expires > now;
|
|
439
|
-
});
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Retrieve stored auth cookies for a domain from the vault.
|
|
444
|
-
* Filters out expired cookies automatically.
|
|
445
|
-
*/
|
|
446
|
-
export async function getStoredAuth(
|
|
447
|
-
domain: string
|
|
448
|
-
): Promise<AuthCookie[] | null> {
|
|
449
|
-
const bundle = await getStoredAuthBundle(domain);
|
|
450
|
-
return bundle?.cookies?.length ? bundle.cookies : null;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Retrieve the stored auth bundle for a domain from the vault.
|
|
455
|
-
* Preserves headers/source metadata while filtering expired cookies.
|
|
456
|
-
*/
|
|
457
|
-
export async function getStoredAuthBundle(
|
|
458
|
-
domain: string
|
|
459
|
-
): Promise<StoredAuthBundle | null> {
|
|
460
|
-
const regDomain = getRegistrableDomain(domain);
|
|
461
|
-
const keysToTry = [`auth:${regDomain}`];
|
|
462
|
-
if (domain !== regDomain) keysToTry.push(`auth:${domain}`);
|
|
463
|
-
|
|
464
|
-
for (const key of keysToTry) {
|
|
465
|
-
const stored = await getCredential(key);
|
|
466
|
-
if (!stored) continue;
|
|
467
|
-
try {
|
|
468
|
-
const parsed = JSON.parse(stored) as Partial<StoredAuthBundle> & { cookies?: AuthCookie[] };
|
|
469
|
-
const cookies = parsed.cookies ?? [];
|
|
470
|
-
const valid = filterExpired(cookies);
|
|
471
|
-
if (cookies.length > 0 && valid.length === 0 && Object.keys(parsed.headers ?? {}).length === 0) {
|
|
472
|
-
log("auth", `all ${cookies.length} cookies for ${domain} (key: ${key}) are expired — deleting`);
|
|
473
|
-
await deleteCredential(key);
|
|
474
|
-
continue;
|
|
475
|
-
}
|
|
476
|
-
if (valid.length < cookies.length) {
|
|
477
|
-
log("auth", `filtered ${cookies.length - valid.length} expired cookies for ${domain}`);
|
|
478
|
-
}
|
|
479
|
-
return {
|
|
480
|
-
cookies: valid,
|
|
481
|
-
headers: parsed.headers ?? {},
|
|
482
|
-
source_keys: parsed.source_keys ?? [],
|
|
483
|
-
source_meta: parsed.source_meta ?? null,
|
|
484
|
-
};
|
|
485
|
-
} catch {
|
|
486
|
-
continue;
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return null;
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Bird-style unified cookie resolution with auto-extract fallback.
|
|
495
|
-
*
|
|
496
|
-
* Fallback chain:
|
|
497
|
-
* 1. Vault cookies (fast path)
|
|
498
|
-
* 2. Auto-extract from Chrome/Firefox SQLite (bird pattern — always fresh)
|
|
499
|
-
*/
|
|
500
|
-
export async function getAuthCookies(
|
|
501
|
-
domain: string
|
|
502
|
-
): Promise<AuthCookie[] | null> {
|
|
503
|
-
const vaultCookies = await getStoredAuth(domain);
|
|
504
|
-
if (vaultCookies && vaultCookies.length > 0) return vaultCookies;
|
|
505
|
-
|
|
506
|
-
log("auth", `no vault cookies for ${domain} — auto-extracting from browser`);
|
|
507
|
-
try {
|
|
508
|
-
const result = await extractBrowserAuth(domain);
|
|
509
|
-
if (result.success && result.cookies_stored > 0) {
|
|
510
|
-
return getStoredAuth(domain);
|
|
511
|
-
}
|
|
512
|
-
} catch (err) {
|
|
513
|
-
log("auth", `browser auto-extract failed for ${domain}: ${err instanceof Error ? err.message : err}`);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
return null;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
/**
|
|
520
|
-
* Refresh credentials from browser after a 401/403.
|
|
521
|
-
* Returns true if fresh cookies were stored.
|
|
522
|
-
*/
|
|
523
|
-
export async function refreshAuthFromBrowser(domain: string): Promise<boolean> {
|
|
524
|
-
log("auth", `401/403 received — attempting to refresh auth for ${domain} from browser`);
|
|
525
|
-
try {
|
|
526
|
-
const result = await extractBrowserAuth(domain);
|
|
527
|
-
if (result.success && result.cookies_stored > 0) {
|
|
528
|
-
log("auth", `refreshed ${result.cookies_stored} cookies for ${domain} from browser`);
|
|
529
|
-
return true;
|
|
530
|
-
}
|
|
531
|
-
} catch (err) {
|
|
532
|
-
log("auth", `browser refresh failed for ${domain}: ${err instanceof Error ? err.message : err}`);
|
|
533
|
-
}
|
|
534
|
-
return false;
|
|
535
|
-
}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
export type AuthStrategy = "login_if_needed" | "ensure_account" | "refresh_session" | "none";
|
|
2
|
-
|
|
3
|
-
export interface AuthDependency {
|
|
4
|
-
domain: string;
|
|
5
|
-
strategy: AuthStrategy;
|
|
6
|
-
login_url?: string;
|
|
7
|
-
session_check_url?: string;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface AuthResult {
|
|
11
|
-
authenticated: boolean;
|
|
12
|
-
session_token?: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface AuthRuntime {
|
|
16
|
-
resolveAuth(dep: AuthDependency): Promise<AuthResult>;
|
|
17
|
-
isSessionValid(domain: string): Promise<boolean>;
|
|
18
|
-
refreshSession(domain: string): Promise<boolean>;
|
|
19
|
-
loginIfNeeded(domain: string, loginUrl?: string): Promise<boolean>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Stub auth runtime — checks local session store only.
|
|
24
|
-
* Interactive login (browser-based) to be wired in a follow-up.
|
|
25
|
-
*/
|
|
26
|
-
export class LocalAuthRuntime implements AuthRuntime {
|
|
27
|
-
private sessions = new Map<string, { token: string; expires: number }>();
|
|
28
|
-
|
|
29
|
-
async resolveAuth(dep: AuthDependency): Promise<AuthResult> {
|
|
30
|
-
if (dep.strategy === "none") return { authenticated: true };
|
|
31
|
-
|
|
32
|
-
const session = this.sessions.get(dep.domain);
|
|
33
|
-
if (session && session.expires > Date.now()) {
|
|
34
|
-
return { authenticated: true, session_token: session.token };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (dep.strategy === "refresh_session" && session) {
|
|
38
|
-
const refreshed = await this.refreshSession(dep.domain);
|
|
39
|
-
if (refreshed) {
|
|
40
|
-
const updated = this.sessions.get(dep.domain);
|
|
41
|
-
return { authenticated: true, session_token: updated?.token };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return { authenticated: false };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async isSessionValid(domain: string): Promise<boolean> {
|
|
49
|
-
const session = this.sessions.get(domain);
|
|
50
|
-
return !!session && session.expires > Date.now();
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async refreshSession(domain: string): Promise<boolean> {
|
|
54
|
-
const session = this.sessions.get(domain);
|
|
55
|
-
if (session) {
|
|
56
|
-
session.expires = Date.now() + 3600_000;
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async loginIfNeeded(domain: string, _loginUrl?: string): Promise<boolean> {
|
|
63
|
-
// Try session refresh first — cheap path
|
|
64
|
-
const refreshed = await this.refreshSession(domain);
|
|
65
|
-
if (refreshed) return true;
|
|
66
|
-
// Full interactive login not yet implemented in LocalAuthRuntime.
|
|
67
|
-
// Browser-based login will be wired through the auth/index.ts flow.
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
setSession(domain: string, token: string, ttlMs: number = 3600_000) {
|
|
72
|
-
this.sessions.set(domain, { token, expires: Date.now() + ttlMs });
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export const authRuntime: AuthRuntime = new LocalAuthRuntime();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Resolve a batch of auth dependencies using the singleton runtime.
|
|
81
|
-
* Used by the orchestrator auth prerequisite detection before endpoint execution.
|
|
82
|
-
* Returns one AuthResult per dependency, in the same order.
|
|
83
|
-
*/
|
|
84
|
-
export async function resolveAuthPrerequisites(
|
|
85
|
-
deps: AuthDependency[],
|
|
86
|
-
): Promise<AuthResult[]> {
|
|
87
|
-
return Promise.all(deps.map((dep) => authRuntime.resolveAuth(dep)));
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Derive auth dependencies from a skill manifest endpoints.
|
|
92
|
-
* Inspects semantic.auth_required and auth_profile_ref to surface
|
|
93
|
-
* which domains need authentication and what strategy to use.
|
|
94
|
-
*/
|
|
95
|
-
export function deriveAuthDependencies(
|
|
96
|
-
skill: { domain: string; auth_profile_ref?: string; endpoints: Array<{ endpoint_id: string; semantic?: { auth_required?: boolean } }> },
|
|
97
|
-
targetEndpointId?: string,
|
|
98
|
-
): AuthDependency[] {
|
|
99
|
-
const endpoints = targetEndpointId
|
|
100
|
-
? skill.endpoints.filter((ep) => ep.endpoint_id === targetEndpointId)
|
|
101
|
-
: skill.endpoints;
|
|
102
|
-
|
|
103
|
-
const needsAuth = endpoints.some(
|
|
104
|
-
(ep) => ep.semantic?.auth_required === true,
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
if (!needsAuth && !skill.auth_profile_ref) return [];
|
|
108
|
-
|
|
109
|
-
return [
|
|
110
|
-
{
|
|
111
|
-
domain: skill.domain,
|
|
112
|
-
strategy: "login_if_needed" as AuthStrategy,
|
|
113
|
-
login_url: `https://${skill.domain}/login`,
|
|
114
|
-
},
|
|
115
|
-
];
|
|
116
|
-
}
|