rechrome 1.31.7 → 1.31.8

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/package.json +1 -1
  2. package/serve.js +56 -37
  3. package/serve.ts +56 -37
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.31.7",
3
+ "version": "1.31.8",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/serve.js CHANGED
@@ -1,8 +1,9 @@
1
1
  import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, type Listener } from "./listeners.js";
2
2
  import { file } from "bun";
3
3
  import { createHash, X509Certificate } from "crypto";
4
- import { mkdirSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
4
+ import { mkdirSync, lstatSync, chmodSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
5
5
  import { join, resolve, relative, isAbsolute } from "path";
6
+ import { tmpdir } from "os";
6
7
  import {
7
8
  log,
8
9
  parseUrl,
@@ -136,8 +137,42 @@ export function inferSilentExtensionFailure(options: {
136
137
  return `Extension connection timeout after ${handshakeTimeoutMs}ms. Automatic recovery retry failed; reload the Playwright MCP Bridge extension at chrome://extensions and retry.\n`;
137
138
  }
138
139
 
139
- function tmpSocketRoot(): string {
140
- return `${(process.env.TMPDIR || "/tmp").replace(/\/$/, "")}/playwright-cli`;
140
+ // Every CLI child must agree on its socket namespace, including probes and reapers.
141
+ export function playwrightCliEnv(
142
+ overrides: Record<string, string | undefined> = {},
143
+ runtime = { env: process.env, platform: process.platform, uid: process.getuid?.() },
144
+ ): Record<string, string | undefined> {
145
+ const source = runtime.env;
146
+ const env: Record<string, string | undefined> = {
147
+ PATH: source.PATH,
148
+ HOME: source.HOME || HOME,
149
+ USERPROFILE: source.USERPROFILE,
150
+ TMPDIR: source.TMPDIR,
151
+ DISPLAY: source.DISPLAY,
152
+ XDG_RUNTIME_DIR: source.XDG_RUNTIME_DIR,
153
+ DEBUG: source.DEBUG,
154
+ PWDEBUG: source.PWDEBUG,
155
+ ...overrides,
156
+ PLAYWRIGHT_SOCKETS_DIR: source.PLAYWRIGHT_SOCKETS_DIR,
157
+ };
158
+ if (runtime.platform === "darwin") {
159
+ if (runtime.uid === undefined) throw new Error("Cannot determine socket directory owner");
160
+ const dir = source.PLAYWRIGHT_SOCKETS_DIR || `/tmp/pw-${runtime.uid}`;
161
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
162
+ const stat = lstatSync(dir);
163
+ if (!stat.isDirectory() || stat.uid !== runtime.uid)
164
+ throw new Error(`Refusing unsafe Playwright socket directory: ${dir}`);
165
+ chmodSync(dir, 0o700);
166
+ env.PLAYWRIGHT_SOCKETS_DIR = dir;
167
+ }
168
+ return env;
169
+ }
170
+
171
+ export function tmpSocketRoot(env = playwrightCliEnv()): string {
172
+ // Match the shipped coreBundle makeSocketPath('cli', session). CLI children do
173
+ // not inherit USER/USERNAME, so the vendor's fallback username is "default".
174
+ const userHash = createHash("sha1").update(env.USERNAME || env.USER || "default").digest("hex").slice(0, 8);
175
+ return join(env.PLAYWRIGHT_SOCKETS_DIR || join(env.TMPDIR || tmpdir(), `pw-${userHash}`), "cli");
141
176
  }
142
177
 
143
178
  // On startup, adopt any iso-* sessions a previous daemon left behind so they still get reaped
@@ -146,15 +181,11 @@ function tmpSocketRoot(): string {
146
181
  function adoptOrphanedIsoSessions(): void {
147
182
  try {
148
183
  const root = tmpSocketRoot();
149
- for (const sub of readdirSync(root)) {
150
- let entries: string[];
151
- try { entries = readdirSync(`${root}/${sub}`); } catch { continue; }
152
- for (const f of entries) {
153
- const m = f.match(/^([0-9a-f]+-iso-[0-9a-f]+)/) || f.match(/^(iso-[0-9a-f]+)/);
154
- if (m && !isoLastUsed.has(m[1])) {
155
- isoLastUsed.set(m[1], Date.now());
156
- log(`adopted orphaned isolated session for reaping: ${m[1]}`);
157
- }
184
+ for (const f of readdirSync(root)) {
185
+ const sess = f.endsWith(".sock") ? f.slice(0, -5) : "";
186
+ if (isIsoSession(sess) && !isoLastUsed.has(sess)) {
187
+ isoLastUsed.set(sess, Date.now());
188
+ log(`adopted orphaned isolated session for reaping: ${sess}`);
158
189
  }
159
190
  }
160
191
  } catch {
@@ -174,7 +205,7 @@ function reapIdleIsoSessions(bin: string, binArgs: string[], workDir: string): v
174
205
  stdout: "ignore",
175
206
  stderr: "ignore",
176
207
  windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
177
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
208
+ env: playwrightCliEnv(),
178
209
  });
179
210
  log(`reaped idle isolated session (idle ${Math.round((now - last) / 1000)}s): ${sess}`);
180
211
  } catch (e) {
@@ -566,7 +597,7 @@ export async function serve() {
566
597
  const probe = Bun.spawn([pbin, ...pbinArgs, "tab-list", `-s=${target}`], {
567
598
  cwd: workDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
568
599
  windowsHide: true,
569
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
600
+ env: playwrightCliEnv(),
570
601
  });
571
602
  const probeStatus = await Promise.race([
572
603
  probe.exited,
@@ -735,7 +766,7 @@ export async function serve() {
735
766
  stdout: "pipe",
736
767
  stderr: "pipe",
737
768
  windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
738
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
769
+ env: playwrightCliEnv(),
739
770
  });
740
771
  const [listStatus, listOut] = await Promise.race([
741
772
  Promise.all([listProc.exited, new Response(listProc.stdout).text()]),
@@ -764,14 +795,6 @@ export async function serve() {
764
795
  }
765
796
 
766
797
  const childEnv: Record<string, string | undefined> = {
767
- PATH: process.env.PATH,
768
- HOME: HOME,
769
- USERPROFILE: process.env.USERPROFILE,
770
- TMPDIR: process.env.TMPDIR,
771
- DISPLAY: process.env.DISPLAY,
772
- XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
773
- DEBUG: process.env.DEBUG, // forward debug namespaces (e.g. pw:mcp:relay) for diagnostics
774
- PWDEBUG: process.env.PWDEBUG,
775
798
  ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
776
799
  ...passthroughEnv,
777
800
  // Enable extension bridge when credentials are present
@@ -779,19 +802,15 @@ export async function serve() {
779
802
  ? { PLAYWRIGHT_MCP_EXTENSION: "1" }
780
803
  : {}),
781
804
  };
782
- // For open commands: clean up stale sockets so a closed browser can be reopened
783
- if (isOpenCmd) {
784
- const tmpDir = (process.env.TMPDIR || "/tmp").replace(/\/$/, "");
785
- const playwrightTmpDir = `${tmpDir}/playwright-cli`;
805
+ // Only a remaining open needs cleanup; a successful probe converted it to
806
+ // goto and its live daemon socket must remain reachable.
807
+ if (filteredArgs[0] === "open") {
808
+ const socketRoot = tmpSocketRoot();
786
809
  try {
787
- const { readdirSync } = await import("fs");
788
- for (const sub of readdirSync(playwrightTmpDir)) {
789
- const subDir = `${playwrightTmpDir}/${sub}`;
790
- for (const f of readdirSync(subDir)) {
791
- if (f.startsWith(namespacedSession)) {
792
- const sockPath = `${subDir}/${f}`;
793
- try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
794
- }
810
+ for (const f of readdirSync(socketRoot)) {
811
+ if (f === `${namespacedSession}.sock`) {
812
+ const sockPath = join(socketRoot, f);
813
+ try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
795
814
  }
796
815
  }
797
816
  } catch {}
@@ -804,7 +823,7 @@ export async function serve() {
804
823
  stdout: "pipe",
805
824
  stderr: "pipe",
806
825
  windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
807
- env: childEnv,
826
+ env: playwrightCliEnv(childEnv),
808
827
  });
809
828
 
810
829
  const TIMEOUT = 60_000;
@@ -860,7 +879,7 @@ export async function serve() {
860
879
  Bun.spawn([bin, ...binArgs, "close", `-s=${namespacedSession}`], {
861
880
  cwd: runWorkDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
862
881
  windowsHide: true,
863
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
882
+ env: playwrightCliEnv(),
864
883
  });
865
884
  } catch {}
866
885
  sessionTimeouts.delete(namespacedSession);
package/serve.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, type Listener } from "./listeners.ts";
2
2
  import { file } from "bun";
3
3
  import { createHash, X509Certificate } from "crypto";
4
- import { mkdirSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
4
+ import { mkdirSync, lstatSync, chmodSync, unlinkSync, accessSync, readdirSync, realpathSync, constants as fsConstants } from "fs";
5
5
  import { join, resolve, relative, isAbsolute } from "path";
6
+ import { tmpdir } from "os";
6
7
  import {
7
8
  log,
8
9
  parseUrl,
@@ -136,8 +137,42 @@ export function inferSilentExtensionFailure(options: {
136
137
  return `Extension connection timeout after ${handshakeTimeoutMs}ms. Automatic recovery retry failed; reload the Playwright MCP Bridge extension at chrome://extensions and retry.\n`;
137
138
  }
138
139
 
139
- function tmpSocketRoot(): string {
140
- return `${(process.env.TMPDIR || "/tmp").replace(/\/$/, "")}/playwright-cli`;
140
+ // Every CLI child must agree on its socket namespace, including probes and reapers.
141
+ export function playwrightCliEnv(
142
+ overrides: Record<string, string | undefined> = {},
143
+ runtime = { env: process.env, platform: process.platform, uid: process.getuid?.() },
144
+ ): Record<string, string | undefined> {
145
+ const source = runtime.env;
146
+ const env: Record<string, string | undefined> = {
147
+ PATH: source.PATH,
148
+ HOME: source.HOME || HOME,
149
+ USERPROFILE: source.USERPROFILE,
150
+ TMPDIR: source.TMPDIR,
151
+ DISPLAY: source.DISPLAY,
152
+ XDG_RUNTIME_DIR: source.XDG_RUNTIME_DIR,
153
+ DEBUG: source.DEBUG,
154
+ PWDEBUG: source.PWDEBUG,
155
+ ...overrides,
156
+ PLAYWRIGHT_SOCKETS_DIR: source.PLAYWRIGHT_SOCKETS_DIR,
157
+ };
158
+ if (runtime.platform === "darwin") {
159
+ if (runtime.uid === undefined) throw new Error("Cannot determine socket directory owner");
160
+ const dir = source.PLAYWRIGHT_SOCKETS_DIR || `/tmp/pw-${runtime.uid}`;
161
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
162
+ const stat = lstatSync(dir);
163
+ if (!stat.isDirectory() || stat.uid !== runtime.uid)
164
+ throw new Error(`Refusing unsafe Playwright socket directory: ${dir}`);
165
+ chmodSync(dir, 0o700);
166
+ env.PLAYWRIGHT_SOCKETS_DIR = dir;
167
+ }
168
+ return env;
169
+ }
170
+
171
+ export function tmpSocketRoot(env = playwrightCliEnv()): string {
172
+ // Match the shipped coreBundle makeSocketPath('cli', session). CLI children do
173
+ // not inherit USER/USERNAME, so the vendor's fallback username is "default".
174
+ const userHash = createHash("sha1").update(env.USERNAME || env.USER || "default").digest("hex").slice(0, 8);
175
+ return join(env.PLAYWRIGHT_SOCKETS_DIR || join(env.TMPDIR || tmpdir(), `pw-${userHash}`), "cli");
141
176
  }
142
177
 
143
178
  // On startup, adopt any iso-* sessions a previous daemon left behind so they still get reaped
@@ -146,15 +181,11 @@ function tmpSocketRoot(): string {
146
181
  function adoptOrphanedIsoSessions(): void {
147
182
  try {
148
183
  const root = tmpSocketRoot();
149
- for (const sub of readdirSync(root)) {
150
- let entries: string[];
151
- try { entries = readdirSync(`${root}/${sub}`); } catch { continue; }
152
- for (const f of entries) {
153
- const m = f.match(/^([0-9a-f]+-iso-[0-9a-f]+)/) || f.match(/^(iso-[0-9a-f]+)/);
154
- if (m && !isoLastUsed.has(m[1])) {
155
- isoLastUsed.set(m[1], Date.now());
156
- log(`adopted orphaned isolated session for reaping: ${m[1]}`);
157
- }
184
+ for (const f of readdirSync(root)) {
185
+ const sess = f.endsWith(".sock") ? f.slice(0, -5) : "";
186
+ if (isIsoSession(sess) && !isoLastUsed.has(sess)) {
187
+ isoLastUsed.set(sess, Date.now());
188
+ log(`adopted orphaned isolated session for reaping: ${sess}`);
158
189
  }
159
190
  }
160
191
  } catch {
@@ -174,7 +205,7 @@ function reapIdleIsoSessions(bin: string, binArgs: string[], workDir: string): v
174
205
  stdout: "ignore",
175
206
  stderr: "ignore",
176
207
  windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
177
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
208
+ env: playwrightCliEnv(),
178
209
  });
179
210
  log(`reaped idle isolated session (idle ${Math.round((now - last) / 1000)}s): ${sess}`);
180
211
  } catch (e) {
@@ -566,7 +597,7 @@ export async function serve() {
566
597
  const probe = Bun.spawn([pbin, ...pbinArgs, "tab-list", `-s=${target}`], {
567
598
  cwd: workDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
568
599
  windowsHide: true,
569
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
600
+ env: playwrightCliEnv(),
570
601
  });
571
602
  const probeStatus = await Promise.race([
572
603
  probe.exited,
@@ -735,7 +766,7 @@ export async function serve() {
735
766
  stdout: "pipe",
736
767
  stderr: "pipe",
737
768
  windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
738
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
769
+ env: playwrightCliEnv(),
739
770
  });
740
771
  const [listStatus, listOut] = await Promise.race([
741
772
  Promise.all([listProc.exited, new Response(listProc.stdout).text()]),
@@ -764,14 +795,6 @@ export async function serve() {
764
795
  }
765
796
 
766
797
  const childEnv: Record<string, string | undefined> = {
767
- PATH: process.env.PATH,
768
- HOME: HOME,
769
- USERPROFILE: process.env.USERPROFILE,
770
- TMPDIR: process.env.TMPDIR,
771
- DISPLAY: process.env.DISPLAY,
772
- XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR,
773
- DEBUG: process.env.DEBUG, // forward debug namespaces (e.g. pw:mcp:relay) for diagnostics
774
- PWDEBUG: process.env.PWDEBUG,
775
798
  ...(clientName ? { PLAYWRIGHT_MCP_CLIENT_NAME: shortClientLabel(clientName) } : {}),
776
799
  ...passthroughEnv,
777
800
  // Enable extension bridge when credentials are present
@@ -779,19 +802,15 @@ export async function serve() {
779
802
  ? { PLAYWRIGHT_MCP_EXTENSION: "1" }
780
803
  : {}),
781
804
  };
782
- // For open commands: clean up stale sockets so a closed browser can be reopened
783
- if (isOpenCmd) {
784
- const tmpDir = (process.env.TMPDIR || "/tmp").replace(/\/$/, "");
785
- const playwrightTmpDir = `${tmpDir}/playwright-cli`;
805
+ // Only a remaining open needs cleanup; a successful probe converted it to
806
+ // goto and its live daemon socket must remain reachable.
807
+ if (filteredArgs[0] === "open") {
808
+ const socketRoot = tmpSocketRoot();
786
809
  try {
787
- const { readdirSync } = await import("fs");
788
- for (const sub of readdirSync(playwrightTmpDir)) {
789
- const subDir = `${playwrightTmpDir}/${sub}`;
790
- for (const f of readdirSync(subDir)) {
791
- if (f.startsWith(namespacedSession)) {
792
- const sockPath = `${subDir}/${f}`;
793
- try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
794
- }
810
+ for (const f of readdirSync(socketRoot)) {
811
+ if (f === `${namespacedSession}.sock`) {
812
+ const sockPath = join(socketRoot, f);
813
+ try { unlinkSync(sockPath); log(`Removed stale socket: ${sockPath}`); } catch {}
795
814
  }
796
815
  }
797
816
  } catch {}
@@ -804,7 +823,7 @@ export async function serve() {
804
823
  stdout: "pipe",
805
824
  stderr: "pipe",
806
825
  windowsHide: true, // hide the CLI child's console; the user's Chrome (a GUI grandchild) stays visible
807
- env: childEnv,
826
+ env: playwrightCliEnv(childEnv),
808
827
  });
809
828
 
810
829
  const TIMEOUT = 60_000;
@@ -860,7 +879,7 @@ export async function serve() {
860
879
  Bun.spawn([bin, ...binArgs, "close", `-s=${namespacedSession}`], {
861
880
  cwd: runWorkDir, stdin: "ignore", stdout: "ignore", stderr: "ignore",
862
881
  windowsHide: true,
863
- env: { PATH: process.env.PATH, HOME: HOME, USERPROFILE: process.env.USERPROFILE },
882
+ env: playwrightCliEnv(),
864
883
  });
865
884
  } catch {}
866
885
  sessionTimeouts.delete(namespacedSession);