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.
Files changed (105) hide show
  1. package/dist/cli.js +79 -33
  2. package/dist/index.js +2 -6
  3. package/dist/mcp.js +73 -22
  4. package/dist/server.js +25994 -0
  5. package/package.json +1 -2
  6. package/vendor/kuri/manifest.json +1 -1
  7. package/runtime-src/agent-outcome.ts +0 -166
  8. package/runtime-src/analytics-session.ts +0 -55
  9. package/runtime-src/api/browse-index.ts +0 -343
  10. package/runtime-src/api/browse-session.ts +0 -572
  11. package/runtime-src/api/browse-submit-prereqs.ts +0 -48
  12. package/runtime-src/api/browse-submit.ts +0 -1184
  13. package/runtime-src/api/routes.ts +0 -1833
  14. package/runtime-src/auth/browser-cookies.ts +0 -423
  15. package/runtime-src/auth/index.ts +0 -535
  16. package/runtime-src/auth/runtime.ts +0 -116
  17. package/runtime-src/browser/index.ts +0 -659
  18. package/runtime-src/browser/types.ts +0 -41
  19. package/runtime-src/build-info.generated.ts +0 -6
  20. package/runtime-src/capture/index.ts +0 -1929
  21. package/runtime-src/capture/prefetch.ts +0 -95
  22. package/runtime-src/capture/rsc.ts +0 -45
  23. package/runtime-src/cli/shortcuts.ts +0 -273
  24. package/runtime-src/cli.ts +0 -1734
  25. package/runtime-src/client/graph-client.ts +0 -100
  26. package/runtime-src/client/index.ts +0 -1425
  27. package/runtime-src/debug-trace.ts +0 -18
  28. package/runtime-src/domain.ts +0 -38
  29. package/runtime-src/execution/index.ts +0 -3407
  30. package/runtime-src/execution/retry.ts +0 -46
  31. package/runtime-src/execution/robots.ts +0 -167
  32. package/runtime-src/execution/search-forms.ts +0 -188
  33. package/runtime-src/execution/token-resolver.ts +0 -135
  34. package/runtime-src/extraction/index.ts +0 -1507
  35. package/runtime-src/foundry/publish-bundle.ts +0 -392
  36. package/runtime-src/graph/agent-augment.ts +0 -315
  37. package/runtime-src/graph/index.ts +0 -1532
  38. package/runtime-src/graph/local-fixtures.ts +0 -393
  39. package/runtime-src/graph/local-harness.ts +0 -646
  40. package/runtime-src/graph/planner.ts +0 -411
  41. package/runtime-src/graph/session.ts +0 -294
  42. package/runtime-src/graph/trace-store.ts +0 -136
  43. package/runtime-src/impact-log.ts +0 -227
  44. package/runtime-src/index.ts +0 -24
  45. package/runtime-src/indexer/index.ts +0 -465
  46. package/runtime-src/intent-match.ts +0 -1515
  47. package/runtime-src/kuri/client.ts +0 -1839
  48. package/runtime-src/logger.ts +0 -30
  49. package/runtime-src/marketplace/index.ts +0 -111
  50. package/runtime-src/mcp.ts +0 -1911
  51. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  52. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  53. package/runtime-src/orchestrator/dag-feedback.ts +0 -257
  54. package/runtime-src/orchestrator/first-pass-action.ts +0 -403
  55. package/runtime-src/orchestrator/index.ts +0 -4480
  56. package/runtime-src/orchestrator/passive-publish.ts +0 -187
  57. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  58. package/runtime-src/payments/cascade.ts +0 -137
  59. package/runtime-src/payments/index.ts +0 -270
  60. package/runtime-src/payments/lobster-pay.ts +0 -182
  61. package/runtime-src/payments/wallet.ts +0 -98
  62. package/runtime-src/publish/review-context.ts +0 -93
  63. package/runtime-src/publish/sanitize.ts +0 -197
  64. package/runtime-src/publish/schema-review.ts +0 -192
  65. package/runtime-src/publish-admission.ts +0 -388
  66. package/runtime-src/ratelimit/index.ts +0 -23
  67. package/runtime-src/reverse-engineer/bundle-scanner.ts +0 -127
  68. package/runtime-src/reverse-engineer/description-prompt.ts +0 -213
  69. package/runtime-src/reverse-engineer/index.ts +0 -1551
  70. package/runtime-src/reverse-engineer/token-sources.ts +0 -379
  71. package/runtime-src/router.ts +0 -17
  72. package/runtime-src/routing-telemetry.ts +0 -395
  73. package/runtime-src/runtime/browser-access.ts +0 -11
  74. package/runtime-src/runtime/browser-auth.ts +0 -12
  75. package/runtime-src/runtime/browser-host.ts +0 -48
  76. package/runtime-src/runtime/lifecycle.ts +0 -17
  77. package/runtime-src/runtime/local-server.ts +0 -311
  78. package/runtime-src/runtime/paths.ts +0 -99
  79. package/runtime-src/runtime/setup.ts +0 -251
  80. package/runtime-src/runtime/supervisor.ts +0 -69
  81. package/runtime-src/runtime/update-hints.ts +0 -351
  82. package/runtime-src/server.ts +0 -100
  83. package/runtime-src/session-logs.ts +0 -142
  84. package/runtime-src/settings.ts +0 -221
  85. package/runtime-src/single-binary.ts +0 -143
  86. package/runtime-src/site-policy.ts +0 -54
  87. package/runtime-src/stale-cleanup-runner.ts +0 -144
  88. package/runtime-src/stale-cleanup.ts +0 -133
  89. package/runtime-src/telemetry-attribution.ts +0 -120
  90. package/runtime-src/telemetry.ts +0 -253
  91. package/runtime-src/template-params.ts +0 -141
  92. package/runtime-src/transform/drift.ts +0 -60
  93. package/runtime-src/transform/index.ts +0 -277
  94. package/runtime-src/types/index.ts +0 -1
  95. package/runtime-src/types/skill.ts +0 -931
  96. package/runtime-src/vault/index.ts +0 -196
  97. package/runtime-src/verification/auth-gate.ts +0 -8
  98. package/runtime-src/verification/candidates.ts +0 -27
  99. package/runtime-src/verification/index.ts +0 -120
  100. package/runtime-src/verification/matrix.ts +0 -30
  101. package/runtime-src/version.ts +0 -148
  102. package/runtime-src/workflow/artifact.ts +0 -161
  103. package/runtime-src/workflow/compile.ts +0 -808
  104. package/runtime-src/workflow/publish.ts +0 -225
  105. 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
- }