@haowjy/remote-workspace 0.1.2 → 0.1.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/README.md +4 -2
- package/dist/launcher.js +50 -12
- package/dist/server.js +24 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Mobile-friendly read/upload web workspace for this repository.
|
|
|
9
9
|
- Delete images from `.clipboard/` and `.playwright-mcp/` from the UI
|
|
10
10
|
- Preview text files and images
|
|
11
11
|
- Render Markdown with Mermaid diagrams
|
|
12
|
-
- Hide and block access to
|
|
12
|
+
- Hide and block access to configured path segments (always hides `.git`)
|
|
13
13
|
- Dedicated collapsible `.clipboard` panel for upload + quick image viewing
|
|
14
14
|
|
|
15
15
|
This app is intentionally **no text editing** to keep remote access simple and lower risk.
|
|
@@ -62,6 +62,7 @@ cd /path/to/your/repo && pnpm dev
|
|
|
62
62
|
```bash
|
|
63
63
|
pnpm dev -- config /path/to/config
|
|
64
64
|
pnpm dev -- port 18111
|
|
65
|
+
pnpm dev -- always-hidden .git,.env,.secrets
|
|
65
66
|
pnpm dev -- install
|
|
66
67
|
pnpm dev -- no-serve
|
|
67
68
|
pnpm dev -- password your-password
|
|
@@ -76,6 +77,7 @@ pnpm dev -- password your-password --funnel
|
|
|
76
77
|
- `REMOTE_WS_MAX_UPLOAD_BYTES` (default `26214400`)
|
|
77
78
|
- `REMOTE_WS_PASSWORD` (optional, enables HTTP Basic Auth when set)
|
|
78
79
|
- `REMOTE_WS_CONFIG_FILE` (optional config file path override)
|
|
80
|
+
- `REMOTE_WS_ALWAYS_HIDDEN` (optional comma-separated extra hidden path segments)
|
|
79
81
|
- `REPO_ROOT` (injected by launcher script)
|
|
80
82
|
|
|
81
83
|
Password config file format (default: repo root `.remote-workspace.conf`):
|
|
@@ -102,7 +104,7 @@ Config file selection precedence:
|
|
|
102
104
|
- `POST /api/clipboard/upload` always writes to `REPO_ROOT/.clipboard`
|
|
103
105
|
- `DELETE /api/clipboard/file?name=<filename>` deletes one image in `REPO_ROOT/.clipboard`
|
|
104
106
|
- `.clipboard` panel uses dedicated clipboard endpoints (`/api/clipboard/upload`, `/api/clipboard/list`, `/api/clipboard/file`)
|
|
105
|
-
- Main repository browser
|
|
107
|
+
- Main repository browser blocks always-hidden path segments (`.git` + optional configured segments)
|
|
106
108
|
- Gitignored paths are hidden/blocked (for example `node_modules/`, build artifacts, local secrets)
|
|
107
109
|
- Accepted upload types are images only (`png`, `jpg`, `jpeg`, `gif`, `webp`, `svg`, `bmp`, `heic`, `heif`, `avif`)
|
|
108
110
|
- Clipboard panel supports both file picker and `Paste From Clipboard` button (when browser clipboard image API is available)
|
package/dist/launcher.js
CHANGED
|
@@ -14,6 +14,7 @@ Options:
|
|
|
14
14
|
--repo-root <path> Repository root to expose (default: REPO_ROOT or cwd)
|
|
15
15
|
--config <path> Config file path (default uses precedence search)
|
|
16
16
|
--port <port> Listen port (default: REMOTE_WS_PORT or 18080)
|
|
17
|
+
--always-hidden <csv> Extra always-hidden path segments (comma-separated)
|
|
17
18
|
--install Force dependency install before start
|
|
18
19
|
--skip-install Skip install check even if node_modules is missing
|
|
19
20
|
--no-serve Skip tailscale serve setup
|
|
@@ -27,6 +28,7 @@ Examples:
|
|
|
27
28
|
pnpm dev -- --repo-root /path/to/repo
|
|
28
29
|
pnpm dev -- --config ~/.config/remote-workspace/config
|
|
29
30
|
pnpm dev -- --port 18111
|
|
31
|
+
pnpm dev -- --always-hidden .git,.env,.secrets
|
|
30
32
|
pnpm dev -- --password
|
|
31
33
|
pnpm dev -- --password mypass --serve
|
|
32
34
|
pnpm dev -- --password mypass --funnel
|
|
@@ -47,6 +49,23 @@ function parsePort(rawPort) {
|
|
|
47
49
|
}
|
|
48
50
|
return value;
|
|
49
51
|
}
|
|
52
|
+
function parseAlwaysHiddenCsv(rawValue, sourceLabel) {
|
|
53
|
+
const segments = rawValue
|
|
54
|
+
.split(",")
|
|
55
|
+
.map((value) => value.trim())
|
|
56
|
+
.filter(Boolean);
|
|
57
|
+
if (segments.length === 0) {
|
|
58
|
+
fail(`Invalid ${sourceLabel} value: provide at least one segment.`);
|
|
59
|
+
}
|
|
60
|
+
const normalizedSegments = new Set();
|
|
61
|
+
for (const segment of segments) {
|
|
62
|
+
if (segment.includes("/") || segment.includes("\\") || segment.includes("\u0000")) {
|
|
63
|
+
fail(`Invalid ${sourceLabel} segment "${segment}": segments must not contain path separators or null bytes.`);
|
|
64
|
+
}
|
|
65
|
+
normalizedSegments.add(segment);
|
|
66
|
+
}
|
|
67
|
+
return Array.from(normalizedSegments).join(",");
|
|
68
|
+
}
|
|
50
69
|
function parseConfigPassword(configFilePath) {
|
|
51
70
|
if (!existsSync(configFilePath)) {
|
|
52
71
|
return "";
|
|
@@ -100,6 +119,7 @@ function parseArgs(argv) {
|
|
|
100
119
|
let workspacePassword = "";
|
|
101
120
|
let configFileFromArg = null;
|
|
102
121
|
let configFile = "";
|
|
122
|
+
let alwaysHidden = null;
|
|
103
123
|
for (let i = 0; i < argv.length; i += 1) {
|
|
104
124
|
const arg = argv[i];
|
|
105
125
|
switch (arg) {
|
|
@@ -130,6 +150,15 @@ function parseArgs(argv) {
|
|
|
130
150
|
i += 1;
|
|
131
151
|
break;
|
|
132
152
|
}
|
|
153
|
+
case "--always-hidden": {
|
|
154
|
+
const next = argv[i + 1];
|
|
155
|
+
if (!next || next.startsWith("--")) {
|
|
156
|
+
fail("Missing value for --always-hidden.");
|
|
157
|
+
}
|
|
158
|
+
alwaysHidden = parseAlwaysHiddenCsv(next, "--always-hidden");
|
|
159
|
+
i += 1;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
133
162
|
case "--install":
|
|
134
163
|
forceInstall = true;
|
|
135
164
|
break;
|
|
@@ -229,6 +258,7 @@ function parseArgs(argv) {
|
|
|
229
258
|
passwordEnabled,
|
|
230
259
|
workspacePassword,
|
|
231
260
|
configFile,
|
|
261
|
+
alwaysHidden,
|
|
232
262
|
};
|
|
233
263
|
}
|
|
234
264
|
function hasCommand(commandName) {
|
|
@@ -291,25 +321,33 @@ function main() {
|
|
|
291
321
|
if (args.passwordEnabled) {
|
|
292
322
|
console.log("Auth: Basic Auth enabled");
|
|
293
323
|
}
|
|
324
|
+
if (args.alwaysHidden !== null) {
|
|
325
|
+
console.log(`Always hidden segments: .git + ${args.alwaysHidden}`);
|
|
326
|
+
}
|
|
327
|
+
else if (process.env.REMOTE_WS_ALWAYS_HIDDEN) {
|
|
328
|
+
console.log(`Always hidden segments: .git + ${process.env.REMOTE_WS_ALWAYS_HIDDEN}`);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
console.log("Always hidden segments: .git");
|
|
332
|
+
}
|
|
294
333
|
console.log("");
|
|
334
|
+
const childEnv = {
|
|
335
|
+
...process.env,
|
|
336
|
+
REPO_ROOT: args.repoRoot,
|
|
337
|
+
REMOTE_WS_PORT: String(args.port),
|
|
338
|
+
REMOTE_WS_PASSWORD: args.workspacePassword,
|
|
339
|
+
};
|
|
340
|
+
if (args.alwaysHidden !== null) {
|
|
341
|
+
childEnv.REMOTE_WS_ALWAYS_HIDDEN = args.alwaysHidden;
|
|
342
|
+
}
|
|
295
343
|
const child = runBuiltServer
|
|
296
344
|
? spawn("node", [builtServerPath], {
|
|
297
345
|
stdio: "inherit",
|
|
298
|
-
env:
|
|
299
|
-
...process.env,
|
|
300
|
-
REPO_ROOT: args.repoRoot,
|
|
301
|
-
REMOTE_WS_PORT: String(args.port),
|
|
302
|
-
REMOTE_WS_PASSWORD: args.workspacePassword,
|
|
303
|
-
},
|
|
346
|
+
env: childEnv,
|
|
304
347
|
})
|
|
305
348
|
: spawn("pnpm", ["--dir", appDir, "dev:server"], {
|
|
306
349
|
stdio: "inherit",
|
|
307
|
-
env:
|
|
308
|
-
...process.env,
|
|
309
|
-
REPO_ROOT: args.repoRoot,
|
|
310
|
-
REMOTE_WS_PORT: String(args.port),
|
|
311
|
-
REMOTE_WS_PASSWORD: args.workspacePassword,
|
|
312
|
-
},
|
|
350
|
+
env: childEnv,
|
|
313
351
|
});
|
|
314
352
|
child.on("exit", (code, signal) => {
|
|
315
353
|
if (signal) {
|
package/dist/server.js
CHANGED
|
@@ -23,6 +23,26 @@ function parseIntegerFromEnv(envName, fallbackValue, options) {
|
|
|
23
23
|
}
|
|
24
24
|
return parsedValue;
|
|
25
25
|
}
|
|
26
|
+
function assertValidHiddenSegment(segment, sourceName) {
|
|
27
|
+
if (segment.includes("/") || segment.includes("\\") || segment.includes("\u0000")) {
|
|
28
|
+
throw new Error(`Invalid ${sourceName} segment "${segment}": segments must not contain path separators or null bytes`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function parseAlwaysHiddenSegments(rawValue) {
|
|
32
|
+
const segments = new Set([".git"]);
|
|
33
|
+
if (rawValue === undefined) {
|
|
34
|
+
return segments;
|
|
35
|
+
}
|
|
36
|
+
for (const rawSegment of rawValue.split(",")) {
|
|
37
|
+
const segment = rawSegment.trim();
|
|
38
|
+
if (!segment) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
assertValidHiddenSegment(segment, "REMOTE_WS_ALWAYS_HIDDEN");
|
|
42
|
+
segments.add(segment);
|
|
43
|
+
}
|
|
44
|
+
return segments;
|
|
45
|
+
}
|
|
26
46
|
const REPO_ROOT = path.resolve(process.env.REPO_ROOT ?? process.cwd());
|
|
27
47
|
const HOST = "127.0.0.1";
|
|
28
48
|
const PORT = parseIntegerFromEnv("REMOTE_WS_PORT", 18080, { min: 1, max: 65535 });
|
|
@@ -54,6 +74,7 @@ const ALLOWED_IMAGE_EXTENSIONS = new Set([
|
|
|
54
74
|
const IMAGE_CACHE_CONTROL = "private, max-age=60, stale-while-revalidate=300";
|
|
55
75
|
const METADATA_CACHE_CONTROL = "private, max-age=10, stale-while-revalidate=30";
|
|
56
76
|
const BASIC_AUTH_PASSWORD = process.env.REMOTE_WS_PASSWORD ?? "";
|
|
77
|
+
const ALWAYS_HIDDEN_SEGMENTS = parseAlwaysHiddenSegments(process.env.REMOTE_WS_ALWAYS_HIDDEN);
|
|
57
78
|
const AUTH_WINDOW_MS = 10 * 60 * 1000;
|
|
58
79
|
const AUTH_MAX_ATTEMPTS = 20;
|
|
59
80
|
const AUTH_BLOCK_MS = 15 * 60 * 1000;
|
|
@@ -223,7 +244,9 @@ function isHiddenRepoRelativePath(repoRelativePath) {
|
|
|
223
244
|
if (!repoRelativePath) {
|
|
224
245
|
return false;
|
|
225
246
|
}
|
|
226
|
-
return repoRelativePath
|
|
247
|
+
return repoRelativePath
|
|
248
|
+
.split("/")
|
|
249
|
+
.some((segment) => ALWAYS_HIDDEN_SEGMENTS.has(segment));
|
|
227
250
|
}
|
|
228
251
|
function isBlockedHiddenRepoRelativePath(repoRelativePath) {
|
|
229
252
|
return isHiddenRepoRelativePath(repoRelativePath);
|