rechrome 1.38.2 → 1.38.3

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/listeners.js CHANGED
@@ -109,7 +109,7 @@ async function writeListeners(config) {
109
109
  `, { mode: 384 });
110
110
  await rename(temp, LISTENERS_FILE);
111
111
  }
112
- var SCOPED_COMMANDS = new Set(["open", "goto", "tab-new", "tab-list", "tab-select", "tab-close", "snapshot", "screenshot", "click", "dblclick", "fill", "type", "press", "hover", "select", "check", "uncheck", "go-back", "go-forward", "reload", "resize", "close", "fill-secret"]);
112
+ var SCOPED_COMMANDS = new Set(["open", "goto", "tab-new", "tab-list", "tab-select", "tab-close", "snapshot", "screenshot", "click", "dblclick", "fill", "type", "press", "hover", "select", "check", "uncheck", "go-back", "go-forward", "reload", "resize", "close", "fill-secret", "console", "dialog-accept", "dialog-dismiss"]);
113
113
  function authorizeProfileRequest(listener, body) {
114
114
  if (!body || Array.isArray(body) || !body.identity || typeof body.identity.key !== "string" || !body.identity.key || !Array.isArray(body.args) || body.args.some((a) => typeof a !== "string"))
115
115
  throw new Error("Scoped requests require identity.key, identity.profile and string args");
@@ -121,7 +121,7 @@ function authorizeProfileRequest(listener, body) {
121
121
  const args = body.args.filter((a) => /^-s=[a-zA-Z0-9_-]{1,64}$/.test(a) === false);
122
122
  if (!(args[0] === "upload" && listener.uploads !== undefined) && !SCOPED_COMMANDS.has(args[0]))
123
123
  throw new Error("Command is unavailable on profile-scoped listeners; use the local management listener for arbitrary code or host access");
124
- if (body.args.filter((a) => a.startsWith("-s=")).length > 1 || args.slice(1).some((a) => a.startsWith("-") && !(args[0] === "screenshot" && a === "--full-page") && !(args[0] === "fill-secret" && (a === "--submit" || a === "--totp" || a.startsWith("--allow-domain=")))))
124
+ if (body.args.filter((a) => a.startsWith("-s=")).length > 1 || args.slice(1).some((a) => a.startsWith("-") && !(args[0] === "screenshot" && a === "--full-page") && !(args[0] === "console" && a === "--clear") && !(args[0] === "fill-secret" && (a === "--submit" || a === "--totp" || a.startsWith("--allow-domain=")))))
125
125
  throw new Error("CLI option overrides are unavailable on profile-scoped listeners");
126
126
  if (["open", "goto", "tab-new"].includes(args[0]) && args[1] && !/^https?:\/\//i.test(args[1]) && args[1] !== "about:blank")
127
127
  throw new Error("Scoped navigation accepts only HTTP(S) or about:blank");
package/listeners.ts CHANGED
@@ -96,7 +96,7 @@ export async function writeListeners(config: ListenerConfig): Promise<void> {
96
96
 
97
97
  // Restricted listeners deliberately exclude arbitrary code, host filesystem access,
98
98
  // attachment/config overrides, and commands that manage all browser sessions.
99
- const SCOPED_COMMANDS = new Set(["open", "goto", "tab-new", "tab-list", "tab-select", "tab-close", "snapshot", "screenshot", "click", "dblclick", "fill", "type", "press", "hover", "select", "check", "uncheck", "go-back", "go-forward", "reload", "resize", "close", "fill-secret"]);
99
+ const SCOPED_COMMANDS = new Set(["open", "goto", "tab-new", "tab-list", "tab-select", "tab-close", "snapshot", "screenshot", "click", "dblclick", "fill", "type", "press", "hover", "select", "check", "uncheck", "go-back", "go-forward", "reload", "resize", "close", "fill-secret", "console", "dialog-accept", "dialog-dismiss"]);
100
100
  export function authorizeProfileRequest(listener: Listener, body: any): string {
101
101
  if (!body || Array.isArray(body) || !body.identity || typeof body.identity.key !== "string" || !body.identity.key || !Array.isArray(body.args) || body.args.some((a: unknown) => typeof a !== "string")) throw new Error("Scoped requests require identity.key, identity.profile and string args");
102
102
  const profile = body.identity.profile;
@@ -104,7 +104,7 @@ export function authorizeProfileRequest(listener: Listener, body: any): string {
104
104
  if (body.env?.PLAYWRIGHT_MCP_PROFILE_DIRECTORY && body.env.PLAYWRIGHT_MCP_PROFILE_DIRECTORY !== profile) throw new Error("Profile override does not match request identity");
105
105
  const args = body.args.filter((a: string) => /^-s=[a-zA-Z0-9_-]{1,64}$/.test(a) === false);
106
106
  if (!(args[0] === "upload" && listener.uploads !== undefined) && !SCOPED_COMMANDS.has(args[0])) throw new Error("Command is unavailable on profile-scoped listeners; use the local management listener for arbitrary code or host access");
107
- if (body.args.filter((a: string) => a.startsWith("-s=")).length > 1 || args.slice(1).some((a: string) => a.startsWith("-") && !(args[0] === "screenshot" && a === "--full-page") && !(args[0] === "fill-secret" && (a === "--submit" || a === "--totp" || a.startsWith("--allow-domain="))))) throw new Error("CLI option overrides are unavailable on profile-scoped listeners");
107
+ if (body.args.filter((a: string) => a.startsWith("-s=")).length > 1 || args.slice(1).some((a: string) => a.startsWith("-") && !(args[0] === "screenshot" && a === "--full-page") && !(args[0] === "console" && a === "--clear") && !(args[0] === "fill-secret" && (a === "--submit" || a === "--totp" || a.startsWith("--allow-domain="))))) throw new Error("CLI option overrides are unavailable on profile-scoped listeners");
108
108
  if (["open", "goto", "tab-new"].includes(args[0]) && args[1] && !/^https?:\/\//i.test(args[1]) && args[1] !== "about:blank") throw new Error("Scoped navigation accepts only HTTP(S) or about:blank");
109
109
  // Upload is handled by the byte-only receiver; the caller cannot supply host paths.
110
110
  if (args[0] === "upload" && (args.length !== 1 || !body.upload)) throw new Error("Upload requires stdin bytes; host paths are unavailable");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rechrome",
3
- "version": "1.38.2",
3
+ "version": "1.38.3",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/snomiao/rechrome.git"
package/rechrome.js CHANGED
@@ -4166,9 +4166,30 @@ const selfCommand = () => [process.execPath, Bun.main];
4166
4166
  */
4167
4167
  const bridgedUrls = new Set<string>();
4168
4168
 
4169
+ /**
4170
+ * RECHROME_URL may address one of this machine's own scoped listeners directly (e.g. its tailnet
4171
+ * IP), which refuses management-only commands. Same user, same daemon: go through the loopback
4172
+ * management listener instead. The session is unchanged, since it is keyed by identity, not by
4173
+ * listener. HTTPS is left alone: the daemon's certificate doesn't name 127.0.0.1.
4174
+ */
4175
+ export function preferManagementListener(url: string, config: { listeners: Listener[] } | null): string {
4176
+ const { key, host, port, protocol } = parseUrl(url);
4177
+ const own = config?.listeners.find(l => l.key === key);
4178
+ const management = config?.listeners.find(l => l.profiles === "*" && isLoopback(l.host));
4179
+ if (!key || !own || own.profiles === "*" || !management || protocol !== "http") return url;
4180
+ if (own.host !== host.replace(/^\[|\]$/g, "") || own.port !== port) return url;
4181
+ const source = new URL(url);
4182
+ const next = new URL(`http://${listenerAddress(management)}${normalizePrefix(management.prefix)}`);
4183
+ next.search = source.search;
4184
+ const fragment = new URLSearchParams(source.hash.slice(1).replace(/^\?/, ""));
4185
+ fragment.set("key", management.key);
4186
+ next.hash = fragment.toString();
4187
+ return next.toString();
4188
+ }
4189
+
4169
4190
  export async function transportUrl(url: string): Promise<string> {
4170
4191
  const parts = shareLinkParts(url);
4171
- if (!parts?.webrtc) return url;
4192
+ if (!parts?.webrtc) return preferManagementListener(url, await readListeners().catch(() => null));
4172
4193
  const info = await spawnBridge(parts.token, parts.origin, selfCommand());
4173
4194
  const bridged = new URL(`http://127.0.0.1:${info.port}/`);
4174
4195
  new URL(url).searchParams.forEach((v, k) => bridged.searchParams.set(k, v));
package/rechrome.ts CHANGED
@@ -4166,9 +4166,30 @@ const selfCommand = () => [process.execPath, Bun.main];
4166
4166
  */
4167
4167
  const bridgedUrls = new Set<string>();
4168
4168
 
4169
+ /**
4170
+ * RECHROME_URL may address one of this machine's own scoped listeners directly (e.g. its tailnet
4171
+ * IP), which refuses management-only commands. Same user, same daemon: go through the loopback
4172
+ * management listener instead. The session is unchanged, since it is keyed by identity, not by
4173
+ * listener. HTTPS is left alone: the daemon's certificate doesn't name 127.0.0.1.
4174
+ */
4175
+ export function preferManagementListener(url: string, config: { listeners: Listener[] } | null): string {
4176
+ const { key, host, port, protocol } = parseUrl(url);
4177
+ const own = config?.listeners.find(l => l.key === key);
4178
+ const management = config?.listeners.find(l => l.profiles === "*" && isLoopback(l.host));
4179
+ if (!key || !own || own.profiles === "*" || !management || protocol !== "http") return url;
4180
+ if (own.host !== host.replace(/^\[|\]$/g, "") || own.port !== port) return url;
4181
+ const source = new URL(url);
4182
+ const next = new URL(`http://${listenerAddress(management)}${normalizePrefix(management.prefix)}`);
4183
+ next.search = source.search;
4184
+ const fragment = new URLSearchParams(source.hash.slice(1).replace(/^\?/, ""));
4185
+ fragment.set("key", management.key);
4186
+ next.hash = fragment.toString();
4187
+ return next.toString();
4188
+ }
4189
+
4169
4190
  export async function transportUrl(url: string): Promise<string> {
4170
4191
  const parts = shareLinkParts(url);
4171
- if (!parts?.webrtc) return url;
4192
+ if (!parts?.webrtc) return preferManagementListener(url, await readListeners().catch(() => null));
4172
4193
  const info = await spawnBridge(parts.token, parts.origin, selfCommand());
4173
4194
  const bridged = new URL(`http://127.0.0.1:${info.port}/`);
4174
4195
  new URL(url).searchParams.forEach((v, k) => bridged.searchParams.set(k, v));