locadot 1.6.0-beta.0 → 1.6.0-beta.2
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/CHANGELOG.md +4 -1
- package/README.md +1 -1
- package/dist/lib/commands.js +1 -0
- package/dist/lib/system.js +1 -1
- package/dist/server.js +14 -0
- package/dist/utils/startup.js +37 -15
- package/dist/utils/trust.js +5 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
-
The version in `package.json` is
|
|
5
|
+
The version in `package.json` is 1.6.0-beta.2 (published under the `beta` tag). The owner decides the bump. **2.0.0** is suggested because of the
|
|
6
6
|
breaking items below.
|
|
7
7
|
|
|
8
8
|
### Breaking
|
|
@@ -28,6 +28,9 @@ breaking items below.
|
|
|
28
28
|
- Test suite (`pnpm test`) and a `prepublishOnly` build.
|
|
29
29
|
|
|
30
30
|
### Fixed
|
|
31
|
+
- Windows: `startup:enable` no longer needs admin rights. It used to fail with "Access is denied". Start at logon now uses the user's Startup folder instead of a scheduled task. (BUG-14)
|
|
32
|
+
- Windows: console windows no longer flash up from the background proxy's `certutil`/`schtasks` checks. Start at logon now runs hidden and writes to the log. (BUG-13)
|
|
33
|
+
- Upstream `Connection`/`Upgrade` headers are no longer forwarded. Apache sites proxied through locadot kept dropping the connection and lost assets. (BUG-12)
|
|
31
34
|
- `stop`, `restart` and `kill` now actually stop the proxy. (BUG-01)
|
|
32
35
|
- A failed start is reported with the log tail and exits 1. (BUG-07)
|
|
33
36
|
- `update` accepts http targets and `add` accepts https ones. (BUG-03)
|
package/README.md
CHANGED
|
@@ -82,7 +82,7 @@ Options for `add` / `update`:
|
|
|
82
82
|
| `locadot clear:logs` | Clear the log file. |
|
|
83
83
|
| `locadot path` | Show every file locadot uses. `path:logs` and `path:hosts` print a single path. |
|
|
84
84
|
| `locadot token` | Print the dashboard API token, for scripts and AI agents. |
|
|
85
|
-
| `locadot startup:enable` / `startup:disable` / `startup:status` | Start the proxy at boot (Linux cron, macOS LaunchAgent) or logon (Windows
|
|
85
|
+
| `locadot startup:enable` / `startup:disable` / `startup:status` | Start the proxy at boot (Linux cron, macOS LaunchAgent) or logon (a hidden script in the Windows Startup folder, no admin needed). |
|
|
86
86
|
|
|
87
87
|
---
|
|
88
88
|
|
package/dist/lib/commands.js
CHANGED
|
@@ -280,6 +280,7 @@ class Commands {
|
|
|
280
280
|
? ["open", [url]]
|
|
281
281
|
: ["xdg-open", [url]];
|
|
282
282
|
try {
|
|
283
|
+
// windows-hide-exempt: run from the user's terminal (no popup), and hiding `cmd /c start` can hide the browser too.
|
|
283
284
|
(0, child_process_1.spawn)(command, args, { detached: true, stdio: "ignore" }).on("error", () => { }).unref();
|
|
284
285
|
}
|
|
285
286
|
catch { }
|
package/dist/lib/system.js
CHANGED
|
@@ -46,7 +46,7 @@ const canBindPrivileged = () => {
|
|
|
46
46
|
return start !== null && start <= Math.min(constants_1.default.server.httpPort, constants_1.default.server.httpsPort);
|
|
47
47
|
};
|
|
48
48
|
exports.canBindPrivileged = canBindPrivileged;
|
|
49
|
-
// Startup.info
|
|
49
|
+
// Startup.info may shell out (crontab); don't do that on every 5 s dashboard poll.
|
|
50
50
|
const STARTUP_TTL_MS = 30000;
|
|
51
51
|
let startupCache;
|
|
52
52
|
const invalidateSystemStatus = () => {
|
package/dist/server.js
CHANGED
|
@@ -65,6 +65,20 @@ async function startCentralProxy() {
|
|
|
65
65
|
const stats = new Map();
|
|
66
66
|
const proxy = http_proxy_1.default.createProxyServer({});
|
|
67
67
|
proxy.on("error", (err) => logger_1.default.warn(`proxy error: ${err.message}`));
|
|
68
|
+
// Hop-by-hop headers describe the upstream connection, not ours. Apache sends
|
|
69
|
+
// `Connection: Upgrade, close` + `Upgrade: h2`, which made us close the browser's
|
|
70
|
+
// socket after every response (ERR_TOO_MANY_RETRIES on asset-heavy pages).
|
|
71
|
+
proxy.on("proxyRes", (proxyRes) => {
|
|
72
|
+
if (proxyRes.statusCode === 101)
|
|
73
|
+
return;
|
|
74
|
+
const listed = String(proxyRes.headers.connection || "")
|
|
75
|
+
.split(",")
|
|
76
|
+
.map((name) => name.trim().toLowerCase())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
for (const name of [...listed, "connection", "keep-alive", "upgrade", "proxy-connection"]) {
|
|
79
|
+
delete proxyRes.headers[name];
|
|
80
|
+
}
|
|
81
|
+
});
|
|
68
82
|
const info = {
|
|
69
83
|
pid: process.pid,
|
|
70
84
|
version: package_json_1.version,
|
package/dist/utils/startup.js
CHANGED
|
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.windowsLauncherScript = void 0;
|
|
6
7
|
const path_1 = __importDefault(require("path"));
|
|
7
8
|
const os_1 = __importDefault(require("os"));
|
|
8
9
|
const fs_1 = __importDefault(require("fs"));
|
|
@@ -15,6 +16,10 @@ const SERVICE_NAME = "locadot-proxy";
|
|
|
15
16
|
const MAC_LABEL = "com.locadot.proxy";
|
|
16
17
|
const CRON_MARK = "# locadot-proxy";
|
|
17
18
|
const MARKER = path_1.default.join(constants_1.default.paths.HOME, "startup.json");
|
|
19
|
+
// Per-user Startup folder: unlike an ONLOGON scheduled task it needs no admin rights.
|
|
20
|
+
const windowsLauncher = () => path_1.default.join(process.env.APPDATA || path_1.default.join(os_1.default.homedir(), "AppData", "Roaming"), "Microsoft", "Windows", "Start Menu", "Programs", "Startup", `${SERVICE_NAME}.vbs`);
|
|
21
|
+
// Earlier versions registered an ONLOGON scheduled task and kept the script in the state dir.
|
|
22
|
+
const LEGACY_LAUNCHER = path_1.default.join(constants_1.default.paths.HOME, "startup-hidden.vbs");
|
|
18
23
|
const plistPath = () => path_1.default.join(os_1.default.homedir(), "Library", "LaunchAgents", `${MAC_LABEL}.plist`);
|
|
19
24
|
const quote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
|
|
20
25
|
// Resolved on use, not import: the entry path depends on how locadot was launched.
|
|
@@ -22,6 +27,25 @@ const launchArgs = () => {
|
|
|
22
27
|
const { command, path: args } = locadot_file_1.default.getStartProxyFile();
|
|
23
28
|
return { command, args: [...args, "--home", constants_1.default.paths.HOME] };
|
|
24
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Launching node.exe directly at logon opens a console window, and closing it kills the
|
|
32
|
+
* proxy. Explorer runs the .vbs with wscript, which has no console, and Run(…, 0) keeps cmd
|
|
33
|
+
* and node hidden too. cmd also appends the output to the log, like the cron line does.
|
|
34
|
+
*/
|
|
35
|
+
const windowsLauncherScript = (command, args) => {
|
|
36
|
+
const env = `set LOCADOT_HTTP_PORT=${constants_1.default.server.httpPort}&& set LOCADOT_HTTPS_PORT=${constants_1.default.server.httpsPort}&&`;
|
|
37
|
+
const line = `cmd /d /c ${env} ${[command, ...args].map((a) => `"${a}"`).join(" ")} >> "${constants_1.default.paths.LOGS}" 2>&1`;
|
|
38
|
+
return `CreateObject("WScript.Shell").Run "${line.replace(/"/g, '""')}", 0, False\r\n`;
|
|
39
|
+
};
|
|
40
|
+
exports.windowsLauncherScript = windowsLauncherScript;
|
|
41
|
+
/** Best effort: deleting a task the user created without elevation needs none. */
|
|
42
|
+
const removeLegacyTask = () => {
|
|
43
|
+
try {
|
|
44
|
+
(0, child_process_1.execFileSync)("schtasks", ["/Delete", "/TN", SERVICE_NAME, "/F"], { stdio: "ignore", windowsHide: true });
|
|
45
|
+
}
|
|
46
|
+
catch { }
|
|
47
|
+
fs_1.default.rmSync(LEGACY_LAUNCHER, { force: true });
|
|
48
|
+
};
|
|
25
49
|
function execSudo(cmd) {
|
|
26
50
|
return new Promise((resolve, reject) => {
|
|
27
51
|
sudo_prompt_1.default.exec(cmd, { name: "Locadot" }, (error, stdout, stderr) => {
|
|
@@ -56,10 +80,11 @@ class Startup {
|
|
|
56
80
|
let method = "";
|
|
57
81
|
switch (platform) {
|
|
58
82
|
case "windows": {
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
(
|
|
62
|
-
|
|
83
|
+
// Runs at the user's logon; ports 80/443 need no elevation on Windows.
|
|
84
|
+
removeLegacyTask();
|
|
85
|
+
fs_1.default.mkdirSync(path_1.default.dirname(windowsLauncher()), { recursive: true });
|
|
86
|
+
fs_1.default.writeFileSync(windowsLauncher(), (0, exports.windowsLauncherScript)(command, args));
|
|
87
|
+
method = "startup-folder";
|
|
63
88
|
break;
|
|
64
89
|
}
|
|
65
90
|
case "linux": {
|
|
@@ -71,7 +96,7 @@ class Startup {
|
|
|
71
96
|
method = "root-crontab";
|
|
72
97
|
}
|
|
73
98
|
else {
|
|
74
|
-
(0, child_process_1.execFileSync)("sh", ["-c", edit], { stdio: "inherit" });
|
|
99
|
+
(0, child_process_1.execFileSync)("sh", ["-c", edit], { stdio: "inherit", windowsHide: true });
|
|
75
100
|
method = "user-crontab";
|
|
76
101
|
}
|
|
77
102
|
break;
|
|
@@ -98,7 +123,7 @@ class Startup {
|
|
|
98
123
|
</plist>`;
|
|
99
124
|
fs_1.default.mkdirSync(path_1.default.dirname(plistPath()), { recursive: true });
|
|
100
125
|
fs_1.default.writeFileSync(plistPath(), plist);
|
|
101
|
-
(0, child_process_1.execFileSync)("launchctl", ["load", "-w", plistPath()], { stdio: "inherit" });
|
|
126
|
+
(0, child_process_1.execFileSync)("launchctl", ["load", "-w", plistPath()], { stdio: "inherit", windowsHide: true });
|
|
102
127
|
method = "launch-agent";
|
|
103
128
|
break;
|
|
104
129
|
}
|
|
@@ -114,21 +139,19 @@ class Startup {
|
|
|
114
139
|
const method = Startup.marker()?.method;
|
|
115
140
|
switch (platform) {
|
|
116
141
|
case "windows":
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
catch { }
|
|
142
|
+
removeLegacyTask();
|
|
143
|
+
fs_1.default.rmSync(windowsLauncher(), { force: true });
|
|
121
144
|
break;
|
|
122
145
|
case "linux":
|
|
123
146
|
if (method === "root-crontab")
|
|
124
147
|
await execSudo(`sh -c ${quote(cronEdit())}`);
|
|
125
148
|
else
|
|
126
|
-
(0, child_process_1.execFileSync)("sh", ["-c", cronEdit()], { stdio: "inherit" });
|
|
149
|
+
(0, child_process_1.execFileSync)("sh", ["-c", cronEdit()], { stdio: "inherit", windowsHide: true });
|
|
127
150
|
break;
|
|
128
151
|
case "mac":
|
|
129
152
|
if (fs_1.default.existsSync(plistPath())) {
|
|
130
153
|
try {
|
|
131
|
-
(0, child_process_1.execFileSync)("launchctl", ["unload", "-w", plistPath()], { stdio: "ignore" });
|
|
154
|
+
(0, child_process_1.execFileSync)("launchctl", ["unload", "-w", plistPath()], { stdio: "ignore", windowsHide: true });
|
|
132
155
|
}
|
|
133
156
|
catch { }
|
|
134
157
|
fs_1.default.rmSync(plistPath(), { force: true });
|
|
@@ -163,12 +186,11 @@ class Startup {
|
|
|
163
186
|
case "linux":
|
|
164
187
|
if (marker?.method === "root-crontab")
|
|
165
188
|
return true;
|
|
166
|
-
return (0, child_process_1.execFileSync)("sh", ["-c", "crontab -l 2>/dev/null || true"], { encoding: "utf8" }).includes(CRON_MARK);
|
|
189
|
+
return (0, child_process_1.execFileSync)("sh", ["-c", "crontab -l 2>/dev/null || true"], { encoding: "utf8", windowsHide: true }).includes(CRON_MARK);
|
|
167
190
|
case "mac":
|
|
168
191
|
return fs_1.default.existsSync(plistPath());
|
|
169
192
|
case "windows":
|
|
170
|
-
|
|
171
|
-
return true;
|
|
193
|
+
return fs_1.default.existsSync(windowsLauncher());
|
|
172
194
|
default:
|
|
173
195
|
return false;
|
|
174
196
|
}
|
package/dist/utils/trust.js
CHANGED
|
@@ -32,7 +32,7 @@ function execSudo(cmd) {
|
|
|
32
32
|
}
|
|
33
33
|
function hasBinary(bin) {
|
|
34
34
|
try {
|
|
35
|
-
(0, child_process_1.execSync)(`command -v ${bin}`, { stdio: "ignore" });
|
|
35
|
+
(0, child_process_1.execSync)(`command -v ${bin}`, { stdio: "ignore", windowsHide: true });
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
38
|
catch {
|
|
@@ -75,6 +75,7 @@ function trustNssDbs(certPath) {
|
|
|
75
75
|
try {
|
|
76
76
|
(0, child_process_1.execSync)(`certutil -d "${db}" -A -t "C,," -n "${CA_NAME}" -i "${certPath}"`, {
|
|
77
77
|
stdio: "ignore",
|
|
78
|
+
windowsHide: true,
|
|
78
79
|
});
|
|
79
80
|
}
|
|
80
81
|
catch {
|
|
@@ -87,7 +88,7 @@ function untrustNssDbs() {
|
|
|
87
88
|
return;
|
|
88
89
|
for (const db of nssProfileDirs()) {
|
|
89
90
|
try {
|
|
90
|
-
(0, child_process_1.execSync)(`certutil -d "${db}" -D -n "${CA_NAME}"`, { stdio: "ignore" });
|
|
91
|
+
(0, child_process_1.execSync)(`certutil -d "${db}" -D -n "${CA_NAME}"`, { stdio: "ignore", windowsHide: true });
|
|
91
92
|
}
|
|
92
93
|
catch {
|
|
93
94
|
// Not present in this profile — nothing to remove.
|
|
@@ -133,7 +134,7 @@ async function untrustMac() {
|
|
|
133
134
|
}
|
|
134
135
|
function isTrustedMac(certPath) {
|
|
135
136
|
try {
|
|
136
|
-
(0, child_process_1.execSync)(`security verify-cert -c "${certPath}"`, { stdio: "ignore" });
|
|
137
|
+
(0, child_process_1.execSync)(`security verify-cert -c "${certPath}"`, { stdio: "ignore", windowsHide: true });
|
|
137
138
|
return true;
|
|
138
139
|
}
|
|
139
140
|
catch {
|
|
@@ -148,7 +149,7 @@ async function untrustWindows() {
|
|
|
148
149
|
}
|
|
149
150
|
function isTrustedWindows() {
|
|
150
151
|
try {
|
|
151
|
-
(0, child_process_1.execSync)(`certutil -store Root "${CA_NAME}"`, { stdio: "ignore" });
|
|
152
|
+
(0, child_process_1.execSync)(`certutil -store Root "${CA_NAME}"`, { stdio: "ignore", windowsHide: true });
|
|
152
153
|
return true;
|
|
153
154
|
}
|
|
154
155
|
catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "locadot",
|
|
3
|
-
"version": "1.6.0-beta.
|
|
3
|
+
"version": "1.6.0-beta.2",
|
|
4
4
|
"description": "Secure your local development environment with HTTPS and custom domains like dev.localhost.",
|
|
5
5
|
"homepage": "https://www.npmjs.com/package/locadot",
|
|
6
6
|
"main": "dist/index.js",
|