hoversource 1.0.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.
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +75 -0
- package/dist/cert-ca.d.ts +19 -0
- package/dist/cert-ca.js +180 -0
- package/dist/cert.d.ts +15 -0
- package/dist/cert.js +76 -0
- package/dist/cli.d.ts +22 -0
- package/dist/cli.js +770 -0
- package/dist/custom-jsx-dev-runtime-cjs.cjs +21 -0
- package/dist/custom-jsx-dev-runtime.d.ts +3 -0
- package/dist/custom-jsx-dev-runtime.js +21 -0
- package/dist/custom-jsx-runtime-cjs.cjs +76 -0
- package/dist/custom-jsx-runtime.d.ts +3 -0
- package/dist/custom-jsx-runtime.js +72 -0
- package/dist/launcher/ElectronCdpLauncher.d.ts +4 -0
- package/dist/launcher/ElectronCdpLauncher.js +63 -0
- package/dist/launcher/WebProxyLauncher.d.ts +5 -0
- package/dist/launcher/WebProxyLauncher.js +114 -0
- package/dist/launcher/types.d.ts +10 -0
- package/dist/launcher/types.js +1 -0
- package/dist/loader.d.ts +1 -0
- package/dist/loader.js +31 -0
- package/dist/patcher/ReactRuntimePatcher.d.ts +5 -0
- package/dist/patcher/ReactRuntimePatcher.js +263 -0
- package/dist/patcher/types.d.ts +4 -0
- package/dist/patcher/types.js +1 -0
- package/dist/pipeline.d.ts +14 -0
- package/dist/pipeline.js +45 -0
- package/dist/port.d.ts +76 -0
- package/dist/port.js +560 -0
- package/dist/proxy.d.ts +16 -0
- package/dist/proxy.js +220 -0
- package/dist/utils/patchState.d.ts +3 -0
- package/dist/utils/patchState.js +59 -0
- package/package.json +27 -0
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import https from "node:https";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { ProxyResponsePipeline } from "./pipeline.js";
|
|
5
|
+
import { getOrCreateLocalSslCert } from "./cert.js";
|
|
6
|
+
export function rewriteCookieHeaders(headers, useHttps) {
|
|
7
|
+
const setCookie = headers["set-cookie"];
|
|
8
|
+
if (setCookie) {
|
|
9
|
+
headers["set-cookie"] = setCookie.map((cookie) => {
|
|
10
|
+
let rewritten = cookie.replace(/;\s*domain\s*=\s*[^;]+/gi, "");
|
|
11
|
+
if (!useHttps) {
|
|
12
|
+
rewritten = rewritten.replace(/;\s*secure\b/gi, "");
|
|
13
|
+
}
|
|
14
|
+
return rewritten;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Starts a reverse HTTP/HTTPS proxy that forwards requests to `targetUrl` and
|
|
20
|
+
* injects `<script src="${overlayScriptUrl}"></script>` into HTML responses.
|
|
21
|
+
*/
|
|
22
|
+
export function startProxy(options) {
|
|
23
|
+
const { targetUrl, proxyPort, companionPort, overlayScriptUrl, useHttps, sslKeyPath, sslCertPath } = options;
|
|
24
|
+
const target = new URL(targetUrl);
|
|
25
|
+
const pipeline = new ProxyResponsePipeline();
|
|
26
|
+
const requestHandler = (req, res) => {
|
|
27
|
+
// ─── PART A: Proxy requests under /hoversource/ to Companion Server ──────
|
|
28
|
+
if (req.url?.startsWith("/hoversource/")) {
|
|
29
|
+
const rewrittenPath = req.url.replace(/^\/hoversource/, "") || "/";
|
|
30
|
+
const isOverlayScript = rewrittenPath === "/hoversource-overlay.js";
|
|
31
|
+
const companionReqOpts = {
|
|
32
|
+
hostname: "127.0.0.1",
|
|
33
|
+
port: companionPort,
|
|
34
|
+
path: rewrittenPath,
|
|
35
|
+
method: req.method,
|
|
36
|
+
headers: {
|
|
37
|
+
...req.headers,
|
|
38
|
+
host: `127.0.0.1:${companionPort}`,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const companionReq = http.request(companionReqOpts, (companionRes) => {
|
|
42
|
+
const responseHeaders = { ...companionRes.headers };
|
|
43
|
+
// Ensure CSP is stripped from companion server responses just in case
|
|
44
|
+
delete responseHeaders["content-security-policy"];
|
|
45
|
+
delete responseHeaders["content-security-policy-report-only"];
|
|
46
|
+
if (isOverlayScript) {
|
|
47
|
+
const chunks = [];
|
|
48
|
+
companionRes.on("data", (chunk) => chunks.push(chunk));
|
|
49
|
+
companionRes.on("end", () => {
|
|
50
|
+
let body = Buffer.concat(chunks).toString("utf-8");
|
|
51
|
+
body = `globalThis.__HOVERSOURCE_PROXY__ = true;\n` + body;
|
|
52
|
+
const bodyBuf = Buffer.from(body, "utf-8");
|
|
53
|
+
delete responseHeaders["content-length"];
|
|
54
|
+
res.writeHead(companionRes.statusCode || 200, responseHeaders);
|
|
55
|
+
res.end(bodyBuf);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
res.writeHead(companionRes.statusCode || 200, responseHeaders);
|
|
60
|
+
companionRes.pipe(res, { end: true });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
companionReq.on("error", (err) => {
|
|
64
|
+
console.error(`[HoverSource Proxy] Failed to proxy request to companion server:`, err.message);
|
|
65
|
+
if (!res.headersSent) {
|
|
66
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
67
|
+
res.end(`HoverSource proxy: companion server upstream error.`);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
req.pipe(companionReq, { end: true });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
// ─── PART B: Proxy target application requests ─────────────────────────
|
|
74
|
+
const headers = { ...req.headers };
|
|
75
|
+
// Filter accept-encoding to only allow compression formats that our pipeline/zlib can handle
|
|
76
|
+
if (headers["accept-encoding"]) {
|
|
77
|
+
const accepted = String(headers["accept-encoding"]).toLowerCase();
|
|
78
|
+
const supported = [];
|
|
79
|
+
if (accepted.includes("gzip"))
|
|
80
|
+
supported.push("gzip");
|
|
81
|
+
if (accepted.includes("deflate"))
|
|
82
|
+
supported.push("deflate");
|
|
83
|
+
if (accepted.includes("br"))
|
|
84
|
+
supported.push("br");
|
|
85
|
+
if (supported.length > 0) {
|
|
86
|
+
headers["accept-encoding"] = supported.join(", ");
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
delete headers["accept-encoding"];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Map upstream agent request based on target protocol
|
|
93
|
+
const isTargetHttps = target.protocol === "https:";
|
|
94
|
+
const agent = isTargetHttps ? https : http;
|
|
95
|
+
const requestOptions = {
|
|
96
|
+
hostname: target.hostname,
|
|
97
|
+
port: target.port || (isTargetHttps ? 443 : 80),
|
|
98
|
+
path: req.url,
|
|
99
|
+
method: req.method,
|
|
100
|
+
headers: {
|
|
101
|
+
...headers,
|
|
102
|
+
host: target.host,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
const proxyReq = agent.request(requestOptions, (proxyRes) => {
|
|
106
|
+
const contentType = proxyRes.headers["content-type"] || "";
|
|
107
|
+
// Performance Optimization: Stream non-HTML responses directly
|
|
108
|
+
if (!pipeline.shouldTransform(contentType)) {
|
|
109
|
+
const responseHeaders = { ...proxyRes.headers };
|
|
110
|
+
// Strip CSP headers to avoid iframe and asset restrictions
|
|
111
|
+
delete responseHeaders["content-security-policy"];
|
|
112
|
+
delete responseHeaders["content-security-policy-report-only"];
|
|
113
|
+
// Rewrite cookie domains and secure flag
|
|
114
|
+
rewriteCookieHeaders(responseHeaders, !!useHttps);
|
|
115
|
+
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
116
|
+
proxyRes.pipe(res, { end: true });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// HTML Content: Buffer and transform
|
|
120
|
+
const chunks = [];
|
|
121
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
122
|
+
proxyRes.on("end", () => {
|
|
123
|
+
const rawBody = Buffer.concat(chunks);
|
|
124
|
+
const contentEncoding = (proxyRes.headers["content-encoding"] || "").toString();
|
|
125
|
+
const transformedBody = pipeline.transform(rawBody, contentEncoding, {
|
|
126
|
+
overlayScriptUrl,
|
|
127
|
+
});
|
|
128
|
+
const responseHeaders = { ...proxyRes.headers };
|
|
129
|
+
// Recalculate content headers after modification
|
|
130
|
+
delete responseHeaders["content-length"];
|
|
131
|
+
delete responseHeaders["content-encoding"];
|
|
132
|
+
// Strip CSP headers to ensure the browser loads the overlay script
|
|
133
|
+
delete responseHeaders["content-security-policy"];
|
|
134
|
+
delete responseHeaders["content-security-policy-report-only"];
|
|
135
|
+
responseHeaders["content-type"] = "text/html; charset=utf-8";
|
|
136
|
+
// Rewrite cookie domains and secure flag
|
|
137
|
+
rewriteCookieHeaders(responseHeaders, !!useHttps);
|
|
138
|
+
res.writeHead(proxyRes.statusCode || 200, responseHeaders);
|
|
139
|
+
res.end(transformedBody);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
proxyReq.on("error", (err) => {
|
|
143
|
+
console.error(`[HoverSource Proxy] Error forwarding request to ${targetUrl}:`, err.message);
|
|
144
|
+
if (!res.headersSent) {
|
|
145
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
146
|
+
res.end(`HoverSource proxy: upstream error -- is your dev server running at ${targetUrl}?`);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
req.pipe(proxyReq, { end: true });
|
|
150
|
+
};
|
|
151
|
+
let server;
|
|
152
|
+
if (useHttps) {
|
|
153
|
+
const credentials = getOrCreateLocalSslCert({
|
|
154
|
+
keyPath: sslKeyPath,
|
|
155
|
+
certPath: sslCertPath,
|
|
156
|
+
targetHost: target.hostname,
|
|
157
|
+
});
|
|
158
|
+
server = https.createServer(credentials, requestHandler);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
server = http.createServer(requestHandler);
|
|
162
|
+
}
|
|
163
|
+
const upgradeHandler = (req, socket, head) => {
|
|
164
|
+
const isTargetHttps = target.protocol === "https:";
|
|
165
|
+
const agent = isTargetHttps ? https : http;
|
|
166
|
+
const requestOptions = {
|
|
167
|
+
hostname: target.hostname,
|
|
168
|
+
port: target.port || (isTargetHttps ? 443 : 80),
|
|
169
|
+
path: req.url,
|
|
170
|
+
method: req.method,
|
|
171
|
+
headers: {
|
|
172
|
+
...req.headers,
|
|
173
|
+
host: target.host,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
const proxyReq = agent.request(requestOptions);
|
|
177
|
+
proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
|
|
178
|
+
socket.on("error", (err) => {
|
|
179
|
+
if (process.env.DEBUG) {
|
|
180
|
+
console.debug("[HoverSource Proxy] Client socket error:", err.message);
|
|
181
|
+
}
|
|
182
|
+
proxySocket.destroy();
|
|
183
|
+
});
|
|
184
|
+
proxySocket.on("error", (err) => {
|
|
185
|
+
if (process.env.DEBUG) {
|
|
186
|
+
console.debug("[HoverSource Proxy] Target socket error:", err.message);
|
|
187
|
+
}
|
|
188
|
+
socket.destroy();
|
|
189
|
+
});
|
|
190
|
+
let responseHeader = `HTTP/${req.httpVersion} 101 Switching Protocols\r\n`;
|
|
191
|
+
for (const [key, value] of Object.entries(proxyRes.headers)) {
|
|
192
|
+
if (Array.isArray(value)) {
|
|
193
|
+
for (const val of value) {
|
|
194
|
+
responseHeader += `${key}: ${val}\r\n`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (value !== undefined) {
|
|
198
|
+
responseHeader += `${key}: ${value}\r\n`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
responseHeader += "\r\n";
|
|
202
|
+
socket.write(responseHeader);
|
|
203
|
+
proxySocket.write(proxyHead);
|
|
204
|
+
socket.pipe(proxySocket);
|
|
205
|
+
proxySocket.pipe(socket);
|
|
206
|
+
});
|
|
207
|
+
proxyReq.on("error", (err) => {
|
|
208
|
+
console.error(`[HoverSource Proxy] WebSocket proxy error forwarding upgrade to ${targetUrl}:`, err.message);
|
|
209
|
+
socket.end();
|
|
210
|
+
});
|
|
211
|
+
proxyReq.end();
|
|
212
|
+
};
|
|
213
|
+
server.on("upgrade", upgradeHandler);
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
server.listen(proxyPort, () => {
|
|
216
|
+
resolve();
|
|
217
|
+
});
|
|
218
|
+
server.on("error", reject);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
const PATCH_STATE_FILE = path.join(os.tmpdir(), "hoversource_patches.json");
|
|
5
|
+
export function recordPatchState(filePath, originalContent) {
|
|
6
|
+
let state = {};
|
|
7
|
+
if (fs.existsSync(PATCH_STATE_FILE)) {
|
|
8
|
+
try {
|
|
9
|
+
state = JSON.parse(fs.readFileSync(PATCH_STATE_FILE, "utf-8"));
|
|
10
|
+
}
|
|
11
|
+
catch (err) {
|
|
12
|
+
console.debug("[HoverSource] Failed to parse patch state:", err);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
state[filePath] = originalContent;
|
|
16
|
+
try {
|
|
17
|
+
fs.writeFileSync(PATCH_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
console.debug("[HoverSource] Failed to write patch state:", err);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function removePatchState(filePath) {
|
|
24
|
+
if (!fs.existsSync(PATCH_STATE_FILE))
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
const state = JSON.parse(fs.readFileSync(PATCH_STATE_FILE, "utf-8"));
|
|
28
|
+
delete state[filePath];
|
|
29
|
+
if (Object.keys(state).length === 0) {
|
|
30
|
+
fs.unlinkSync(PATCH_STATE_FILE);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
fs.writeFileSync(PATCH_STATE_FILE, JSON.stringify(state, null, 2), "utf-8");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.debug("[HoverSource] Failed to remove patch state:", err);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function restoreLeftoverPatches() {
|
|
41
|
+
if (!fs.existsSync(PATCH_STATE_FILE))
|
|
42
|
+
return;
|
|
43
|
+
try {
|
|
44
|
+
const state = JSON.parse(fs.readFileSync(PATCH_STATE_FILE, "utf-8"));
|
|
45
|
+
for (const [filePath, originalContent] of Object.entries(state)) {
|
|
46
|
+
try {
|
|
47
|
+
fs.writeFileSync(filePath, originalContent, "utf-8");
|
|
48
|
+
console.log(`[HoverSource] [Self-Healing] Restored leftover patch in ${filePath}`);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
console.warn(`[HoverSource] [Self-Healing] Failed to restore leftover patch in ${filePath}:`, err.message);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
fs.unlinkSync(PATCH_STATE_FILE);
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
console.debug("[HoverSource] Leftover patch restore failed:", err);
|
|
58
|
+
}
|
|
59
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hoversource",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"hs": "./dist/cli.js",
|
|
8
|
+
"hoversource": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"!dist/**/__tests__",
|
|
13
|
+
"!dist/**/*.test.*"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -b",
|
|
17
|
+
"clean": "tsc -b --clean",
|
|
18
|
+
"test": "vitest run src"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@hoversource/companion-server": "*",
|
|
22
|
+
"@hoversource/client-injector": "*"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"vitest": "^4.1.8"
|
|
26
|
+
}
|
|
27
|
+
}
|