diggylab 0.1.34 → 0.1.35
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 +10 -1
- package/cli.js +63 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ headless Chromium — the Electron app is not involved.
|
|
|
6
6
|
|
|
7
7
|
## Requirements
|
|
8
8
|
|
|
9
|
-
- Node.js
|
|
9
|
+
- Node.js 20+
|
|
10
10
|
- Linux packages: `xvfb`, `x11vnc`, `websockify`, `novnc` (Debian/Ubuntu: `apt install xvfb x11vnc websockify novnc`). The Docker image below bundles them.
|
|
11
11
|
- Chromium for Playwright: run `diggylab install-browser` once after installing the CLI.
|
|
12
12
|
|
|
@@ -80,6 +80,15 @@ The noVNC URL carries a one-time token, the port is only open while `login` runs
|
|
|
80
80
|
torn down when the login finishes or times out (10 minutes). If you prefer not to expose a port,
|
|
81
81
|
pass `--bind 127.0.0.1` and forward it over SSH (`ssh -L 6080:127.0.0.1:6080 <server>`).
|
|
82
82
|
|
|
83
|
+
### Which address the link uses
|
|
84
|
+
|
|
85
|
+
The link points at the machine's default-route address, so a multi-homed server advertises the
|
|
86
|
+
network it actually reaches the internet through. When that address is private, the agent also looks
|
|
87
|
+
up the public IP and prints a second link for phones on another network — forward the port first.
|
|
88
|
+
|
|
89
|
+
Pin it explicitly with `--public-host <ip or domain>` or `DIGGYLAB_PUBLIC_HOST`, for example when a
|
|
90
|
+
domain points at the server.
|
|
91
|
+
|
|
83
92
|
## Security notes
|
|
84
93
|
|
|
85
94
|
- `~/.diggylab/key` (mode `600`) encrypts device credentials, the Facebook session, and the
|
package/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/cli/cli.ts
|
|
4
4
|
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
5
5
|
import { spawn as spawn2 } from "node:child_process";
|
|
6
|
-
import { existsSync as existsSync2, readFileSync as
|
|
6
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, realpathSync } from "node:fs";
|
|
7
7
|
import { hostname, homedir } from "node:os";
|
|
8
8
|
import { dirname as dirname3, join as join4 } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -2411,11 +2411,13 @@ function terminalQr(value) {
|
|
|
2411
2411
|
// src/cli/virtual-display.ts
|
|
2412
2412
|
import { spawn } from "node:child_process";
|
|
2413
2413
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
2414
|
-
import { mkdirSync as mkdirSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2414
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2415
2415
|
import { createConnection } from "node:net";
|
|
2416
2416
|
import { networkInterfaces } from "node:os";
|
|
2417
2417
|
import { join as join3 } from "node:path";
|
|
2418
2418
|
var START_TIMEOUT_MS = 15e3;
|
|
2419
|
+
var PUBLIC_IP_TIMEOUT_MS = 3e3;
|
|
2420
|
+
var PUBLIC_IP_ENDPOINT = "https://api.ipify.org";
|
|
2419
2421
|
function waitForPort(host, port, timeoutMs) {
|
|
2420
2422
|
const deadline = Date.now() + timeoutMs;
|
|
2421
2423
|
return new Promise((resolve, reject) => {
|
|
@@ -2437,13 +2439,36 @@ function waitForPort(host, port, timeoutMs) {
|
|
|
2437
2439
|
attempt();
|
|
2438
2440
|
});
|
|
2439
2441
|
}
|
|
2440
|
-
function
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2442
|
+
function isPrivateAddress(address) {
|
|
2443
|
+
const [first, second] = address.split(".").map(Number);
|
|
2444
|
+
return first === 10 || first === 127 || first === 169 && second === 254 || first === 172 && second >= 16 && second <= 31 || first === 192 && second === 168 || first === 100 && second >= 64 && second <= 127;
|
|
2445
|
+
}
|
|
2446
|
+
function defaultRouteInterface() {
|
|
2447
|
+
if (process.platform !== "linux") return null;
|
|
2448
|
+
try {
|
|
2449
|
+
for (const line of readFileSync2("/proc/net/route", "utf8").split("\n").slice(1)) {
|
|
2450
|
+
const [iface, destination, , , , , flags] = line.trim().split(/\s+/);
|
|
2451
|
+
if (destination === "00000000" && Number.parseInt(flags ?? "0", 16) & 2) return iface;
|
|
2444
2452
|
}
|
|
2453
|
+
} catch {
|
|
2454
|
+
}
|
|
2455
|
+
return null;
|
|
2456
|
+
}
|
|
2457
|
+
function localAddress() {
|
|
2458
|
+
const interfaces = networkInterfaces();
|
|
2459
|
+
const addressOf = (addresses) => addresses?.find((entry) => entry.family === "IPv4" && !entry.internal)?.address ?? null;
|
|
2460
|
+
const preferred = defaultRouteInterface();
|
|
2461
|
+
return (preferred ? addressOf(interfaces[preferred]) : null) ?? Object.values(interfaces).map(addressOf).find((address) => address !== null) ?? "127.0.0.1";
|
|
2462
|
+
}
|
|
2463
|
+
async function detectPublicIp(fetcher = fetch) {
|
|
2464
|
+
try {
|
|
2465
|
+
const response = await fetcher(PUBLIC_IP_ENDPOINT, { signal: AbortSignal.timeout(PUBLIC_IP_TIMEOUT_MS) });
|
|
2466
|
+
if (!response.ok) return null;
|
|
2467
|
+
const value = (await response.text()).trim();
|
|
2468
|
+
return /^\d{1,3}(?:\.\d{1,3}){3}$/.test(value) ? value : null;
|
|
2469
|
+
} catch {
|
|
2470
|
+
return null;
|
|
2445
2471
|
}
|
|
2446
|
-
return "127.0.0.1";
|
|
2447
2472
|
}
|
|
2448
2473
|
function start(command, args, env, deps) {
|
|
2449
2474
|
const child = deps.spawnProcess(command, args, env);
|
|
@@ -2454,7 +2479,13 @@ function start(command, args, env, deps) {
|
|
|
2454
2479
|
return { child, failure };
|
|
2455
2480
|
}
|
|
2456
2481
|
async function startVirtualDisplay(options, overrides = {}) {
|
|
2457
|
-
const deps = {
|
|
2482
|
+
const deps = {
|
|
2483
|
+
detectPublicIp: () => detectPublicIp(),
|
|
2484
|
+
localAddress,
|
|
2485
|
+
spawnProcess: spawn,
|
|
2486
|
+
waitForPort,
|
|
2487
|
+
...overrides
|
|
2488
|
+
};
|
|
2458
2489
|
const bind = options.bind ?? "0.0.0.0";
|
|
2459
2490
|
const displayNumber = options.displayNumber ?? 99;
|
|
2460
2491
|
const height = options.height ?? 900;
|
|
@@ -2488,10 +2519,13 @@ async function startVirtualDisplay(options, overrides = {}) {
|
|
|
2488
2519
|
await stop();
|
|
2489
2520
|
throw error;
|
|
2490
2521
|
}
|
|
2491
|
-
const publicHost = options.publicHost ?? localAddress();
|
|
2522
|
+
const publicHost = options.publicHost ?? deps.localAddress();
|
|
2523
|
+
const link = (host) => `http://${host}:${port}/vnc.html?autoconnect=1&token=${encodeURIComponent(token)}`;
|
|
2524
|
+
const publicIp = options.publicHost || !isPrivateAddress(publicHost) ? null : await deps.detectPublicIp();
|
|
2492
2525
|
return {
|
|
2526
|
+
alternateUrl: publicIp && publicIp !== publicHost ? link(publicIp) : null,
|
|
2493
2527
|
stop,
|
|
2494
|
-
url:
|
|
2528
|
+
url: link(publicHost)
|
|
2495
2529
|
};
|
|
2496
2530
|
}
|
|
2497
2531
|
|
|
@@ -2512,13 +2546,14 @@ Global options:
|
|
|
2512
2546
|
--config <directory> Agent state directory (default ~/.diggylab)
|
|
2513
2547
|
--api <origin> API origin (default $DIGGYLAB_API_URL or https://api.diggylab.com)`;
|
|
2514
2548
|
var MISSING_BROWSER_HINT = "Chromium for Playwright is not installed yet. Run `diggylab install-browser` once.";
|
|
2549
|
+
var MISSING_DISPLAY_HINT = "The shared login screen needs Xvfb, x11vnc, websockify, and noVNC. Install them (apt install xvfb x11vnc websockify novnc) or use the Docker image.";
|
|
2515
2550
|
function agentVersion() {
|
|
2516
2551
|
let directory = fileURLToPath(new URL(".", import.meta.url));
|
|
2517
2552
|
for (let depth = 0; depth < 4; depth += 1) {
|
|
2518
2553
|
const manifest = join4(directory, "package.json");
|
|
2519
2554
|
if (existsSync2(manifest)) {
|
|
2520
2555
|
try {
|
|
2521
|
-
const version = JSON.parse(
|
|
2556
|
+
const version = JSON.parse(readFileSync3(manifest, "utf8")).version;
|
|
2522
2557
|
if (typeof version === "string" && version) return version;
|
|
2523
2558
|
} catch {
|
|
2524
2559
|
}
|
|
@@ -2640,18 +2675,23 @@ async function commandLogin(runtime, options, io) {
|
|
|
2640
2675
|
if (!needsVirtualDisplay) {
|
|
2641
2676
|
io.log(process.env.DISPLAY ? `Opening the Facebook login page on display ${process.env.DISPLAY}.` : "Opening the Facebook login page in a browser window.");
|
|
2642
2677
|
} else {
|
|
2678
|
+
const configuredHost = options.publicHost ?? process.env.DIGGYLAB_PUBLIC_HOST?.trim();
|
|
2643
2679
|
display = await startVirtualDisplay({
|
|
2644
2680
|
bind: options.bind,
|
|
2645
2681
|
displayNumber: options.display,
|
|
2646
2682
|
novncWeb: options.novncWeb,
|
|
2647
2683
|
port: options.port,
|
|
2648
|
-
publicHost:
|
|
2684
|
+
publicHost: configuredHost || void 0,
|
|
2649
2685
|
tokenDirectory: join4(runtime.directory, "novnc")
|
|
2650
2686
|
});
|
|
2651
2687
|
process.env.DISPLAY = `:${options.display ?? 99}`;
|
|
2652
2688
|
io.log(terminalQr(display.url));
|
|
2653
2689
|
io.log(`Open this link on your phone, or scan the QR above:
|
|
2654
2690
|
${display.url}`);
|
|
2691
|
+
if (display.alternateUrl) {
|
|
2692
|
+
io.log(`Phone on another network? Forward port ${options.port ?? 6080} and use:
|
|
2693
|
+
${display.alternateUrl}`);
|
|
2694
|
+
}
|
|
2655
2695
|
io.log("Log into Facebook on that screen; the agent saves the session automatically.");
|
|
2656
2696
|
}
|
|
2657
2697
|
await waitForFacebookLogin(login, LOGIN_TIMEOUT_MS + 6e4);
|
|
@@ -2659,6 +2699,10 @@ ${display.url}`);
|
|
|
2659
2699
|
return 0;
|
|
2660
2700
|
} catch (error) {
|
|
2661
2701
|
const message = error instanceof Error ? error.message : String(error);
|
|
2702
|
+
if (/spawn (?:Xvfb|x11vnc|websockify) ENOENT/.test(message)) {
|
|
2703
|
+
io.error(MISSING_DISPLAY_HINT);
|
|
2704
|
+
return 1;
|
|
2705
|
+
}
|
|
2662
2706
|
if (/Executable doesn't exist|playwright install/i.test(message)) {
|
|
2663
2707
|
io.error(MISSING_BROWSER_HINT);
|
|
2664
2708
|
return 1;
|
|
@@ -2683,11 +2727,16 @@ async function commandInstallBrowser(io) {
|
|
|
2683
2727
|
return 1;
|
|
2684
2728
|
}
|
|
2685
2729
|
io.log("Downloading Chromium for Playwright...");
|
|
2686
|
-
const
|
|
2687
|
-
const child = spawn2(process.execPath, [cli,
|
|
2730
|
+
const run = (args) => new Promise((resolve, reject) => {
|
|
2731
|
+
const child = spawn2(process.execPath, [cli, ...args], { stdio: "inherit" });
|
|
2688
2732
|
child.once("error", reject);
|
|
2689
2733
|
child.once("exit", (value) => resolve(value ?? 1));
|
|
2690
2734
|
});
|
|
2735
|
+
let code = process.platform === "linux" ? await run(["install", "--with-deps", "chromium"]) : 0;
|
|
2736
|
+
if (code !== 0) {
|
|
2737
|
+
io.log("Installing the system libraries failed; downloading the browser only...");
|
|
2738
|
+
code = await run(["install", "chromium"]);
|
|
2739
|
+
}
|
|
2691
2740
|
if (code !== 0) {
|
|
2692
2741
|
io.error("Chromium download failed. Check the network and try again.");
|
|
2693
2742
|
return code;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "diggylab",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.35",
|
|
4
4
|
"description": "DiggyLab Desktop Agent collects posts and information from Facebook Groups on a paired computer. (headless CLI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"README.md"
|
|
12
12
|
],
|
|
13
13
|
"engines": {
|
|
14
|
-
"node": ">=
|
|
14
|
+
"node": ">=20"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"playwright": "^1.62.0",
|