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 +2 -2
- package/listeners.ts +2 -2
- package/package.json +1 -1
- package/rechrome.js +22 -1
- package/rechrome.ts +22 -1
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
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));
|