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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/cli.js +63 -14
  3. 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 22+
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 readFileSync2, realpathSync } from "node:fs";
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 localAddress() {
2441
- for (const addresses of Object.values(networkInterfaces())) {
2442
- for (const address of addresses ?? []) {
2443
- if (address.family === "IPv4" && !address.internal) return address.address;
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 = { spawnProcess: spawn, waitForPort, ...overrides };
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: `http://${publicHost}:${port}/vnc.html?autoconnect=1&token=${encodeURIComponent(token)}`
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(readFileSync2(manifest, "utf8")).version;
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: options.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 code = await new Promise((resolve, reject) => {
2687
- const child = spawn2(process.execPath, [cli, "install", "chromium"], { stdio: "inherit" });
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.34",
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": ">=22"
14
+ "node": ">=20"
15
15
  },
16
16
  "dependencies": {
17
17
  "playwright": "^1.62.0",