@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 +13 -0
- package/package.json +1 -1
- package/server.js +27 -4
- package/skills/markdown-viewer/SKILL.md +4 -3
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.
|
|
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
|
-
|
|
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.
|
|
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
|