rechrome 1.10.2 → 1.11.0

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 (5) hide show
  1. package/package.json +1 -1
  2. package/rech.js +70 -11
  3. package/rech.ts +70 -11
  4. package/serve.js +1 -1
  5. package/serve.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.10.2",
3
+ "version": "1.11.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rech.js CHANGED
@@ -120,7 +120,7 @@ export function parseUrl(raw: string) {
120
120
  export async function getOrCreateUrl(): Promise<string> {
121
121
  // Treat a URL without a bearer key as missing — it cannot authenticate
122
122
  try { if (process.env[ENV_KEY] && new URL(process.env[ENV_KEY]!).username) return process.env[ENV_KEY]!; } catch {}
123
- const key = randomBytes(9).toString("base64url"); // 12 chars
123
+ const key = randomBytes(12).toString("base64url"); // 16 chars
124
124
  const url = `http://${key}@127.0.0.1:${DEFAULT_PORT}`;
125
125
  const newLine = `${ENV_KEY}=${url}`;
126
126
  // Write to ~/.env.local so it's not shadowed by project .env.local
@@ -362,7 +362,7 @@ async function callServe(
362
362
  process.exit(1);
363
363
  });
364
364
  if (res.status === 401) {
365
- console.error(`[rech] rech-client -> rech-server[ok]\n -x: token rejected (used: ${key.slice(0, 6)}...) -> playwright[unknown]`);
365
+ console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
366
366
  process.exit(1);
367
367
  }
368
368
  return res.json();
@@ -486,6 +486,17 @@ async function runOxmgr(args: string[]): Promise<number> {
486
486
  }
487
487
 
488
488
  async function daemonInstall(serveUrl: string): Promise<void> {
489
+ // Persist the URL to ~/.env.local before starting the daemon. The daemon's
490
+ // loadEnv() walks CWD→root reading .env.local files and unconditionally
491
+ // overwrites process.env.RECHROME_URL from whichever file it finds first.
492
+ // Without this write, oxmgr's --env RECHROME_URL=... gets clobbered by a
493
+ // stale ~/.env.local entry — the daemon then listens on a different bearer
494
+ // key than the one daemonInstall was called with, and every client request
495
+ // is rejected with "bearer key rejected".
496
+ const envRaw = await file(globalEnvFile).text().catch(() => "");
497
+ const filtered = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
498
+ await Bun.write(globalEnvFile, [...filtered, `${ENV_KEY}=${serveUrl}`, ""].join("\n"));
499
+
489
500
  const home = process.env.HOME!;
490
501
  const bunBin = Bun.which("bun") ?? process.execPath;
491
502
  const rechScript = import.meta.filename;
@@ -543,6 +554,13 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
543
554
 
544
555
  // [1/4] Daemon
545
556
  console.log("\n[1/4] Setting up serve daemon...");
557
+
558
+ // Bind address (persists to ~/.env.local as RECH_HOST).
559
+ // Read the persisted value from ~/.env.local directly — process.env may be shadowed by nearer .env files.
560
+ const globalEnvRaw = await file(globalEnvFile).text().catch(() => "");
561
+ const persistedBindMatch = globalEnvRaw.match(/^\s*RECH_HOST\s*=\s*(.*?)\s*$/m);
562
+ const persistedBind = persistedBindMatch?.[1].replace(/^["']|["']$/g, "") || "127.0.0.1";
563
+
546
564
  // Clear stale hostname-based URL so we always use 127.0.0.1 locally
547
565
  if (process.env[ENV_KEY]) {
548
566
  try {
@@ -560,14 +578,35 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
560
578
  headers: { Authorization: `Bearer ${serveKey}` },
561
579
  signal: AbortSignal.timeout(2000),
562
580
  }).catch(() => null) : null;
563
- if (anonPing && authPing?.ok) {
564
- console.log(` Already running at ${protocol}://${host}:${port} — skipping reinstall`);
565
- } else if (anonPing && !authPing?.ok) {
566
- console.log(` Server running but key mismatch — reinstalling with new key`);
567
- await daemonInstall(url);
568
- } else {
569
- await daemonInstall(url);
570
- console.log(` Registered daemon: ${OXMGR_PROCESS_NAME}`);
581
+ // The daemon's *live* bind (from /ping) is authoritative — persisted RECH_HOST may diverge if the user edited it manually.
582
+ const liveBind = authPing?.ok
583
+ ? await authPing.clone().json().then((b: { bind?: string }) => b?.bind).catch(() => undefined)
584
+ : undefined;
585
+ // Pre-patch daemons return plain "ok" with no bind info — we can't trust persisted/env values to match their live bind, so force reinstall to be safe.
586
+ const liveBindUnknown = !!authPing?.ok && !liveBind;
587
+ const currentBind = liveBind || persistedBind;
588
+
589
+ // Non-TTY honors explicit process.env.RECH_HOST (shell or merged env stack) — matches the documented `RECH_HOST=0.0.0.0 rech setup` flow.
590
+ let desiredBind = process.env.RECH_HOST || currentBind;
591
+ if (isTTY) {
592
+ console.log(`\n Bind address (current: ${currentBind}):`);
593
+ console.log(` 1. 127.0.0.1 (localhost only)`);
594
+ console.log(` 2. 0.0.0.0 (all interfaces — HTTP plaintext, trust your network)`);
595
+ const defaultBindChoice = currentBind === "0.0.0.0" ? "2" : "1";
596
+ const bindAns = (await ask(` Choice [${defaultBindChoice}]: `, defaultBindChoice)).trim();
597
+ desiredBind = bindAns === "2" || bindAns === "0.0.0.0" ? "0.0.0.0" : "127.0.0.1";
598
+ }
599
+ const bindChanged = desiredBind !== currentBind;
600
+ const persistedChanged = desiredBind !== persistedBind;
601
+ if (persistedChanged) {
602
+ const lines = globalEnvRaw.trimEnd().split("\n").filter(l => !/^\s*RECH_HOST\s*=/.test(l));
603
+ await Bun.write(globalEnvFile, [...lines, `RECH_HOST=${desiredBind}`, ""].join("\n"));
604
+ console.log(` Saved RECH_HOST=${desiredBind} to ~/.env.local`);
605
+ }
606
+ // Always align process.env with the desired bind — a nearer .env.local may have shadowed it.
607
+ process.env.RECH_HOST = desiredBind;
608
+
609
+ const waitForServe = async () => {
571
610
  process.stdout.write(" Starting");
572
611
  let ping = null;
573
612
  for (let i = 0; i < 15; i++) {
@@ -583,6 +622,26 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
583
622
  process.exit(1);
584
623
  }
585
624
  console.log(` Serve running at ${protocol}://${host}:${port}`);
625
+ };
626
+
627
+ if (anonPing && authPing?.ok && !bindChanged && !liveBindUnknown) {
628
+ console.log(` Already running at ${protocol}://${host}:${port} — skipping reinstall`);
629
+ } else if (anonPing && authPing?.ok && liveBindUnknown) {
630
+ console.log(` Pre-patch daemon detected (no live bind info) — reinstalling to verify bind`);
631
+ await daemonInstall(url);
632
+ await waitForServe();
633
+ } else if (anonPing && bindChanged) {
634
+ console.log(` Bind changed (${currentBind} → ${desiredBind}) — reinstalling`);
635
+ await daemonInstall(url);
636
+ await waitForServe();
637
+ } else if (anonPing && !authPing?.ok) {
638
+ console.log(` Server running but key mismatch — reinstalling with new key`);
639
+ await daemonInstall(url);
640
+ await waitForServe();
641
+ } else {
642
+ await daemonInstall(url);
643
+ console.log(` Registered daemon: ${OXMGR_PROCESS_NAME}`);
644
+ await waitForServe();
586
645
  }
587
646
 
588
647
  const cache = await readChromeProfileCache();
@@ -682,7 +741,7 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
682
741
 
683
742
  // Build RECHROME_URL and show it before asking where to save
684
743
  const rechUrl = new URL(url);
685
- if (!rechUrl.username) rechUrl.username = randomBytes(9).toString("base64url");
744
+ if (!rechUrl.username) rechUrl.username = randomBytes(12).toString("base64url");
686
745
  rechUrl.searchParams.set("extension_id", extId);
687
746
  rechUrl.searchParams.set("token", token);
688
747
  rechUrl.searchParams.set("profile", profileEmail);
package/rech.ts CHANGED
@@ -120,7 +120,7 @@ export function parseUrl(raw: string) {
120
120
  export async function getOrCreateUrl(): Promise<string> {
121
121
  // Treat a URL without a bearer key as missing — it cannot authenticate
122
122
  try { if (process.env[ENV_KEY] && new URL(process.env[ENV_KEY]!).username) return process.env[ENV_KEY]!; } catch {}
123
- const key = randomBytes(9).toString("base64url"); // 12 chars
123
+ const key = randomBytes(12).toString("base64url"); // 16 chars
124
124
  const url = `http://${key}@127.0.0.1:${DEFAULT_PORT}`;
125
125
  const newLine = `${ENV_KEY}=${url}`;
126
126
  // Write to ~/.env.local so it's not shadowed by project .env.local
@@ -362,7 +362,7 @@ async function callServe(
362
362
  process.exit(1);
363
363
  });
364
364
  if (res.status === 401) {
365
- console.error(`[rech] rech-client -> rech-server[ok]\n -x: token rejected (used: ${key.slice(0, 6)}...) -> playwright[unknown]`);
365
+ console.error(`[rech] rech-client -> rech-server[ok]\n -x: bearer key rejected (used: ${key.slice(0, 4)}...) -> playwright[unknown]`);
366
366
  process.exit(1);
367
367
  }
368
368
  return res.json();
@@ -486,6 +486,17 @@ async function runOxmgr(args: string[]): Promise<number> {
486
486
  }
487
487
 
488
488
  async function daemonInstall(serveUrl: string): Promise<void> {
489
+ // Persist the URL to ~/.env.local before starting the daemon. The daemon's
490
+ // loadEnv() walks CWD→root reading .env.local files and unconditionally
491
+ // overwrites process.env.RECHROME_URL from whichever file it finds first.
492
+ // Without this write, oxmgr's --env RECHROME_URL=... gets clobbered by a
493
+ // stale ~/.env.local entry — the daemon then listens on a different bearer
494
+ // key than the one daemonInstall was called with, and every client request
495
+ // is rejected with "bearer key rejected".
496
+ const envRaw = await file(globalEnvFile).text().catch(() => "");
497
+ const filtered = envRaw.trimEnd().split("\n").filter(l => !l.startsWith(`${ENV_KEY}=`));
498
+ await Bun.write(globalEnvFile, [...filtered, `${ENV_KEY}=${serveUrl}`, ""].join("\n"));
499
+
489
500
  const home = process.env.HOME!;
490
501
  const bunBin = Bun.which("bun") ?? process.execPath;
491
502
  const rechScript = import.meta.filename;
@@ -543,6 +554,13 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
543
554
 
544
555
  // [1/4] Daemon
545
556
  console.log("\n[1/4] Setting up serve daemon...");
557
+
558
+ // Bind address (persists to ~/.env.local as RECH_HOST).
559
+ // Read the persisted value from ~/.env.local directly — process.env may be shadowed by nearer .env files.
560
+ const globalEnvRaw = await file(globalEnvFile).text().catch(() => "");
561
+ const persistedBindMatch = globalEnvRaw.match(/^\s*RECH_HOST\s*=\s*(.*?)\s*$/m);
562
+ const persistedBind = persistedBindMatch?.[1].replace(/^["']|["']$/g, "") || "127.0.0.1";
563
+
546
564
  // Clear stale hostname-based URL so we always use 127.0.0.1 locally
547
565
  if (process.env[ENV_KEY]) {
548
566
  try {
@@ -560,14 +578,35 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
560
578
  headers: { Authorization: `Bearer ${serveKey}` },
561
579
  signal: AbortSignal.timeout(2000),
562
580
  }).catch(() => null) : null;
563
- if (anonPing && authPing?.ok) {
564
- console.log(` Already running at ${protocol}://${host}:${port} — skipping reinstall`);
565
- } else if (anonPing && !authPing?.ok) {
566
- console.log(` Server running but key mismatch — reinstalling with new key`);
567
- await daemonInstall(url);
568
- } else {
569
- await daemonInstall(url);
570
- console.log(` Registered daemon: ${OXMGR_PROCESS_NAME}`);
581
+ // The daemon's *live* bind (from /ping) is authoritative — persisted RECH_HOST may diverge if the user edited it manually.
582
+ const liveBind = authPing?.ok
583
+ ? await authPing.clone().json().then((b: { bind?: string }) => b?.bind).catch(() => undefined)
584
+ : undefined;
585
+ // Pre-patch daemons return plain "ok" with no bind info — we can't trust persisted/env values to match their live bind, so force reinstall to be safe.
586
+ const liveBindUnknown = !!authPing?.ok && !liveBind;
587
+ const currentBind = liveBind || persistedBind;
588
+
589
+ // Non-TTY honors explicit process.env.RECH_HOST (shell or merged env stack) — matches the documented `RECH_HOST=0.0.0.0 rech setup` flow.
590
+ let desiredBind = process.env.RECH_HOST || currentBind;
591
+ if (isTTY) {
592
+ console.log(`\n Bind address (current: ${currentBind}):`);
593
+ console.log(` 1. 127.0.0.1 (localhost only)`);
594
+ console.log(` 2. 0.0.0.0 (all interfaces — HTTP plaintext, trust your network)`);
595
+ const defaultBindChoice = currentBind === "0.0.0.0" ? "2" : "1";
596
+ const bindAns = (await ask(` Choice [${defaultBindChoice}]: `, defaultBindChoice)).trim();
597
+ desiredBind = bindAns === "2" || bindAns === "0.0.0.0" ? "0.0.0.0" : "127.0.0.1";
598
+ }
599
+ const bindChanged = desiredBind !== currentBind;
600
+ const persistedChanged = desiredBind !== persistedBind;
601
+ if (persistedChanged) {
602
+ const lines = globalEnvRaw.trimEnd().split("\n").filter(l => !/^\s*RECH_HOST\s*=/.test(l));
603
+ await Bun.write(globalEnvFile, [...lines, `RECH_HOST=${desiredBind}`, ""].join("\n"));
604
+ console.log(` Saved RECH_HOST=${desiredBind} to ~/.env.local`);
605
+ }
606
+ // Always align process.env with the desired bind — a nearer .env.local may have shadowed it.
607
+ process.env.RECH_HOST = desiredBind;
608
+
609
+ const waitForServe = async () => {
571
610
  process.stdout.write(" Starting");
572
611
  let ping = null;
573
612
  for (let i = 0; i < 15; i++) {
@@ -583,6 +622,26 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
583
622
  process.exit(1);
584
623
  }
585
624
  console.log(` Serve running at ${protocol}://${host}:${port}`);
625
+ };
626
+
627
+ if (anonPing && authPing?.ok && !bindChanged && !liveBindUnknown) {
628
+ console.log(` Already running at ${protocol}://${host}:${port} — skipping reinstall`);
629
+ } else if (anonPing && authPing?.ok && liveBindUnknown) {
630
+ console.log(` Pre-patch daemon detected (no live bind info) — reinstalling to verify bind`);
631
+ await daemonInstall(url);
632
+ await waitForServe();
633
+ } else if (anonPing && bindChanged) {
634
+ console.log(` Bind changed (${currentBind} → ${desiredBind}) — reinstalling`);
635
+ await daemonInstall(url);
636
+ await waitForServe();
637
+ } else if (anonPing && !authPing?.ok) {
638
+ console.log(` Server running but key mismatch — reinstalling with new key`);
639
+ await daemonInstall(url);
640
+ await waitForServe();
641
+ } else {
642
+ await daemonInstall(url);
643
+ console.log(` Registered daemon: ${OXMGR_PROCESS_NAME}`);
644
+ await waitForServe();
586
645
  }
587
646
 
588
647
  const cache = await readChromeProfileCache();
@@ -682,7 +741,7 @@ async function setup(opts: { profile?: string } = {}): Promise<void> {
682
741
 
683
742
  // Build RECHROME_URL and show it before asking where to save
684
743
  const rechUrl = new URL(url);
685
- if (!rechUrl.username) rechUrl.username = randomBytes(9).toString("base64url");
744
+ if (!rechUrl.username) rechUrl.username = randomBytes(12).toString("base64url");
686
745
  rechUrl.searchParams.set("extension_id", extId);
687
746
  rechUrl.searchParams.set("token", token);
688
747
  rechUrl.searchParams.set("profile", profileEmail);
package/serve.js CHANGED
@@ -110,7 +110,7 @@ export async function serve() {
110
110
  if (reqUrl.pathname === "/ping") {
111
111
  const denied = authCheck(req, key);
112
112
  if (denied) return denied;
113
- return new Response("ok");
113
+ return Response.json({ ok: true, bind: listenHost });
114
114
  }
115
115
  if (reqUrl.pathname !== "/run") return new Response("rech server\n");
116
116
  const denied = authCheck(req, key);
package/serve.ts CHANGED
@@ -110,7 +110,7 @@ export async function serve() {
110
110
  if (reqUrl.pathname === "/ping") {
111
111
  const denied = authCheck(req, key);
112
112
  if (denied) return denied;
113
- return new Response("ok");
113
+ return Response.json({ ok: true, bind: listenHost });
114
114
  }
115
115
  if (reqUrl.pathname !== "/run") return new Response("rech server\n");
116
116
  const denied = authCheck(req, key);