mrmd-sync 0.2.0

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 ADDED
@@ -0,0 +1,925 @@
1
+ # mrmd-sync
2
+
3
+ Real-time sync server for collaborative markdown editing. Connects browsers to your filesystem with bidirectional sync via Yjs CRDTs.
4
+
5
+ ```
6
+ Browser ←──WebSocket──→ mrmd-sync ←──→ ./docs/*.md
7
+ │ │
8
+ └── Real-time collab ────┘
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```bash
14
+ # Start syncing a directory
15
+ npx mrmd-sync ./docs
16
+ ```
17
+
18
+ That's it. Open `ws://localhost:4444/readme` in mrmd-editor and start editing. Changes sync to `./docs/readme.md`.
19
+
20
+ ---
21
+
22
+ ## Table of Contents
23
+
24
+ - [Installation](#installation)
25
+ - [CLI Usage](#cli-usage)
26
+ - [Programmatic Usage](#programmatic-usage)
27
+ - [Examples](#examples)
28
+ - [Basic File Sync](#basic-file-sync)
29
+ - [With Authentication](#with-authentication)
30
+ - [Express Integration](#express-integration)
31
+ - [Multiple Directories](#multiple-directories)
32
+ - [Configuration](#configuration)
33
+ - [HTTP Endpoints](#http-endpoints)
34
+ - [API Reference](#api-reference)
35
+ - [How It Works](#how-it-works)
36
+ - [Security](#security)
37
+ - [Operational Notes](#operational-notes)
38
+ - [Troubleshooting](#troubleshooting)
39
+
40
+ ---
41
+
42
+ ## Installation
43
+
44
+ ```bash
45
+ # Global install
46
+ npm install -g mrmd-sync
47
+
48
+ # Or use npx (no install)
49
+ npx mrmd-sync ./docs
50
+
51
+ # Or add to project
52
+ npm install mrmd-sync
53
+ ```
54
+
55
+ ---
56
+
57
+ ## CLI Usage
58
+
59
+ ### Basic
60
+
61
+ ```bash
62
+ # Sync current directory's docs folder
63
+ mrmd-sync ./docs
64
+
65
+ # Custom port
66
+ mrmd-sync --port 8080 ./docs
67
+
68
+ # Short flag
69
+ mrmd-sync -p 8080 ./docs
70
+ ```
71
+
72
+ ### All Options
73
+
74
+ ```bash
75
+ mrmd-sync [options] [directory]
76
+
77
+ Options:
78
+ --port, -p <port> WebSocket port (default: 4444)
79
+ --debounce <ms> Write debounce delay (default: 1000)
80
+ --max-connections <n> Max total connections (default: 100)
81
+ --max-per-doc <n> Max connections per document (default: 50)
82
+ --max-file-size <bytes> Max file size to sync (default: 10485760)
83
+ --ping-interval <ms> Heartbeat interval (default: 30000)
84
+ --cleanup-delay <ms> Doc cleanup after disconnect (default: 60000)
85
+ --i-know-what-i-am-doing Allow syncing system paths (/, /etc, /home)
86
+ --help, -h Show help
87
+ ```
88
+
89
+ ### Example: Production Settings
90
+
91
+ ```bash
92
+ mrmd-sync \
93
+ --port 443 \
94
+ --max-connections 500 \
95
+ --max-per-doc 100 \
96
+ --debounce 2000 \
97
+ ./production-docs
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Programmatic Usage
103
+
104
+ ```javascript
105
+ import { createServer } from 'mrmd-sync';
106
+
107
+ const server = createServer({
108
+ dir: './docs',
109
+ port: 4444,
110
+ });
111
+
112
+ // Server is now running
113
+ console.log('Sync server started');
114
+
115
+ // Graceful shutdown
116
+ process.on('SIGINT', () => {
117
+ server.close();
118
+ });
119
+ ```
120
+
121
+ ---
122
+
123
+ ## Examples
124
+
125
+ ### Basic File Sync
126
+
127
+ The simplest setup - sync a folder:
128
+
129
+ ```javascript
130
+ import { createServer } from 'mrmd-sync';
131
+
132
+ // Start server
133
+ const server = createServer({
134
+ dir: './my-notes',
135
+ port: 4444,
136
+ });
137
+
138
+ // That's it! Connect from browser:
139
+ // ws://localhost:4444/meeting-notes → ./my-notes/meeting-notes.md
140
+ // ws://localhost:4444/todo → ./my-notes/todo.md
141
+ // ws://localhost:4444/journal/2024 → ./my-notes/journal/2024.md
142
+ ```
143
+
144
+ **Browser side (with mrmd-editor):**
145
+
146
+ ```javascript
147
+ import mrmd from 'mrmd-editor';
148
+
149
+ // Connect to sync server
150
+ const drive = mrmd.drive('ws://localhost:4444');
151
+
152
+ // Open a document
153
+ const editor = drive.open('meeting-notes', '#editor');
154
+
155
+ // Everything typed syncs automatically!
156
+ ```
157
+
158
+ ---
159
+
160
+ ### With Authentication
161
+
162
+ Protect documents with custom auth:
163
+
164
+ ```javascript
165
+ import { createServer } from 'mrmd-sync';
166
+ import jwt from 'jsonwebtoken';
167
+
168
+ const server = createServer({
169
+ dir: './docs',
170
+ port: 4444,
171
+
172
+ // Auth receives the HTTP request and document name
173
+ auth: async (req, docName) => {
174
+ // Get token from query string: ws://localhost:4444/doc?token=xxx
175
+ const url = new URL(req.url, 'http://localhost');
176
+ const token = url.searchParams.get('token');
177
+
178
+ if (!token) {
179
+ return false; // Reject connection
180
+ }
181
+
182
+ try {
183
+ const payload = jwt.verify(token, process.env.JWT_SECRET);
184
+
185
+ // Check document access
186
+ if (docName.startsWith('private/') && !payload.isAdmin) {
187
+ return false;
188
+ }
189
+
190
+ return true; // Allow connection
191
+ } catch {
192
+ return false; // Invalid token
193
+ }
194
+ },
195
+ });
196
+ ```
197
+
198
+ **Browser side:**
199
+
200
+ ```javascript
201
+ const token = await getAuthToken();
202
+ const drive = mrmd.drive(`ws://localhost:4444?token=${token}`);
203
+ ```
204
+
205
+ ---
206
+
207
+ ### Express Integration
208
+
209
+ Run alongside an Express server:
210
+
211
+ ```javascript
212
+ import express from 'express';
213
+ import { createServer } from 'mrmd-sync';
214
+
215
+ const app = express();
216
+
217
+ // Your REST API
218
+ app.get('/api/documents', (req, res) => {
219
+ res.json({ documents: ['readme', 'notes', 'todo'] });
220
+ });
221
+
222
+ // Start Express
223
+ const httpServer = app.listen(3000);
224
+
225
+ // Start sync server on different port
226
+ const syncServer = createServer({
227
+ dir: './docs',
228
+ port: 4444,
229
+ auth: async (req, docName) => {
230
+ // Validate session cookie
231
+ const sessionId = parseCookie(req.headers.cookie)?.session;
232
+ return await validateSession(sessionId);
233
+ },
234
+ });
235
+
236
+ console.log('API: http://localhost:3000');
237
+ console.log('Sync: ws://localhost:4444');
238
+ ```
239
+
240
+ ---
241
+
242
+ ### Multiple Directories
243
+
244
+ Run multiple sync servers for different purposes:
245
+
246
+ ```javascript
247
+ import { createServer } from 'mrmd-sync';
248
+
249
+ // Public docs - no auth, read-heavy
250
+ const publicDocs = createServer({
251
+ dir: './public-docs',
252
+ port: 4444,
253
+ maxConnections: 200,
254
+ maxConnectionsPerDoc: 100,
255
+ });
256
+
257
+ // Private workspace - auth required, smaller limits
258
+ const privateDocs = createServer({
259
+ dir: './private-docs',
260
+ port: 4445,
261
+ maxConnections: 20,
262
+ maxConnectionsPerDoc: 5,
263
+ auth: async (req, docName) => {
264
+ return checkAuth(req);
265
+ },
266
+ });
267
+
268
+ // Team collaboration - balanced settings
269
+ const teamDocs = createServer({
270
+ dir: './team-docs',
271
+ port: 4446,
272
+ maxConnections: 50,
273
+ maxConnectionsPerDoc: 20,
274
+ debounceMs: 500, // Faster saves for active collab
275
+ });
276
+ ```
277
+
278
+ ---
279
+
280
+ ### Monitoring & Stats
281
+
282
+ Get real-time statistics:
283
+
284
+ ```javascript
285
+ import { createServer } from 'mrmd-sync';
286
+
287
+ const server = createServer({ dir: './docs', port: 4444 });
288
+
289
+ // Check stats periodically
290
+ setInterval(() => {
291
+ const stats = server.getStats();
292
+ console.log(`Connections: ${stats.totalConnections}`);
293
+ console.log(`Active docs: ${stats.totalDocs}`);
294
+
295
+ // Per-document breakdown
296
+ stats.docs.forEach(doc => {
297
+ console.log(` ${doc.name}: ${doc.connections} clients`);
298
+ });
299
+ }, 10000);
300
+ ```
301
+
302
+ **Example output:**
303
+
304
+ ```
305
+ Connections: 12
306
+ Active docs: 3
307
+ readme: 5 clients
308
+ meeting-notes: 4 clients
309
+ todo: 3 clients
310
+ ```
311
+
312
+ ---
313
+
314
+ ### Custom File Handling
315
+
316
+ Access the underlying Yjs documents:
317
+
318
+ ```javascript
319
+ import { createServer } from 'mrmd-sync';
320
+
321
+ const server = createServer({ dir: './docs', port: 4444 });
322
+
323
+ // Get a specific document
324
+ const doc = server.getDoc('readme');
325
+
326
+ // Access Yjs Y.Text
327
+ const content = doc.ytext.toString();
328
+ console.log('Current content:', content);
329
+
330
+ // Watch for changes
331
+ doc.ydoc.on('update', () => {
332
+ console.log('Document updated!');
333
+ console.log('New content:', doc.ytext.toString());
334
+ });
335
+
336
+ // Programmatically edit (syncs to all clients!)
337
+ doc.ytext.insert(0, '# Hello\n\n');
338
+ ```
339
+
340
+ ---
341
+
342
+ ### Subdirectory Support
343
+
344
+ Organize documents in folders:
345
+
346
+ ```javascript
347
+ const server = createServer({ dir: './docs', port: 4444 });
348
+
349
+ // These all work:
350
+ // ws://localhost:4444/readme → ./docs/readme.md
351
+ // ws://localhost:4444/notes/daily → ./docs/notes/daily.md
352
+ // ws://localhost:4444/2024/jan/01 → ./docs/2024/jan/01.md
353
+ ```
354
+
355
+ **Browser side:**
356
+
357
+ ```javascript
358
+ const drive = mrmd.drive('ws://localhost:4444');
359
+
360
+ // Open nested documents
361
+ drive.open('notes/daily', '#editor1');
362
+ drive.open('2024/jan/01', '#editor2');
363
+ ```
364
+
365
+ ---
366
+
367
+ ## Configuration
368
+
369
+ ### Full Options Reference
370
+
371
+ ```javascript
372
+ createServer({
373
+ // === Directory & Port ===
374
+ dir: './docs', // Base directory for .md files
375
+ port: 4444, // WebSocket server port
376
+
377
+ // === Authentication ===
378
+ auth: async (req, docName) => {
379
+ // req: HTTP upgrade request (has headers, url, etc.)
380
+ // docName: requested document name
381
+ // Return true to allow, false to reject
382
+ return true;
383
+ },
384
+
385
+ // === Performance ===
386
+ debounceMs: 1000, // Delay before writing to disk (ms)
387
+ // Lower = faster saves, more disk I/O
388
+ // Higher = batched writes, less I/O
389
+
390
+ // === Limits ===
391
+ maxConnections: 100, // Total WebSocket connections allowed
392
+ maxConnectionsPerDoc: 50,// Connections per document
393
+ maxMessageSize: 1048576, // Max WebSocket message (1MB)
394
+ maxFileSize: 10485760, // Max file size to sync (10MB)
395
+
396
+ // === Timeouts ===
397
+ pingIntervalMs: 30000, // Heartbeat ping interval (30s)
398
+ // Detects dead connections
399
+ docCleanupDelayMs: 60000,// Cleanup delay after last disconnect (60s)
400
+ // Keeps doc in memory briefly for reconnects
401
+
402
+ // === Security ===
403
+ dangerouslyAllowSystemPaths: false, // Must be true for /, /etc, /home, etc.
404
+ });
405
+ ```
406
+
407
+ ### Recommended Settings by Use Case
408
+
409
+ **Local Development:**
410
+ ```javascript
411
+ {
412
+ dir: './docs',
413
+ port: 4444,
414
+ debounceMs: 500, // Fast feedback
415
+ docCleanupDelayMs: 5000, // Quick cleanup
416
+ }
417
+ ```
418
+
419
+ **Team Collaboration:**
420
+ ```javascript
421
+ {
422
+ dir: './team-docs',
423
+ port: 4444,
424
+ maxConnections: 50,
425
+ maxConnectionsPerDoc: 20,
426
+ debounceMs: 1000,
427
+ auth: validateTeamMember,
428
+ }
429
+ ```
430
+
431
+ **Public Documentation:**
432
+ ```javascript
433
+ {
434
+ dir: './public-docs',
435
+ port: 4444,
436
+ maxConnections: 500,
437
+ maxConnectionsPerDoc: 200,
438
+ debounceMs: 2000, // Reduce write load
439
+ maxFileSize: 1048576, // 1MB limit
440
+ }
441
+ ```
442
+
443
+ ---
444
+
445
+ ## HTTP Endpoints
446
+
447
+ The sync server exposes HTTP endpoints for monitoring and health checks.
448
+
449
+ > **Note:** These endpoints are **unauthenticated** by design - they're intended for internal monitoring (load balancers, Kubernetes probes, Prometheus, etc.). If you need to protect them, put a reverse proxy in front.
450
+
451
+ ### `GET /health` or `GET /healthz`
452
+
453
+ Health check for load balancers and orchestrators.
454
+
455
+ ```bash
456
+ curl http://localhost:4444/health
457
+ ```
458
+
459
+ ```json
460
+ {
461
+ "status": "healthy",
462
+ "shutting_down": false
463
+ }
464
+ ```
465
+
466
+ Returns `200` when healthy, `503` when shutting down.
467
+
468
+ ### `GET /metrics`
469
+
470
+ Server metrics in JSON format.
471
+
472
+ ```bash
473
+ curl http://localhost:4444/metrics
474
+ ```
475
+
476
+ ```json
477
+ {
478
+ "uptime": 3600,
479
+ "connections": {
480
+ "total": 150,
481
+ "active": 12
482
+ },
483
+ "messages": {
484
+ "total": 45230,
485
+ "bytesIn": 1048576,
486
+ "bytesOut": 2097152
487
+ },
488
+ "files": {
489
+ "saves": 89,
490
+ "loads": 23
491
+ },
492
+ "errors": 0,
493
+ "lastActivity": "2024-01-15T10:30:00.000Z"
494
+ }
495
+ ```
496
+
497
+ ### `GET /stats`
498
+
499
+ Detailed statistics including per-document breakdown.
500
+
501
+ ```bash
502
+ curl http://localhost:4444/stats
503
+ ```
504
+
505
+ ```json
506
+ {
507
+ "uptime": 3600,
508
+ "connections": { "total": 150, "active": 12 },
509
+ "documents": [
510
+ { "name": "readme", "connections": 5, "path": "/docs/readme.md" },
511
+ { "name": "notes/daily", "connections": 7, "path": "/docs/notes/daily.md" }
512
+ ],
513
+ "config": {
514
+ "port": 4444,
515
+ "dir": "/docs",
516
+ "debounceMs": 1000,
517
+ "maxConnections": 100,
518
+ "maxConnectionsPerDoc": 50
519
+ }
520
+ }
521
+ ```
522
+
523
+ ---
524
+
525
+ ## API Reference
526
+
527
+ ### `createServer(options)`
528
+
529
+ Creates and starts a sync server.
530
+
531
+ **Parameters:**
532
+
533
+ | Option | Type | Default | Description |
534
+ |--------|------|---------|-------------|
535
+ | `dir` | `string` | `'./docs'` | Directory to sync |
536
+ | `port` | `number` | `4444` | WebSocket port |
537
+ | `auth` | `function` | `null` | Auth handler `(req, docName) => boolean \| Promise<boolean>` |
538
+ | `debounceMs` | `number` | `1000` | Write debounce delay |
539
+ | `maxConnections` | `number` | `100` | Max total connections |
540
+ | `maxConnectionsPerDoc` | `number` | `50` | Max connections per doc |
541
+ | `maxMessageSize` | `number` | `1048576` | Max message size (bytes) |
542
+ | `maxFileSize` | `number` | `10485760` | Max file size (bytes) |
543
+ | `pingIntervalMs` | `number` | `30000` | Heartbeat interval |
544
+ | `docCleanupDelayMs` | `number` | `60000` | Cleanup delay |
545
+ | `dangerouslyAllowSystemPaths` | `boolean` | `false` | Allow syncing `/`, `/etc`, `/home`, etc. |
546
+
547
+ **Returns:** `Server` object
548
+
549
+ ---
550
+
551
+ ### Server Object
552
+
553
+ ```javascript
554
+ const server = createServer(options);
555
+ ```
556
+
557
+ #### `server.getDoc(name)`
558
+
559
+ Get or create a document by name.
560
+
561
+ ```javascript
562
+ const doc = server.getDoc('readme');
563
+ // doc.ydoc - Y.Doc instance
564
+ // doc.ytext - Y.Text instance
565
+ // doc.awareness - Awareness instance
566
+ // doc.conns - Set of WebSocket connections
567
+ // doc.filePath - Path to .md file
568
+ ```
569
+
570
+ #### `server.getStats()`
571
+
572
+ Get current statistics.
573
+
574
+ ```javascript
575
+ const stats = server.getStats();
576
+ // {
577
+ // totalConnections: 12,
578
+ // totalDocs: 3,
579
+ // docs: [
580
+ // { name: 'readme', connections: 5 },
581
+ // { name: 'notes', connections: 7 },
582
+ // ]
583
+ // }
584
+ ```
585
+
586
+ #### `server.close()`
587
+
588
+ Gracefully shutdown the server.
589
+
590
+ ```javascript
591
+ server.close();
592
+ // - Closes all WebSocket connections
593
+ // - Stops file watcher
594
+ // - Cleans up Y.Doc instances
595
+ ```
596
+
597
+ #### `server.config`
598
+
599
+ Access the resolved configuration.
600
+
601
+ ```javascript
602
+ console.log(server.config);
603
+ // { dir: './docs', port: 4444, debounceMs: 1000, ... }
604
+ ```
605
+
606
+ #### `server.wss`
607
+
608
+ Access the underlying WebSocket server (ws library).
609
+
610
+ ```javascript
611
+ server.wss.clients.forEach(client => {
612
+ console.log('Client connected');
613
+ });
614
+ ```
615
+
616
+ #### `server.docs`
617
+
618
+ Access the Map of loaded documents.
619
+
620
+ ```javascript
621
+ server.docs.forEach((doc, name) => {
622
+ console.log(`${name}: ${doc.ytext.length} chars`);
623
+ });
624
+ ```
625
+
626
+ ---
627
+
628
+ ## How It Works
629
+
630
+ ### Architecture
631
+
632
+ ```
633
+ ┌─────────────────────────────────────────────────────────┐
634
+ │ mrmd-sync │
635
+ ├─────────────────────────────────────────────────────────┤
636
+ │ │
637
+ │ Browser A ──┐ │
638
+ │ │ ┌──────────┐ ┌──────────────┐ │
639
+ │ Browser B ──┼────►│ Y.Doc │◄───►│ readme.md │ │
640
+ │ │ │ (CRDT) │ │ (on disk) │ │
641
+ │ Browser C ──┘ └──────────┘ └──────────────┘ │
642
+ │ │ │
643
+ │ Yjs Protocol │
644
+ │ - Sync updates │
645
+ │ - Awareness (cursors) │
646
+ │ │
647
+ └─────────────────────────────────────────────────────────┘
648
+ ```
649
+
650
+ ### Sync Flow
651
+
652
+ **Browser → File:**
653
+ 1. User types in browser
654
+ 2. mrmd-editor sends Yjs update via WebSocket
655
+ 3. Server applies update to Y.Doc
656
+ 4. Server broadcasts to other clients
657
+ 5. Debounced write to `.md` file
658
+
659
+ **File → Browser:**
660
+ 1. External edit (VS Code, git pull, etc.)
661
+ 2. Chokidar detects file change
662
+ 3. Server reads new content
663
+ 4. Character-level diff computed
664
+ 5. Diff applied to Y.Doc
665
+ 6. Update broadcast to all browsers
666
+
667
+ ### Why Yjs?
668
+
669
+ [Yjs](https://yjs.dev) is a CRDT (Conflict-free Replicated Data Type) library that:
670
+
671
+ - **Never loses data** - Concurrent edits merge automatically
672
+ - **Works offline** - Changes sync when reconnected
673
+ - **Character-level sync** - Only changed characters transmitted
674
+ - **Proven** - Used by Notion, Linear, and others
675
+
676
+ ---
677
+
678
+ ## Security
679
+
680
+ ### System Path Protection
681
+
682
+ By default, mrmd-sync **refuses to sync system directories** like `/`, `/etc`, `/home`, `/var`, etc. This prevents accidentally exposing sensitive system files over WebSocket.
683
+
684
+ ```bash
685
+ # This will be REJECTED:
686
+ mrmd-sync /
687
+ mrmd-sync /home
688
+ mrmd-sync /etc/myapp
689
+ ```
690
+
691
+ ```
692
+ Error: Refusing to sync dangerous system path: "/"
693
+
694
+ Syncing system directories (/, /etc, /home, etc.) can expose sensitive files
695
+ and allow remote file creation anywhere on your system.
696
+ ```
697
+
698
+ ### When You Actually Need System Access
699
+
700
+ If you're building a personal file server, NAS interface, or system admin tool, you can explicitly opt-in:
701
+
702
+ **CLI:**
703
+ ```bash
704
+ # The flag name makes you think twice
705
+ mrmd-sync --i-know-what-i-am-doing /
706
+
707
+ # Or the longer version
708
+ mrmd-sync --dangerous-allow-system-paths /home
709
+ ```
710
+
711
+ **Programmatic:**
712
+ ```javascript
713
+ import { createServer } from 'mrmd-sync';
714
+
715
+ const server = createServer({
716
+ dir: '/',
717
+ dangerouslyAllowSystemPaths: true, // Required for /, /etc, /home, etc.
718
+
719
+ // STRONGLY RECOMMENDED: Add authentication!
720
+ auth: async (req, docName) => {
721
+ const token = getTokenFromRequest(req);
722
+ return validateToken(token);
723
+ },
724
+ });
725
+ ```
726
+
727
+ ### What's Considered Dangerous?
728
+
729
+ These paths require explicit opt-in:
730
+
731
+ | Path | Why It's Dangerous |
732
+ |------|-------------------|
733
+ | `/` | Access to entire filesystem |
734
+ | `/etc` | System configuration files |
735
+ | `/home` | All users' home directories |
736
+ | `/var` | System logs, databases |
737
+ | `/usr` | System binaries |
738
+ | `/root` | Root user's home |
739
+ | `/bin`, `/sbin` | System executables |
740
+
741
+ **Safe by default (no flag needed):**
742
+ - `/home/youruser/docs` - User-specific subdirectory
743
+ - `./docs` - Relative paths
744
+ - `/srv/myapp/data` - Application-specific paths
745
+
746
+ ### Best Practices for System Access
747
+
748
+ If you enable system path access:
749
+
750
+ 1. **Always use authentication**
751
+ ```javascript
752
+ createServer({
753
+ dir: '/',
754
+ dangerouslyAllowSystemPaths: true,
755
+ auth: (req, docName) => validateUser(req),
756
+ })
757
+ ```
758
+
759
+ 2. **Run as unprivileged user**
760
+ ```bash
761
+ # Don't run as root!
762
+ sudo -u www-data mrmd-sync --i-know-what-i-am-doing /srv/files
763
+ ```
764
+
765
+ 3. **Use a reverse proxy with TLS**
766
+ ```nginx
767
+ location /sync {
768
+ proxy_pass http://localhost:4444;
769
+ proxy_http_version 1.1;
770
+ proxy_set_header Upgrade $http_upgrade;
771
+ proxy_set_header Connection "upgrade";
772
+ }
773
+ ```
774
+
775
+ 4. **Consider read-only for sensitive paths**
776
+ ```javascript
777
+ auth: async (req, docName) => {
778
+ // Block writes to sensitive directories
779
+ if (docName.startsWith('etc/') || docName.startsWith('var/')) {
780
+ return false;
781
+ }
782
+ return validateUser(req);
783
+ }
784
+ ```
785
+
786
+ ---
787
+
788
+ ## Operational Notes
789
+
790
+ ### Instance Locking
791
+
792
+ mrmd-sync uses a PID file (`.mrmd-sync/server.pid`) to prevent multiple instances from running on the same directory. If you try to start a second instance:
793
+
794
+ ```
795
+ Error: Another mrmd-sync instance is already running on this directory.
796
+
797
+ PID: 12345
798
+ Port: 4444
799
+ Started: 2024-01-15T10:30:00.000Z
800
+
801
+ Stop the other instance first, or use a different directory.
802
+ ```
803
+
804
+ If a previous instance crashed without cleanup, the stale PID file is automatically detected and removed.
805
+
806
+ ### Yjs Document Growth
807
+
808
+ Yjs stores operation history for undo/redo and conflict resolution. Long-running sessions with many edits will accumulate history, increasing memory usage. This is expected behavior - documents are cleaned up after clients disconnect (controlled by `docCleanupDelayMs`).
809
+
810
+ For high-traffic scenarios, consider:
811
+ - Lower `docCleanupDelayMs` to free memory faster
812
+ - Restart periodically during maintenance windows
813
+
814
+ ### External File Edits
815
+
816
+ When files are edited externally (VS Code, git pull, etc.):
817
+ 1. Chokidar detects the change
818
+ 2. A character-level diff is computed
819
+ 3. Changes are applied to the Yjs document
820
+ 4. All connected clients receive the update
821
+
822
+ This is intentional - the `.md` file is the source of truth. However, simultaneous external edits and browser edits may result in merged content.
823
+
824
+ ### Crash Recovery
825
+
826
+ If `persistYjsState: true` (default), Yjs snapshots are saved to `.mrmd-sync/*.yjs`. On restart:
827
+ 1. Snapshot is loaded (contains recent edits)
828
+ 2. File is read
829
+ 3. If they differ, the file wins (it's the source of truth)
830
+
831
+ This protects against corrupt snapshots but means edits between the last file write and a crash may be lost. The `debounceMs` setting controls how quickly edits are persisted to the file.
832
+
833
+ ---
834
+
835
+ ## Troubleshooting
836
+
837
+ ### Connection Rejected
838
+
839
+ **"Max connections reached"**
840
+ ```javascript
841
+ // Increase limit
842
+ createServer({ maxConnections: 200 })
843
+ ```
844
+
845
+ **"Invalid document name"**
846
+ ```
847
+ // Document names must:
848
+ // - Not contain ".."
849
+ // - Not start with "/" or "\"
850
+ // - Only contain: letters, numbers, dashes, underscores, dots, slashes
851
+ //
852
+ // Valid: readme, notes/daily, 2024-01-01, my_doc
853
+ // Invalid: ../secret, /etc/passwd, doc<script>
854
+ ```
855
+
856
+ **"Unauthorized"**
857
+ ```javascript
858
+ // Check your auth function
859
+ auth: async (req, docName) => {
860
+ console.log('Auth attempt:', docName, req.headers);
861
+ // Make sure you return true for valid requests
862
+ return true;
863
+ }
864
+ ```
865
+
866
+ ### File Not Syncing
867
+
868
+ **Check file path:**
869
+ ```javascript
870
+ const doc = server.getDoc('readme');
871
+ console.log('File path:', doc.filePath);
872
+ // Should be: ./docs/readme.md
873
+ ```
874
+
875
+ **Check file permissions:**
876
+ ```bash
877
+ ls -la ./docs/
878
+ # Ensure writable
879
+ ```
880
+
881
+ **Check file size:**
882
+ ```javascript
883
+ // Default max is 10MB
884
+ createServer({ maxFileSize: 50 * 1024 * 1024 }) // 50MB
885
+ ```
886
+
887
+ ### High Memory Usage
888
+
889
+ **Reduce cleanup delay:**
890
+ ```javascript
891
+ createServer({
892
+ docCleanupDelayMs: 10000, // 10s instead of 60s
893
+ })
894
+ ```
895
+
896
+ **Check for leaked connections:**
897
+ ```javascript
898
+ setInterval(() => {
899
+ const stats = server.getStats();
900
+ console.log('Active docs:', stats.totalDocs);
901
+ console.log('Connections:', stats.totalConnections);
902
+ }, 5000);
903
+ ```
904
+
905
+ ### Slow Performance
906
+
907
+ **Increase debounce for write-heavy workloads:**
908
+ ```javascript
909
+ createServer({
910
+ debounceMs: 2000, // 2 seconds
911
+ })
912
+ ```
913
+
914
+ **Reduce max file size:**
915
+ ```javascript
916
+ createServer({
917
+ maxFileSize: 1024 * 1024, // 1MB
918
+ })
919
+ ```
920
+
921
+ ---
922
+
923
+ ## License
924
+
925
+ MIT