@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 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 dotfiles/dot-directories (for example `.env`, `.git`)
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 still blocks all hidden paths and gitignored paths
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.split("/").some((segment) => segment.startsWith("."));
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haowjy/remote-workspace",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Mobile-friendly web workspace for SSH + tmux workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {