@wipcomputer/markdown-viewer 1.0.7 → 1.0.9

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
@@ -129,6 +129,19 @@ That's it. The server watches the file and pushes updates via SSE. You don't nee
129
129
 
130
130
  ---
131
131
 
132
+ ## Security
133
+
134
+ - Server binds to `127.0.0.1` only. It is not accessible from other machines.
135
+ - The `/view?path=` parameter reads files from your local filesystem. Use `--root` to restrict access to a specific directory tree:
136
+
137
+ ```bash
138
+ mdview --root /path/to/your/project
139
+ ```
140
+
141
+ When `--root` is set, the server will only serve files under that directory. Requests for files outside it return 404. Recommended when running in shared or multi-user environments.
142
+
143
+ ---
144
+
132
145
  ## License
133
146
 
134
147
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wipcomputer/markdown-viewer",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Live markdown viewer for AI pair-editing. When you collaborate, the updates render instantly. Works with any AI agent and web browser.",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // Usage:
6
6
  // mdview Start server, open homepage
7
7
  // mdview --port 8080 Use custom port
8
+ // mdview --root /path/to/dir Restrict file access to this directory
8
9
  //
9
10
  // Opens browser to http://127.0.0.1:3000/ — pick files, view with live reload.
10
11
 
@@ -20,18 +21,28 @@ const __dirname = dirname(__filename);
20
21
  // ── Parse args ───────────────────────────────────────────────────────
21
22
 
22
23
  let port = 3000;
24
+ let rootDir = null;
23
25
 
24
26
  const args = process.argv.slice(2);
25
27
  for (let i = 0; i < args.length; i++) {
26
28
  if (args[i] === "--port" && args[i + 1]) {
27
29
  port = parseInt(args[i + 1], 10);
28
30
  i++;
31
+ } else if (args[i] === "--root" && args[i + 1]) {
32
+ rootDir = resolve(args[i + 1]);
33
+ i++;
29
34
  } else if (args[i] === "--help" || args[i] === "-h") {
30
35
  console.log(`mdview: live markdown viewer
31
36
 
32
37
  Usage:
33
38
  mdview Start server, open homepage
34
39
  mdview --port 8080 Use custom port
40
+ mdview --root /path/to/dir Restrict file access to this directory
41
+
42
+ Options:
43
+ --root <dir> Only serve files under this directory. Prevents access
44
+ to files outside the specified path. Recommended for
45
+ shared or multi-user environments.
35
46
 
36
47
  Opens browser to http://127.0.0.1:PORT/ — pick files, view with live reload.
37
48
  Works in all browsers (Safari, Chrome, Firefox).`);
@@ -39,6 +50,16 @@ Works in all browsers (Safari, Chrome, Firefox).`);
39
50
  }
40
51
  }
41
52
 
53
+ // Validate that a file path is allowed. Returns the resolved path or null.
54
+ function validateFilePath(filePath) {
55
+ if (!filePath) return null;
56
+ const resolved = resolve(filePath);
57
+ if (rootDir && !resolved.startsWith(rootDir + "/") && resolved !== rootDir) {
58
+ return null;
59
+ }
60
+ return resolved;
61
+ }
62
+
42
63
  // ── Multi-file watcher ──────────────────────────────────────────────
43
64
 
44
65
  // Map<absolutePath, { clients: Set<res>, lastMtime: number }>
@@ -224,7 +245,7 @@ const server = createServer((req, res) => {
224
245
 
225
246
  // Viewer — file loaded with live reload (path) or sessionStorage (name)
226
247
  if (url.pathname === "/view") {
227
- const filePath = url.searchParams.get("path");
248
+ const filePath = validateFilePath(url.searchParams.get("path"));
228
249
  const fileName = url.searchParams.get("name");
229
250
 
230
251
  if (filePath && existsSync(filePath)) {
@@ -244,7 +265,7 @@ const server = createServer((req, res) => {
244
265
 
245
266
  // API: read a specific file
246
267
  if (url.pathname === "/api/file") {
247
- const filePath = url.searchParams.get("path");
268
+ const filePath = validateFilePath(url.searchParams.get("path"));
248
269
  if (!filePath || !existsSync(filePath)) {
249
270
  res.writeHead(404, { "Content-Type": "text/plain" });
250
271
  res.end("File not found");
@@ -263,7 +284,7 @@ const server = createServer((req, res) => {
263
284
 
264
285
  // API: SSE events for a specific file
265
286
  if (url.pathname === "/api/events") {
266
- const filePath = url.searchParams.get("path");
287
+ const filePath = validateFilePath(url.searchParams.get("path"));
267
288
  if (!filePath || !existsSync(filePath)) {
268
289
  res.writeHead(404, { "Content-Type": "text/plain" });
269
290
  res.end("File not found");
@@ -289,7 +310,8 @@ const server = createServer((req, res) => {
289
310
  if (refPath) {
290
311
  const fileDir = dirname(refPath);
291
312
  const requestedPath = resolve(fileDir, url.pathname.slice(1));
292
- if (requestedPath.startsWith(fileDir) && existsSync(requestedPath) && statSync(requestedPath).isFile()) {
313
+ const validatedStatic = validateFilePath(requestedPath);
314
+ if (validatedStatic && requestedPath.startsWith(fileDir) && existsSync(requestedPath) && statSync(requestedPath).isFile()) {
293
315
  const ext = extname(requestedPath).toLowerCase();
294
316
  res.writeHead(200, { "Content-Type": mimeTypes[ext] || "application/octet-stream" });
295
317
  res.end(readFileSync(requestedPath));
@@ -308,6 +330,7 @@ const server = createServer((req, res) => {
308
330
  server.listen(port, "127.0.0.1", () => {
309
331
  const url = `http://127.0.0.1:${port}`;
310
332
  console.log(`mdview: ${url}`);
333
+ if (rootDir) console.log(`root: ${rootDir} (file access restricted)`);
311
334
  console.log(`Press Ctrl+C to stop.\n`);
312
335
 
313
336
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
@@ -27,6 +27,9 @@ metadata:
27
27
 
28
28
  Live markdown viewer for AI pair-editing. When you collaborate, the updates render instantly. Works with any AI agent and web browser.
29
29
 
30
+ GitHub: [wipcomputer/wip-markdown-viewer](https://github.com/wipcomputer/wip-markdown-viewer)
31
+ npm: [@wipcomputer/markdown-viewer](https://www.npmjs.com/package/@wipcomputer/markdown-viewer)
32
+
30
33
  ## Install
31
34
 
32
35
  ```bash
@@ -35,8 +38,6 @@ npm install -g @wipcomputer/markdown-viewer
35
38
 
36
39
  This installs the `mdview` command globally. Zero runtime dependencies. Pure Node.js.
37
40
 
38
- Source: [github.com/wipcomputer/wip-markdown-viewer](https://github.com/wipcomputer/wip-markdown-viewer)
39
-
40
41
  ## Quick start
41
42
 
42
43
  Start the server (binds to 127.0.0.1 only, never exposed to the network):
@@ -70,7 +71,7 @@ Open multiple tabs to work on multiple documents at once.
70
71
  ## Security
71
72
 
72
73
  - Server binds to `127.0.0.1` only. It is not accessible from other machines.
73
- - The `/view?path=` parameter reads files from your local filesystem. This is expected behavior for a local viewer. Do not expose the port to untrusted networks.
74
+ - The `/view?path=` parameter reads files from your local filesystem. Use `--root <dir>` to restrict access to a specific directory tree. Recommended for shared environments.
74
75
  - Zero npm dependencies. No supply chain risk beyond Node.js itself.
75
76
 
76
77
  ## Features