unbrowse 1.2.0 → 2.0.1
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 +21 -2
- package/dist/cli.js +133 -52
- package/package.json +2 -3
- package/runtime-src/auth/index.ts +25 -64
- package/runtime-src/capture/index.ts +543 -489
- package/runtime-src/kuri/client.ts +556 -0
- package/runtime-src/runtime/setup.ts +38 -11
- package/runtime-src/server.ts +8 -8
- package/vendor/kuri/darwin-arm64/kuri +0 -0
- package/vendor/kuri/darwin-x64/kuri +0 -0
- package/vendor/kuri/linux-arm64/kuri +0 -0
- package/vendor/kuri/linux-x64/kuri +0 -0
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ One agent learns a site once. Every later agent gets the fast path.
|
|
|
15
15
|
npx unbrowse setup
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
`npx unbrowse setup` downloads the CLI on demand,
|
|
18
|
+
`npx unbrowse setup` downloads the CLI on demand, verifies the bundled Kuri runtime, lets you register with an email-shaped display identity, registers the Open Code `/unbrowse` command when Open Code is detected, and starts the local server.
|
|
19
19
|
|
|
20
20
|
For daily use:
|
|
21
21
|
|
|
@@ -30,6 +30,25 @@ If your agent host uses skills:
|
|
|
30
30
|
npx skills add unbrowse-ai/unbrowse
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
## Upgrading
|
|
34
|
+
|
|
35
|
+
Unbrowse no longer self-updates at runtime. If you already have Unbrowse installed, upgrade to the latest version after each release or the new flow may not work on your machine.
|
|
36
|
+
|
|
37
|
+
If you installed the CLI globally:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g unbrowse@latest
|
|
41
|
+
unbrowse setup
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If your agent host uses skills, rerun its skill install/update command too:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx skills add unbrowse-ai/unbrowse
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Need help or want release updates? Join the Discord: [discord.gg/VWugEeFNsG](https://discord.gg/VWugEeFNsG)
|
|
51
|
+
|
|
33
52
|
Every CLI command auto-starts the local server on `http://localhost:6969` by default. Override with `UNBROWSE_URL`, `PORT`, or `HOST`. On first startup it auto-registers as an agent with the marketplace and caches credentials in `~/.unbrowse/config.json`. `unbrowse setup` now prompts for an email-shaped identity first; headless setups can provide `UNBROWSE_AGENT_EMAIL`.
|
|
34
53
|
|
|
35
54
|
Works with Claude Code, Open Code, Cursor, Codex, Windsurf, and any agent host that can call a local CLI or skill.
|
|
@@ -37,7 +56,7 @@ Works with Claude Code, Open Code, Cursor, Codex, Windsurf, and any agent host t
|
|
|
37
56
|
## What setup does
|
|
38
57
|
|
|
39
58
|
- Checks local prerequisites for the npm/npx flow.
|
|
40
|
-
-
|
|
59
|
+
- Verifies the bundled Kuri binary, or builds it from the vendored Kuri source when working from repo source with Zig installed.
|
|
41
60
|
- Registers the Open Code `/unbrowse` command when Open Code is present.
|
|
42
61
|
- Starts the local Unbrowse server unless `--no-start` is passed.
|
|
43
62
|
|
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @bun
|
|
3
|
-
import { createRequire } from "node:module";
|
|
4
|
-
var __create = Object.create;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __defProp = Object.defineProperty;
|
|
7
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __toESM = (mod, isNodeMode, target) => {
|
|
10
|
-
target = mod != null ? __create(__getProtoOf(mod)) : {};
|
|
11
|
-
const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
|
|
12
|
-
for (let key of __getOwnPropNames(mod))
|
|
13
|
-
if (!__hasOwnProp.call(to, key))
|
|
14
|
-
__defProp(to, key, {
|
|
15
|
-
get: () => mod[key],
|
|
16
|
-
enumerable: true
|
|
17
|
-
});
|
|
18
|
-
return to;
|
|
19
|
-
};
|
|
20
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
21
3
|
|
|
22
4
|
// ../../src/cli.ts
|
|
23
5
|
import { config as loadEnv } from "dotenv";
|
|
@@ -322,7 +304,7 @@ import { spawn } from "node:child_process";
|
|
|
322
304
|
import { existsSync as existsSync2, mkdirSync as mkdirSync2, realpathSync } from "node:fs";
|
|
323
305
|
import os from "node:os";
|
|
324
306
|
import path from "node:path";
|
|
325
|
-
import { createRequire
|
|
307
|
+
import { createRequire } from "node:module";
|
|
326
308
|
import { fileURLToPath } from "node:url";
|
|
327
309
|
function getModuleDir(metaUrl) {
|
|
328
310
|
return path.dirname(fileURLToPath(metaUrl));
|
|
@@ -344,7 +326,7 @@ function runtimeArgsForEntrypoint(metaUrl, entrypoint) {
|
|
|
344
326
|
if (process.versions.bun)
|
|
345
327
|
return [entrypoint];
|
|
346
328
|
try {
|
|
347
|
-
const req =
|
|
329
|
+
const req = createRequire(metaUrl);
|
|
348
330
|
const tsxPkg = req.resolve("tsx/package.json");
|
|
349
331
|
const tsxLoader = path.join(path.dirname(tsxPkg), "dist", "loader.mjs");
|
|
350
332
|
if (existsSync2(tsxLoader))
|
|
@@ -483,16 +465,88 @@ function isMainModule(metaUrl) {
|
|
|
483
465
|
}
|
|
484
466
|
|
|
485
467
|
// ../../src/runtime/setup.ts
|
|
486
|
-
import { execFileSync } from "node:child_process";
|
|
487
|
-
import {
|
|
488
|
-
import
|
|
489
|
-
import
|
|
468
|
+
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
469
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, writeFileSync as writeFileSync3 } from "node:fs";
|
|
470
|
+
import os3 from "node:os";
|
|
471
|
+
import path6 from "node:path";
|
|
472
|
+
|
|
473
|
+
// ../../src/kuri/client.ts
|
|
474
|
+
import { execFileSync, spawn as spawn2 } from "node:child_process";
|
|
475
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
476
|
+
import path5 from "node:path";
|
|
477
|
+
|
|
478
|
+
// ../../src/logger.ts
|
|
490
479
|
import path4 from "node:path";
|
|
491
|
-
|
|
480
|
+
import os2 from "node:os";
|
|
481
|
+
var LOG_DIR = path4.join(os2.homedir(), ".unbrowse", "logs");
|
|
482
|
+
|
|
483
|
+
// ../../src/kuri/client.ts
|
|
484
|
+
function kuriBinaryName() {
|
|
485
|
+
return process.platform === "win32" ? "kuri.exe" : "kuri";
|
|
486
|
+
}
|
|
487
|
+
function currentBundledKuriTarget() {
|
|
488
|
+
if (process.platform === "darwin" && process.arch === "arm64")
|
|
489
|
+
return "darwin-arm64";
|
|
490
|
+
if (process.platform === "darwin" && process.arch === "x64")
|
|
491
|
+
return "darwin-x64";
|
|
492
|
+
if (process.platform === "linux" && process.arch === "arm64")
|
|
493
|
+
return "linux-arm64";
|
|
494
|
+
if (process.platform === "linux" && process.arch === "x64")
|
|
495
|
+
return "linux-x64";
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
function resolveBinaryOnPath(name) {
|
|
499
|
+
const checker = process.platform === "win32" ? "where" : "which";
|
|
500
|
+
try {
|
|
501
|
+
const output = execFileSync(checker, [name], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
502
|
+
const match = output.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
503
|
+
return match || null;
|
|
504
|
+
} catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function addCandidate(candidates, candidate) {
|
|
509
|
+
if (!candidate)
|
|
510
|
+
return;
|
|
511
|
+
if (!candidates.includes(candidate))
|
|
512
|
+
candidates.push(candidate);
|
|
513
|
+
}
|
|
514
|
+
function getKuriSourceCandidates() {
|
|
515
|
+
const packageRoot = getPackageRoot(import.meta.url);
|
|
516
|
+
const candidates = [];
|
|
517
|
+
addCandidate(candidates, path5.join(packageRoot, "vendor", "kuri-src"));
|
|
518
|
+
addCandidate(candidates, path5.join(packageRoot, "submodules", "kuri"));
|
|
519
|
+
if (process.env.KURI_PATH)
|
|
520
|
+
addCandidate(candidates, process.env.KURI_PATH);
|
|
521
|
+
if (process.env.HOME)
|
|
522
|
+
addCandidate(candidates, path5.join(process.env.HOME, "kuri"));
|
|
523
|
+
return candidates;
|
|
524
|
+
}
|
|
525
|
+
function getKuriBinaryCandidates() {
|
|
526
|
+
const packageRoot = getPackageRoot(import.meta.url);
|
|
527
|
+
const binaryName = kuriBinaryName();
|
|
528
|
+
const target = currentBundledKuriTarget();
|
|
529
|
+
const candidates = [];
|
|
530
|
+
if (target)
|
|
531
|
+
addCandidate(candidates, path5.join(packageRoot, "vendor", "kuri", target, binaryName));
|
|
532
|
+
for (const sourceDir of getKuriSourceCandidates()) {
|
|
533
|
+
addCandidate(candidates, path5.join(sourceDir, "zig-out", "bin", binaryName));
|
|
534
|
+
}
|
|
535
|
+
addCandidate(candidates, resolveBinaryOnPath("kuri"));
|
|
536
|
+
return candidates;
|
|
537
|
+
}
|
|
538
|
+
function findKuriBinary() {
|
|
539
|
+
if (process.env.KURI_BIN)
|
|
540
|
+
return process.env.KURI_BIN;
|
|
541
|
+
const candidates = getKuriBinaryCandidates();
|
|
542
|
+
return candidates.find((candidate) => existsSync4(candidate)) ?? candidates[0] ?? kuriBinaryName();
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ../../src/runtime/setup.ts
|
|
492
546
|
function hasBinary(name) {
|
|
493
547
|
const checker = process.platform === "win32" ? "where" : "which";
|
|
494
548
|
try {
|
|
495
|
-
|
|
549
|
+
execFileSync2(checker, [name], { stdio: "ignore" });
|
|
496
550
|
return true;
|
|
497
551
|
} catch {
|
|
498
552
|
return false;
|
|
@@ -508,18 +562,18 @@ function detectPackageManagers() {
|
|
|
508
562
|
}
|
|
509
563
|
function resolveConfigHome() {
|
|
510
564
|
if (process.platform === "win32") {
|
|
511
|
-
return process.env.APPDATA ||
|
|
565
|
+
return process.env.APPDATA || path6.join(os3.homedir(), "AppData", "Roaming");
|
|
512
566
|
}
|
|
513
|
-
return process.env.XDG_CONFIG_HOME ||
|
|
567
|
+
return process.env.XDG_CONFIG_HOME || path6.join(os3.homedir(), ".config");
|
|
514
568
|
}
|
|
515
569
|
function getOpenCodeGlobalCommandsDir() {
|
|
516
|
-
return
|
|
570
|
+
return path6.join(resolveConfigHome(), "opencode", "commands");
|
|
517
571
|
}
|
|
518
572
|
function getOpenCodeProjectCommandsDir(cwd) {
|
|
519
|
-
return
|
|
573
|
+
return path6.join(cwd, ".opencode", "commands");
|
|
520
574
|
}
|
|
521
575
|
function detectOpenCode(cwd) {
|
|
522
|
-
return hasBinary("opencode") ||
|
|
576
|
+
return hasBinary("opencode") || existsSync5(path6.join(resolveConfigHome(), "opencode")) || existsSync5(path6.join(cwd, ".opencode"));
|
|
523
577
|
}
|
|
524
578
|
function renderOpenCodeCommand() {
|
|
525
579
|
return `---
|
|
@@ -547,12 +601,12 @@ function writeOpenCodeCommand(scope, cwd) {
|
|
|
547
601
|
if (scope === "auto" && !detected) {
|
|
548
602
|
return { detected: false, action: "not-detected", scope: "off" };
|
|
549
603
|
}
|
|
550
|
-
const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" :
|
|
604
|
+
const resolvedScope = scope === "project" ? "project" : scope === "global" ? "global" : existsSync5(path6.join(cwd, ".opencode")) ? "project" : "global";
|
|
551
605
|
const commandsDir = resolvedScope === "project" ? getOpenCodeProjectCommandsDir(cwd) : getOpenCodeGlobalCommandsDir();
|
|
552
|
-
const commandFile =
|
|
606
|
+
const commandFile = path6.join(ensureDir(commandsDir), "unbrowse.md");
|
|
553
607
|
const content = renderOpenCodeCommand();
|
|
554
|
-
const action =
|
|
555
|
-
mkdirSync4(
|
|
608
|
+
const action = existsSync5(commandFile) ? "updated" : "installed";
|
|
609
|
+
mkdirSync4(path6.dirname(commandFile), { recursive: true });
|
|
556
610
|
writeFileSync3(commandFile, content);
|
|
557
611
|
return {
|
|
558
612
|
detected: detected || scope !== "auto",
|
|
@@ -562,17 +616,44 @@ function writeOpenCodeCommand(scope, cwd) {
|
|
|
562
616
|
};
|
|
563
617
|
}
|
|
564
618
|
async function ensureBrowserEngineInstalled() {
|
|
619
|
+
const binary = findKuriBinary();
|
|
620
|
+
if (existsSync5(binary)) {
|
|
621
|
+
return { installed: true, action: "already-installed" };
|
|
622
|
+
}
|
|
623
|
+
const sourceDir = getKuriSourceCandidates().find((candidate) => existsSync5(path6.join(candidate, "build.zig")));
|
|
624
|
+
if (!sourceDir) {
|
|
625
|
+
return {
|
|
626
|
+
installed: false,
|
|
627
|
+
action: "failed",
|
|
628
|
+
message: `Kuri binary not found. Checked ${binary}`
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
if (!hasBinary("zig")) {
|
|
632
|
+
return {
|
|
633
|
+
installed: false,
|
|
634
|
+
action: "failed",
|
|
635
|
+
message: `Kuri source found at ${sourceDir}, but Zig is not installed`
|
|
636
|
+
};
|
|
637
|
+
}
|
|
565
638
|
try {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
return { installed: true, action: "already-installed" };
|
|
569
|
-
}
|
|
570
|
-
const agentBrowserBin = req.resolve("agent-browser/bin/agent-browser.js");
|
|
571
|
-
execFileSync(process.execPath, [agentBrowserBin, "install"], {
|
|
639
|
+
execFileSync2("zig", ["build", "-Doptimize=ReleaseFast"], {
|
|
640
|
+
cwd: sourceDir,
|
|
572
641
|
stdio: "inherit",
|
|
573
642
|
timeout: 300000
|
|
574
643
|
});
|
|
575
|
-
|
|
644
|
+
const builtBinary = findKuriBinary();
|
|
645
|
+
if (existsSync5(builtBinary)) {
|
|
646
|
+
return {
|
|
647
|
+
installed: true,
|
|
648
|
+
action: "installed",
|
|
649
|
+
message: `Built Kuri from ${sourceDir}`
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
return {
|
|
653
|
+
installed: false,
|
|
654
|
+
action: "failed",
|
|
655
|
+
message: `Kuri build completed but ${builtBinary} was not created`
|
|
656
|
+
};
|
|
576
657
|
} catch (error) {
|
|
577
658
|
const message = error instanceof Error ? error.message : String(error);
|
|
578
659
|
return { installed: false, action: "failed", message };
|
|
@@ -584,7 +665,7 @@ async function runSetup(options) {
|
|
|
584
665
|
return {
|
|
585
666
|
os: {
|
|
586
667
|
platform: process.platform,
|
|
587
|
-
release:
|
|
668
|
+
release: os3.release(),
|
|
588
669
|
arch: process.arch
|
|
589
670
|
},
|
|
590
671
|
package_managers: detectPackageManagers(),
|
|
@@ -621,8 +702,8 @@ function parseArgs(argv) {
|
|
|
621
702
|
}
|
|
622
703
|
return { command, args: positional, flags };
|
|
623
704
|
}
|
|
624
|
-
async function api2(method,
|
|
625
|
-
const res = await fetch(`${BASE_URL}${
|
|
705
|
+
async function api2(method, path7, body) {
|
|
706
|
+
const res = await fetch(`${BASE_URL}${path7}`, {
|
|
626
707
|
method,
|
|
627
708
|
headers: {
|
|
628
709
|
...body ? { "Content-Type": "application/json" } : {},
|
|
@@ -708,10 +789,10 @@ function detectEntityIndex(data) {
|
|
|
708
789
|
}
|
|
709
790
|
return best ? buildEntityIndex(best) : null;
|
|
710
791
|
}
|
|
711
|
-
function resolvePath(obj,
|
|
712
|
-
if (!
|
|
792
|
+
function resolvePath(obj, path7, entityIndex) {
|
|
793
|
+
if (!path7 || obj == null)
|
|
713
794
|
return obj;
|
|
714
|
-
const segments =
|
|
795
|
+
const segments = path7.split(".");
|
|
715
796
|
let cur = obj;
|
|
716
797
|
for (let i = 0;i < segments.length; i++) {
|
|
717
798
|
if (cur == null)
|
|
@@ -750,8 +831,8 @@ function extractFields(data, fields, entityIndex) {
|
|
|
750
831
|
for (const f of fields) {
|
|
751
832
|
const colonIdx = f.indexOf(":");
|
|
752
833
|
const alias = colonIdx >= 0 ? f.slice(0, colonIdx) : f.split(".").pop();
|
|
753
|
-
const
|
|
754
|
-
const resolved = resolvePath(item,
|
|
834
|
+
const path7 = colonIdx >= 0 ? f.slice(colonIdx + 1) : f;
|
|
835
|
+
const resolved = resolvePath(item, path7, entityIndex ?? undefined) ?? [];
|
|
755
836
|
out[alias] = Array.isArray(resolved) ? resolved.length === 0 ? null : resolved.length === 1 ? resolved[0] : resolved : resolved;
|
|
756
837
|
}
|
|
757
838
|
return out;
|
|
@@ -1039,11 +1120,11 @@ async function cmdSearch(flags) {
|
|
|
1039
1120
|
if (!intent)
|
|
1040
1121
|
die("--intent is required");
|
|
1041
1122
|
const domain = flags.domain;
|
|
1042
|
-
const
|
|
1123
|
+
const path7 = domain ? "/v1/search/domain" : "/v1/search";
|
|
1043
1124
|
const body = { intent, k: Number(flags.k) || 5 };
|
|
1044
1125
|
if (domain)
|
|
1045
1126
|
body.domain = domain;
|
|
1046
|
-
output(await api2("POST",
|
|
1127
|
+
output(await api2("POST", path7, body), !!flags.pretty);
|
|
1047
1128
|
}
|
|
1048
1129
|
async function cmdSessions(flags) {
|
|
1049
1130
|
const domain = flags.domain;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unbrowse",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "Reverse-engineer any website into reusable API skills. npm CLI + local engine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"files": [
|
|
10
10
|
"dist",
|
|
11
11
|
"runtime-src",
|
|
12
|
+
"vendor/kuri",
|
|
12
13
|
"README.md",
|
|
13
14
|
"LICENSE"
|
|
14
15
|
],
|
|
@@ -25,8 +26,6 @@
|
|
|
25
26
|
"cheerio": "^1.2.0",
|
|
26
27
|
"dotenv": "^17.3.1",
|
|
27
28
|
"nanoid": "^5.1.6",
|
|
28
|
-
"agent-browser": "^0.13.0",
|
|
29
|
-
"playwright-core": "^1.58.2",
|
|
30
29
|
"tsx": "^4.20.6",
|
|
31
30
|
"ws": "^8.19.0"
|
|
32
31
|
},
|
|
@@ -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);
|