rechrome 1.12.2 → 1.12.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/extension/connect.html +1 -1
- package/extension/lib/background.mjs +518 -262
- package/extension/lib/ui/authToken.css +34 -6
- package/extension/lib/ui/authToken.js +11638 -6158
- package/extension/lib/ui/connect.js +52 -60
- package/extension/lib/ui/status.js +31 -50
- package/extension/manifest.json +8 -7
- package/extension/status.html +1 -1
- package/package.json +1 -1
- package/rech.js +25 -12
- package/rech.ts +25 -12
|
@@ -1,50 +1,48 @@
|
|
|
1
|
-
import { c as clientExports, j as jsxRuntimeExports, r as reactExports,
|
|
1
|
+
import { c as clientExports, j as jsxRuntimeExports, r as reactExports, A as AuthTokenSection, T as TabItem, B as Button, g as getOrCreateAuthToken } from "./authToken.js";
|
|
2
2
|
const SUPPORTED_PROTOCOL_VERSION = 2;
|
|
3
3
|
const ConnectApp = () => {
|
|
4
4
|
const [tabs, setTabs] = reactExports.useState([]);
|
|
5
5
|
const [status, setStatus] = reactExports.useState(null);
|
|
6
|
-
const [showButtons, setShowButtons] = reactExports.useState(true);
|
|
7
6
|
const [showTabList, setShowTabList] = reactExports.useState(true);
|
|
8
7
|
const [clientInfo, setClientInfo] = reactExports.useState("unknown");
|
|
9
|
-
const
|
|
10
|
-
|
|
8
|
+
const setError = reactExports.useCallback((message) => {
|
|
9
|
+
setShowTabList(false);
|
|
10
|
+
setStatus({ type: "error", message });
|
|
11
|
+
}, []);
|
|
11
12
|
reactExports.useEffect(() => {
|
|
12
13
|
const runAsync = async () => {
|
|
13
14
|
const params = new URLSearchParams(window.location.search);
|
|
14
15
|
const relayUrl = params.get("mcpRelayUrl");
|
|
15
16
|
if (!relayUrl) {
|
|
16
|
-
|
|
17
|
+
setError("Missing mcpRelayUrl parameter in URL.");
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
20
|
try {
|
|
20
21
|
const host = new URL(relayUrl).hostname;
|
|
21
22
|
if (host !== "127.0.0.1" && host !== "[::1]") {
|
|
22
|
-
|
|
23
|
+
setError(`Playwright extension only allows loopback connections (127.0.0.1 or [::1]). Received host: ${host}`);
|
|
23
24
|
return;
|
|
24
25
|
}
|
|
25
26
|
} catch (e) {
|
|
26
|
-
|
|
27
|
+
setError(`Invalid mcpRelayUrl parameter in URL: ${relayUrl}. ${e}`);
|
|
27
28
|
return;
|
|
28
29
|
}
|
|
29
|
-
setMcpRelayUrl(relayUrl);
|
|
30
|
-
let info = "unknown";
|
|
31
30
|
try {
|
|
32
31
|
const client = JSON.parse(params.get("client") || "{}");
|
|
33
|
-
info = `${client.name}
|
|
32
|
+
const info = `${client.name || "unknown"}`;
|
|
34
33
|
setClientInfo(info);
|
|
35
34
|
setStatus({
|
|
36
35
|
type: "connecting",
|
|
37
|
-
message:
|
|
36
|
+
message: `"${info}" is trying to connect to the Playwright Extension.`
|
|
38
37
|
});
|
|
39
38
|
} catch (e) {
|
|
40
39
|
setStatus({ type: "error", message: "Failed to parse client version." });
|
|
41
40
|
return;
|
|
42
41
|
}
|
|
43
42
|
const parsedVersion = parseInt(params.get("protocolVersion") ?? "", 10);
|
|
44
|
-
const
|
|
45
|
-
if (
|
|
43
|
+
const requestedVersion = isNaN(parsedVersion) ? 1 : parsedVersion;
|
|
44
|
+
if (requestedVersion > SUPPORTED_PROTOCOL_VERSION) {
|
|
46
45
|
const extensionVersion = chrome.runtime.getManifest().version;
|
|
47
|
-
setShowButtons(false);
|
|
48
46
|
setShowTabList(false);
|
|
49
47
|
setStatus({
|
|
50
48
|
type: "error",
|
|
@@ -54,37 +52,33 @@ const ConnectApp = () => {
|
|
|
54
52
|
});
|
|
55
53
|
return;
|
|
56
54
|
}
|
|
55
|
+
const response = await chrome.runtime.sendMessage({ type: "connectionRequested", mcpRelayUrl: relayUrl, protocolVersion: requestedVersion });
|
|
56
|
+
if (!response.success) {
|
|
57
|
+
setError(response.error);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
57
60
|
const expectedToken = getOrCreateAuthToken();
|
|
58
61
|
const token = params.get("token");
|
|
59
62
|
if (token === expectedToken) {
|
|
60
|
-
await
|
|
61
|
-
await handleConnectToTab(void 0, info);
|
|
63
|
+
await handleConnectToTab();
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
if (token) {
|
|
65
|
-
|
|
67
|
+
setError("Invalid token provided.");
|
|
66
68
|
return;
|
|
67
69
|
}
|
|
68
|
-
|
|
69
|
-
if (params.get("newTab") === "true") {
|
|
70
|
-
setNewTab(true);
|
|
70
|
+
if (params.get("newTab") === "true")
|
|
71
71
|
setShowTabList(false);
|
|
72
|
-
|
|
72
|
+
else
|
|
73
73
|
await loadTabs();
|
|
74
|
-
}
|
|
75
74
|
};
|
|
76
75
|
void runAsync();
|
|
76
|
+
const keepalive = setInterval(() => {
|
|
77
|
+
chrome.runtime.sendMessage({ type: "keepalive" }).catch(() => {
|
|
78
|
+
});
|
|
79
|
+
}, 2e4);
|
|
80
|
+
return () => clearInterval(keepalive);
|
|
77
81
|
}, []);
|
|
78
|
-
const handleReject = reactExports.useCallback((message) => {
|
|
79
|
-
setShowButtons(false);
|
|
80
|
-
setShowTabList(false);
|
|
81
|
-
setStatus({ type: "error", message });
|
|
82
|
-
}, []);
|
|
83
|
-
const connectToMCPRelay = reactExports.useCallback(async (mcpRelayUrl2) => {
|
|
84
|
-
const response = await chrome.runtime.sendMessage({ type: "connectToMCPRelay", mcpRelayUrl: mcpRelayUrl2 });
|
|
85
|
-
if (!response.success)
|
|
86
|
-
handleReject(response.error);
|
|
87
|
-
}, [handleReject]);
|
|
88
82
|
const loadTabs = reactExports.useCallback(async () => {
|
|
89
83
|
const response = await chrome.runtime.sendMessage({ type: "getTabs" });
|
|
90
84
|
if (response.success)
|
|
@@ -92,58 +86,55 @@ const ConnectApp = () => {
|
|
|
92
86
|
else
|
|
93
87
|
setStatus({ type: "error", message: "Failed to load tabs: " + response.error });
|
|
94
88
|
}, []);
|
|
95
|
-
const handleConnectToTab = reactExports.useCallback(async (tab
|
|
96
|
-
const displayName = clientInfoOverride || clientInfo;
|
|
97
|
-
setShowButtons(false);
|
|
89
|
+
const handleConnectToTab = reactExports.useCallback(async (tab) => {
|
|
98
90
|
setShowTabList(false);
|
|
99
91
|
try {
|
|
100
92
|
const response = await chrome.runtime.sendMessage({
|
|
101
93
|
type: "connectToTab",
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
windowId: tab?.windowId
|
|
94
|
+
tab,
|
|
95
|
+
clientName: clientInfo
|
|
105
96
|
});
|
|
106
|
-
if (response
|
|
107
|
-
setStatus({ type: "connected", message: `
|
|
97
|
+
if (response == null ? void 0 : response.success) {
|
|
98
|
+
setStatus({ type: "connected", message: `"${clientInfo}" connected.` });
|
|
108
99
|
} else {
|
|
109
100
|
setStatus({
|
|
110
101
|
type: "error",
|
|
111
|
-
message: response
|
|
102
|
+
message: (response == null ? void 0 : response.error) || `"${clientInfo}" failed to connect.`
|
|
112
103
|
});
|
|
113
104
|
}
|
|
114
105
|
} catch (e) {
|
|
115
106
|
setStatus({
|
|
116
107
|
type: "error",
|
|
117
|
-
message: `
|
|
108
|
+
message: `"${clientInfo}" failed to connect: ${e}`
|
|
118
109
|
});
|
|
119
110
|
}
|
|
120
|
-
}, [clientInfo
|
|
111
|
+
}, [clientInfo]);
|
|
121
112
|
reactExports.useEffect(() => {
|
|
122
113
|
const listener = (message) => {
|
|
123
|
-
if (message.type === "
|
|
124
|
-
|
|
114
|
+
if (message.type === "pendingConnectionClosed") {
|
|
115
|
+
setError("Pending client connection closed.");
|
|
116
|
+
document.title = "Playwright Extension";
|
|
117
|
+
}
|
|
125
118
|
};
|
|
126
119
|
chrome.runtime.onMessage.addListener(listener);
|
|
127
120
|
return () => {
|
|
128
121
|
chrome.runtime.onMessage.removeListener(listener);
|
|
129
122
|
};
|
|
130
|
-
}, [
|
|
123
|
+
}, [setError]);
|
|
131
124
|
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "app-container", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "content-wrapper", children: [
|
|
132
|
-
status && /* @__PURE__ */ jsxRuntimeExports.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "reject", onClick: () => handleReject("Connection rejected. This tab can be closed."), children: "Reject" })
|
|
137
|
-
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "reject", onClick: () => handleReject("Connection rejected. This tab can be closed."), children: "Reject" }) })
|
|
125
|
+
status && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "status-container", children: /* @__PURE__ */ jsxRuntimeExports.jsx(StatusBanner, { status }) }),
|
|
126
|
+
(status == null ? void 0 : status.type) === "connecting" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "warning-banner", children: [
|
|
127
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "⚠️ Warning:" }),
|
|
128
|
+
" Allowing this connection exposes the entire browser to the client, including any signed-in sessions, cookies, and content in other tabs and windows. Once approved, the client may also be able to reconnect later without showing this dialog again, unless you regenerate the token below and then restart the browser."
|
|
138
129
|
] }),
|
|
139
|
-
status
|
|
130
|
+
(status == null ? void 0 : status.type) === "connecting" && /* @__PURE__ */ jsxRuntimeExports.jsx(AuthTokenSection, {}),
|
|
140
131
|
showTabList && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
|
|
141
|
-
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tab-section-title", children: "
|
|
132
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tab-section-title", children: "You can drag tabs into the Playwright group later to make them accessible to the client. Optionally, select a tab to allow and immediately switch to it:" }),
|
|
142
133
|
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: tabs.map((tab) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
143
134
|
TabItem,
|
|
144
135
|
{
|
|
145
136
|
tab,
|
|
146
|
-
button: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "primary", onClick: () => handleConnectToTab(tab), children: "
|
|
137
|
+
button: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "primary", onClick: () => handleConnectToTab(tab), children: "Allow & select" })
|
|
147
138
|
},
|
|
148
139
|
tab.id
|
|
149
140
|
)) })
|
|
@@ -151,15 +142,16 @@ const ConnectApp = () => {
|
|
|
151
142
|
] }) });
|
|
152
143
|
};
|
|
153
144
|
const VersionMismatchError = ({ extensionVersion }) => {
|
|
154
|
-
const readmeUrl = "https://github.com/microsoft/playwright
|
|
155
|
-
const
|
|
145
|
+
const readmeUrl = "https://github.com/microsoft/playwright/blob/main/packages/extension/README.md";
|
|
146
|
+
const chromeWebStoreUrl = "https://chromewebstore.google.com/detail/playwright-extension/mmlmfjhmonkocbjadbfplnigmagldckm";
|
|
156
147
|
return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
|
|
157
|
-
"Playwright
|
|
148
|
+
"Playwright client trying to connect requires newer extension version (current version: ",
|
|
158
149
|
extensionVersion,
|
|
159
150
|
").",
|
|
160
151
|
" ",
|
|
161
|
-
|
|
162
|
-
|
|
152
|
+
"Update ",
|
|
153
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: chromeWebStoreUrl, target: "_blank", rel: "noopener noreferrer", children: "Playwright Extension" }),
|
|
154
|
+
" from the Chrome Web Store to the latest version.",
|
|
163
155
|
" ",
|
|
164
156
|
"See ",
|
|
165
157
|
/* @__PURE__ */ jsxRuntimeExports.jsx("a", { href: readmeUrl, target: "_blank", rel: "noopener noreferrer", children: "installation instructions" }),
|
|
@@ -1,66 +1,47 @@
|
|
|
1
|
-
import { c as clientExports, j as jsxRuntimeExports, r as reactExports,
|
|
1
|
+
import { c as clientExports, j as jsxRuntimeExports, r as reactExports, B as Button, T as TabItem, A as AuthTokenSection } from "./authToken.js";
|
|
2
2
|
const StatusApp = () => {
|
|
3
|
-
const [
|
|
3
|
+
const [connectedTabs, setConnectedTabs] = reactExports.useState([]);
|
|
4
|
+
const [clientName, setClientName] = reactExports.useState(void 0);
|
|
4
5
|
reactExports.useEffect(() => {
|
|
5
6
|
void loadStatus();
|
|
6
7
|
}, []);
|
|
7
8
|
const loadStatus = async () => {
|
|
8
|
-
const {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return { id: tab.id, windowId: tab.windowId, title: tab.title, url: tab.url, favIconUrl: tab.favIconUrl };
|
|
13
|
-
} catch {
|
|
14
|
-
return null;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
const resolved = await Promise.all(
|
|
18
|
-
rawConnections.map(async (c) => {
|
|
19
|
-
const connectedTab = await fetchTab(c.connectedTabId) ?? void 0;
|
|
20
|
-
const playwrightTabs = (await Promise.all(c.playwrightTabIds.map(fetchTab))).filter((t) => t !== null);
|
|
21
|
-
return { ...c, connectedTab, playwrightTabs };
|
|
22
|
-
})
|
|
23
|
-
);
|
|
24
|
-
setConnections(resolved);
|
|
9
|
+
const { connectedTabIds, clientName: clientName2 } = await chrome.runtime.sendMessage({ type: "getConnectionStatus" });
|
|
10
|
+
const tabs = await Promise.all((connectedTabIds ?? []).map((tabId) => chrome.tabs.get(tabId)));
|
|
11
|
+
setConnectedTabs(tabs);
|
|
12
|
+
setClientName(clientName2);
|
|
25
13
|
};
|
|
26
14
|
const openTab = async (tabId) => {
|
|
27
15
|
await chrome.tabs.update(tabId, { active: true });
|
|
28
16
|
window.close();
|
|
29
17
|
};
|
|
30
|
-
const disconnect = async (
|
|
31
|
-
await chrome.runtime.sendMessage({ type: "disconnect"
|
|
32
|
-
|
|
18
|
+
const disconnect = async () => {
|
|
19
|
+
await chrome.runtime.sendMessage({ type: "disconnect" });
|
|
20
|
+
window.close();
|
|
33
21
|
};
|
|
34
22
|
return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "app-container", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "content-wrapper", children: [
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
tab: c.connectedTab,
|
|
47
|
-
button: /* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "primary", onClick: () => disconnect(c.mcpRelayUrl), children: "Disconnect" }),
|
|
48
|
-
onClick: () => openTab(c.connectedTabId)
|
|
49
|
-
}
|
|
50
|
-
)
|
|
23
|
+
connectedTabs.length > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
|
|
24
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "connection-header", children: [
|
|
25
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "client-info", children: [
|
|
26
|
+
"Connected to ",
|
|
27
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs("strong", { children: [
|
|
28
|
+
'"',
|
|
29
|
+
clientName || "unknown",
|
|
30
|
+
'"'
|
|
31
|
+
] })
|
|
32
|
+
] }),
|
|
33
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Button, { variant: "primary", onClick: disconnect, children: "Disconnect" })
|
|
51
34
|
] }),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
] })
|
|
63
|
-
] }, c.mcpRelayUrl)),
|
|
35
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "tab-section-title", children: connectedTabs.length === 1 ? "Accessible page:" : "Accessible pages:" }),
|
|
36
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx("div", { children: connectedTabs.map((tab) => /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
37
|
+
TabItem,
|
|
38
|
+
{
|
|
39
|
+
tab,
|
|
40
|
+
onClick: () => openTab(tab.id)
|
|
41
|
+
},
|
|
42
|
+
tab.id
|
|
43
|
+
)) })
|
|
44
|
+
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "status-banner", children: "No clients are currently connected. You can connect from the Playwright CLI or MCP server by passing the --extension flag." }),
|
|
64
45
|
/* @__PURE__ */ jsxRuntimeExports.jsx(AuthTokenSection, {})
|
|
65
46
|
] }) });
|
|
66
47
|
};
|
package/extension/manifest.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"description": "Share browser tabs with Playwright MCP server (multi-tab fork: Playwright can open and control multiple tabs)",
|
|
3
|
+
"name": "Playwright Extension",
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"description": "Connect your browser to AI agents through Playwright MCP server and CLI. Enables AI-driven web testing, debugging, and automation.",
|
|
7
6
|
"permissions": [
|
|
8
7
|
"debugger",
|
|
9
8
|
"activeTab",
|
|
10
|
-
"tabs"
|
|
9
|
+
"tabs",
|
|
10
|
+
"tabGroups"
|
|
11
11
|
],
|
|
12
12
|
"host_permissions": [
|
|
13
13
|
"<all_urls>"
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"type": "module"
|
|
18
18
|
},
|
|
19
19
|
"action": {
|
|
20
|
-
"default_title": "Playwright
|
|
20
|
+
"default_title": "Playwright Extension",
|
|
21
21
|
"default_icon": {
|
|
22
22
|
"16": "icons/icon-16.png",
|
|
23
23
|
"32": "icons/icon-32.png",
|
|
@@ -30,5 +30,6 @@
|
|
|
30
30
|
"32": "icons/icon-32.png",
|
|
31
31
|
"48": "icons/icon-48.png",
|
|
32
32
|
"128": "icons/icon-128.png"
|
|
33
|
-
}
|
|
33
|
+
},
|
|
34
|
+
"key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwRsUUO4mmbCi4JpmrIoIw31iVW9+xUJRZ6nSzya17PQkaUPDxe1IpgM+vpd/xB6mJWlJSyE1Lj95c0sbomGfVY1M0zUeKbaRVcAb+/a6m59gNR+ubFlmTX0nK9/8fE2FpRB9D+4N5jyeIPQuASW/0oswI2/ijK7hH5NTRX8gWc/ROMSgUj7rKhTAgBrICt/NsStgDPsxRTPPJnhJ/ViJtM1P5KsSYswE987DPoFnpmkFpq8g1ae0eYbQfXy55ieaacC4QWyJPj3daU2kMfBQw7MXnnk0H/WDxouMOIHnd8MlQxpEMqAihj7KpuONH+MUhuj9HEQo4df6bSaIuQ0b4QIDAQAB"
|
|
34
35
|
}
|
package/extension/status.html
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
-
<title>Playwright
|
|
6
|
+
<title>Playwright Extension Status</title>
|
|
7
7
|
<script type="module" crossorigin src="/lib/ui/status.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/lib/ui/authToken.js">
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/lib/ui/authToken.css">
|
package/package.json
CHANGED
package/rech.js
CHANGED
|
@@ -254,7 +254,7 @@ export const EXTENSION_DIST_DIR = join(process.env.HOME!, ".rechrome", "extensio
|
|
|
254
254
|
|
|
255
255
|
// With the manifest `key` field set, Chrome derives this ID deterministically from the key (not the path),
|
|
256
256
|
// so we can locate the extension by ID even when the on-disk path differs from what Chrome stored.
|
|
257
|
-
export const EXTENSION_ID = "
|
|
257
|
+
export const EXTENSION_ID = "mmlmfjhmonkocbjadbfplnigmagldckm";
|
|
258
258
|
|
|
259
259
|
async function ensureExtensionDistInstalled(): Promise<string> {
|
|
260
260
|
const source = existsSync(BUNDLED_EXTENSION_DIST_DIR)
|
|
@@ -279,28 +279,41 @@ async function findInstalledExtension(
|
|
|
279
279
|
const cache = await readChromeProfileCache();
|
|
280
280
|
const profiles = profileDir ? [profileDir] : (cache ? Object.keys(cache) : []);
|
|
281
281
|
// Resolve our known-good install paths up front for path-based fallback matching.
|
|
282
|
+
// LEGACY_EXTENSION_DIST_DIR is intentionally excluded: it points at the pre-V2 multi-tab
|
|
283
|
+
// bridge, which is incompatible with the current cdpRelayV2 relay — matching it would hand
|
|
284
|
+
// setup a stale, broken extension.
|
|
282
285
|
const knownPaths = new Set<string>();
|
|
283
|
-
for (const p of [EXTENSION_DIST_DIR, BUNDLED_EXTENSION_DIST_DIR
|
|
286
|
+
for (const p of [EXTENSION_DIST_DIR, BUNDLED_EXTENSION_DIST_DIR]) {
|
|
284
287
|
try { knownPaths.add(realpathSync(p)); } catch {}
|
|
285
288
|
}
|
|
289
|
+
// Read each profile's settings once so we can prioritize stable-ID matches over path fallbacks.
|
|
290
|
+
const perProfile: Array<{ prof: string; settings: Record<string, any> }> = [];
|
|
286
291
|
for (const prof of profiles) {
|
|
287
292
|
const prefsPath = join(userDataDir, prof, "Secure Preferences");
|
|
288
293
|
const f = file(prefsPath);
|
|
289
294
|
if (!(await f.exists())) continue;
|
|
290
295
|
try {
|
|
291
296
|
const data = JSON.parse(await f.text());
|
|
292
|
-
|
|
293
|
-
for (const [extId, info] of Object.entries(settings as Record<string, any>)) {
|
|
294
|
-
if (!info?.path || info.state === 0) continue; // state 0 = explicitly disabled
|
|
295
|
-
// Primary: stable ID match (works when manifest `key` is set, regardless of path).
|
|
296
|
-
if (extId === EXTENSION_ID) return { id: extId, profile: prof };
|
|
297
|
-
// Fallback: path equality for legacy installs without a stable key.
|
|
298
|
-
let storedPath = info.path as string;
|
|
299
|
-
try { storedPath = realpathSync(storedPath); } catch {}
|
|
300
|
-
if (knownPaths.has(storedPath)) return { id: extId, profile: prof };
|
|
301
|
-
}
|
|
297
|
+
perProfile.push({ prof, settings: (data?.extensions?.settings ?? {}) as Record<string, any> });
|
|
302
298
|
} catch {}
|
|
303
299
|
}
|
|
300
|
+
// Pass 1: stable ID match (manifest `key` set, path-independent). This must win over any path
|
|
301
|
+
// fallback so a stale legacy install sitting on a known path can't shadow the current extension.
|
|
302
|
+
for (const { prof, settings } of perProfile) {
|
|
303
|
+
for (const [extId, info] of Object.entries(settings)) {
|
|
304
|
+
if (!info?.path || info.state === 0) continue; // state 0 = explicitly disabled
|
|
305
|
+
if (extId === EXTENSION_ID) return { id: extId, profile: prof };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Pass 2: path equality fallback for legacy keyless installs without a stable ID.
|
|
309
|
+
for (const { prof, settings } of perProfile) {
|
|
310
|
+
for (const [extId, info] of Object.entries(settings)) {
|
|
311
|
+
if (!info?.path || info.state === 0) continue;
|
|
312
|
+
let storedPath = info.path as string;
|
|
313
|
+
try { storedPath = realpathSync(storedPath); } catch {}
|
|
314
|
+
if (knownPaths.has(storedPath)) return { id: extId, profile: prof };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
304
317
|
return null;
|
|
305
318
|
}
|
|
306
319
|
|
package/rech.ts
CHANGED
|
@@ -254,7 +254,7 @@ export const EXTENSION_DIST_DIR = join(process.env.HOME!, ".rechrome", "extensio
|
|
|
254
254
|
|
|
255
255
|
// With the manifest `key` field set, Chrome derives this ID deterministically from the key (not the path),
|
|
256
256
|
// so we can locate the extension by ID even when the on-disk path differs from what Chrome stored.
|
|
257
|
-
export const EXTENSION_ID = "
|
|
257
|
+
export const EXTENSION_ID = "mmlmfjhmonkocbjadbfplnigmagldckm";
|
|
258
258
|
|
|
259
259
|
async function ensureExtensionDistInstalled(): Promise<string> {
|
|
260
260
|
const source = existsSync(BUNDLED_EXTENSION_DIST_DIR)
|
|
@@ -279,28 +279,41 @@ async function findInstalledExtension(
|
|
|
279
279
|
const cache = await readChromeProfileCache();
|
|
280
280
|
const profiles = profileDir ? [profileDir] : (cache ? Object.keys(cache) : []);
|
|
281
281
|
// Resolve our known-good install paths up front for path-based fallback matching.
|
|
282
|
+
// LEGACY_EXTENSION_DIST_DIR is intentionally excluded: it points at the pre-V2 multi-tab
|
|
283
|
+
// bridge, which is incompatible with the current cdpRelayV2 relay — matching it would hand
|
|
284
|
+
// setup a stale, broken extension.
|
|
282
285
|
const knownPaths = new Set<string>();
|
|
283
|
-
for (const p of [EXTENSION_DIST_DIR, BUNDLED_EXTENSION_DIST_DIR
|
|
286
|
+
for (const p of [EXTENSION_DIST_DIR, BUNDLED_EXTENSION_DIST_DIR]) {
|
|
284
287
|
try { knownPaths.add(realpathSync(p)); } catch {}
|
|
285
288
|
}
|
|
289
|
+
// Read each profile's settings once so we can prioritize stable-ID matches over path fallbacks.
|
|
290
|
+
const perProfile: Array<{ prof: string; settings: Record<string, any> }> = [];
|
|
286
291
|
for (const prof of profiles) {
|
|
287
292
|
const prefsPath = join(userDataDir, prof, "Secure Preferences");
|
|
288
293
|
const f = file(prefsPath);
|
|
289
294
|
if (!(await f.exists())) continue;
|
|
290
295
|
try {
|
|
291
296
|
const data = JSON.parse(await f.text());
|
|
292
|
-
|
|
293
|
-
for (const [extId, info] of Object.entries(settings as Record<string, any>)) {
|
|
294
|
-
if (!info?.path || info.state === 0) continue; // state 0 = explicitly disabled
|
|
295
|
-
// Primary: stable ID match (works when manifest `key` is set, regardless of path).
|
|
296
|
-
if (extId === EXTENSION_ID) return { id: extId, profile: prof };
|
|
297
|
-
// Fallback: path equality for legacy installs without a stable key.
|
|
298
|
-
let storedPath = info.path as string;
|
|
299
|
-
try { storedPath = realpathSync(storedPath); } catch {}
|
|
300
|
-
if (knownPaths.has(storedPath)) return { id: extId, profile: prof };
|
|
301
|
-
}
|
|
297
|
+
perProfile.push({ prof, settings: (data?.extensions?.settings ?? {}) as Record<string, any> });
|
|
302
298
|
} catch {}
|
|
303
299
|
}
|
|
300
|
+
// Pass 1: stable ID match (manifest `key` set, path-independent). This must win over any path
|
|
301
|
+
// fallback so a stale legacy install sitting on a known path can't shadow the current extension.
|
|
302
|
+
for (const { prof, settings } of perProfile) {
|
|
303
|
+
for (const [extId, info] of Object.entries(settings)) {
|
|
304
|
+
if (!info?.path || info.state === 0) continue; // state 0 = explicitly disabled
|
|
305
|
+
if (extId === EXTENSION_ID) return { id: extId, profile: prof };
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
// Pass 2: path equality fallback for legacy keyless installs without a stable ID.
|
|
309
|
+
for (const { prof, settings } of perProfile) {
|
|
310
|
+
for (const [extId, info] of Object.entries(settings)) {
|
|
311
|
+
if (!info?.path || info.state === 0) continue;
|
|
312
|
+
let storedPath = info.path as string;
|
|
313
|
+
try { storedPath = realpathSync(storedPath); } catch {}
|
|
314
|
+
if (knownPaths.has(storedPath)) return { id: extId, profile: prof };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
304
317
|
return null;
|
|
305
318
|
}
|
|
306
319
|
|