javascript-solid-server 0.0.83 → 0.0.85

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.
@@ -320,7 +320,10 @@
320
320
  "WebFetch(domain:remotestorage.io)",
321
321
  "Bash(gh issue create --title \"Feature: --public flag to skip WAC and allow open access\" --body \"$\\(cat << ''ENDOFFILE''\n## Summary\n\nAdd a `--public` flag that disables WAC \\(Web Access Control\\) checks, allowing unauthenticated read/write access to all resources. This enables JSS to be used as a simple file server similar to `npx serve`, but with REST write capabilities.\n\n**Difficulty**: 15/100 \n**Estimated Effort**: 2-4 hours \n**Dependencies**: None\n\n---\n\n## Motivation\n\nCurrently, JSS requires either:\n1. ACL files to grant access, or\n2. Authentication via Solid-OIDC\n\nThis makes it impossible to use JSS as a simple \"just serve this folder\" tool like `npx serve`. The `--public` flag would enable:\n\n1. **Quick local development** - No auth setup needed\n2. **Simple file sharing** - LAN file server with write support\n3. **jsserve wrapper** - Foundation for `npx jsserve` \\(see future issue\\)\n4. **WebDAV alternative** - Simple REST-based file server\n5. **Testing/demos** - Quick Solid server without auth complexity\n\n---\n\n## Proposed Implementation\n\n### CLI Flag\n\n```bash\njss start --public # Open read/write, no auth\njss start --public --read-only # Open read, no writes \\(like npx serve\\)\n```\n\n### Environment Variable\n\n```bash\nJSS_PUBLIC=true jss start\n```\n\n### Config File\n\n```json\n{\n \"public\": true\n}\n```\n\n---\n\n## Implementation Details\n\n### 1. Add flag to CLI \\(`bin/jss.js`\\)\n\n```javascript\n.option\\(''--public'', ''Allow unauthenticated access to all resources \\(disables WAC\\)''\\)\n.option\\(''--read-only'', ''Disable PUT/DELETE methods \\(read-only mode\\)''\\)\n```\n\n### 2. Add to config \\(`src/config.js`\\)\n\n```javascript\nconst DEFAULTS = {\n // ... existing ...\n public: false,\n readOnly: false,\n};\n\n// Environment variable mapping\nif \\(process.env.JSS_PUBLIC\\) {\n config.public = process.env.JSS_PUBLIC === ''true'';\n}\nif \\(process.env.JSS_READ_ONLY\\) {\n config.readOnly = process.env.JSS_READ_ONLY === ''true'';\n}\n```\n\n### 3. Skip WAC when public \\(`src/auth/middleware.js`\\)\n\n```javascript\nexport async function authorize\\(request, reply\\) {\n // Public mode - skip all auth/WAC checks\n if \\(request.config?.public\\) {\n return; // Allow request to proceed\n }\n \n // ... existing WAC logic ...\n}\n```\n\n### 4. Block writes when read-only \\(`src/handlers/resource.js`, `src/handlers/container.js`\\)\n\n```javascript\n// At start of PUT/DELETE handlers\nif \\(request.config?.readOnly\\) {\n return reply.code\\(405\\).send\\({ \n error: ''Method Not Allowed'',\n message: ''Server is in read-only mode''\n }\\);\n}\n```\n\n---\n\n## Behavior Matrix\n\n| Flag Combination | GET | PUT/DELETE | Auth Required |\n|------------------|-----|------------|---------------|\n| \\(default\\) | ACL | ACL | Yes \\(if ACL requires\\) |\n| `--public` | ✅ Allow | ✅ Allow | No |\n| `--public --read-only` | ✅ Allow | ❌ Block | No |\n| `--read-only` \\(no public\\) | ACL | ❌ Block | Yes |\n\n---\n\n## Security Considerations\n\n### Warning on Startup\n\nWhen `--public` is enabled, show a clear warning:\n\n```\n⚠️ WARNING: Server running in PUBLIC mode\n All files are readable and writable without authentication.\n Do not use in production or expose to the internet.\n```\n\n### Binding to localhost by default?\n\nConsider: When `--public` is set, should the default host be `localhost` instead of `0.0.0.0`?\n\n```javascript\nif \\(config.public && !explicitHostSet\\) {\n config.host = ''localhost''; // Safer default for public mode\n}\n```\n\nUser can override with `--public --host 0.0.0.0` if they explicitly want network access.\n\n---\n\n## Examples\n\n### Local Development\n```bash\n# Quick Solid-compatible file server for development\njss start --public --port 3000 --root ./test-data\n```\n\n### Read-Only File Sharing\n```bash\n# Share files on LAN, no writes allowed\njss start --public --read-only --host 0.0.0.0 --root ~/shared\n```\n\n### Testing Solid Apps\n```bash\n# Test app without auth complexity\njss start --public --root ./fixtures\nnpm test\n```\n\n---\n\n## Files to Modify\n\n| File | Changes |\n|------|---------|\n| `bin/jss.js` | Add `--public` and `--read-only` options \\(~5 LOC\\) |\n| `src/config.js` | Add defaults and env var mapping \\(~10 LOC\\) |\n| `src/auth/middleware.js` | Skip WAC when public \\(~5 LOC\\) |\n| `src/handlers/resource.js` | Block writes when read-only \\(~5 LOC\\) |\n| `src/handlers/container.js` | Block writes when read-only \\(~5 LOC\\) |\n| **Total** | **~30 LOC** |\n\n---\n\n## Testing\n\n```javascript\ndescribe\\(''--public flag'', \\(\\) => {\n it\\(''should allow unauthenticated GET'', async \\(\\) => {\n const server = await createServer\\({ public: true, root: tmpDir }\\);\n const res = await request\\(server\\).get\\(''/file.txt''\\);\n expect\\(res.status\\).toBe\\(200\\);\n }\\);\n\n it\\(''should allow unauthenticated PUT'', async \\(\\) => {\n const server = await createServer\\({ public: true, root: tmpDir }\\);\n const res = await request\\(server\\)\n .put\\(''/new-file.txt''\\)\n .send\\(''content''\\);\n expect\\(res.status\\).toBe\\(201\\);\n }\\);\n\n it\\(''should block PUT when read-only'', async \\(\\) => {\n const server = await createServer\\({ public: true, readOnly: true, root: tmpDir }\\);\n const res = await request\\(server\\)\n .put\\(''/new-file.txt''\\)\n .send\\(''content''\\);\n expect\\(res.status\\).toBe\\(405\\);\n }\\);\n}\\);\n```\n\n---\n\n## Related Issues\n\n- Future: `jsserve` package \\(thin wrapper using this flag\\)\n- #100 - Production Readiness \\(this is a dev/convenience feature\\)\n\n---\n\n## Open Questions\n\n1. Should `--public` default to `localhost` binding for safety?\n2. Should there be a `--public-read` \\(read-only public\\) shorthand?\n3. Should `--public` disable IdP/login UI entirely, or just make it optional?\nENDOFFILE\n\\)\")",
322
322
  "Bash(npx serve --help:*)",
323
- "Bash(npm exec serve:*)"
323
+ "Bash(npm exec serve:*)",
324
+ "Bash(npm link)",
325
+ "Bash(npm link:*)",
326
+ "Bash(git push)"
324
327
  ]
325
328
  }
326
329
  }
package/bin/jss.js CHANGED
@@ -78,6 +78,7 @@ program
78
78
  .option('--no-webid-tls', 'Disable WebID-TLS authentication')
79
79
  .option('--public', 'Allow unauthenticated access (skip WAC, open read/write)')
80
80
  .option('--read-only', 'Disable PUT/DELETE/PATCH methods (read-only mode)')
81
+ .option('--live-reload', 'Inject live reload script into HTML (auto-refresh on changes)')
81
82
  .option('-q, --quiet', 'Suppress log output')
82
83
  .option('--print-config', 'Print configuration and exit')
83
84
  .action(async (options) => {
@@ -135,6 +136,7 @@ program
135
136
  singleUserName: config.singleUserName,
136
137
  public: config.public,
137
138
  readOnly: config.readOnly,
139
+ liveReload: config.liveReload,
138
140
  });
139
141
 
140
142
  await server.listen({ port: config.port, host: config.host });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.83",
3
+ "version": "0.0.85",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/config.js CHANGED
@@ -79,6 +79,9 @@ export const defaults = {
79
79
  // Read-only mode - disable PUT/DELETE/PATCH
80
80
  readOnly: false,
81
81
 
82
+ // Live reload - inject script to auto-refresh browser on file changes
83
+ liveReload: false,
84
+
82
85
  // Logging
83
86
  logger: true,
84
87
  quiet: false,
@@ -125,6 +128,7 @@ const envMap = {
125
128
  JSS_DEFAULT_QUOTA: 'defaultQuota',
126
129
  JSS_PUBLIC: 'public',
127
130
  JSS_READ_ONLY: 'readOnly',
131
+ JSS_LIVE_RELOAD: 'liveReload',
128
132
  };
129
133
 
130
134
  /**
@@ -17,6 +17,23 @@ import { emitChange } from '../notifications/events.js';
17
17
  import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
18
18
  import { generateDatabrowserHtml, generateSolidosUiHtml, shouldServeMashlib } from '../mashlib/index.js';
19
19
 
20
+ /**
21
+ * Live reload script - injected into HTML when --live-reload is enabled
22
+ */
23
+ const LIVE_RELOAD_SCRIPT = `<script>(function(){var ws=new WebSocket((location.protocol==='https:'?'wss:':'ws:')+'//' +location.host+'/.notifications');ws.onopen=function(){ws.send('sub '+location.href)};ws.onmessage=function(e){if(e.data.startsWith('pub '))location.reload()};ws.onclose=function(){setTimeout(function(){location.reload()},1000)}})();</script>`;
24
+
25
+ /**
26
+ * Inject live reload script into HTML content
27
+ */
28
+ function injectLiveReload(content) {
29
+ const html = content.toString();
30
+ // Inject before </body> or at end
31
+ if (html.includes('</body>')) {
32
+ return Buffer.from(html.replace('</body>', LIVE_RELOAD_SCRIPT + '</body>'));
33
+ }
34
+ return Buffer.from(html + LIVE_RELOAD_SCRIPT);
35
+ }
36
+
20
37
  /**
21
38
  * Get the storage path and resource URL for a request
22
39
  * In subdomain mode, storage path includes pod name, URL uses subdomain
@@ -198,6 +215,12 @@ export async function handleGet(request, reply) {
198
215
  });
199
216
 
200
217
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
218
+ // Inject live reload script for index.html
219
+ if (request.liveReloadEnabled) {
220
+ reply.header('Cache-Control', 'no-store');
221
+ reply.removeHeader('ETag');
222
+ return reply.send(injectLiveReload(content));
223
+ }
201
224
  return reply.send(content);
202
225
  }
203
226
 
@@ -439,6 +462,13 @@ export async function handleGet(request, reply) {
439
462
  headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);
440
463
 
441
464
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
465
+
466
+ // Inject live reload script into HTML (disable caching since content is modified)
467
+ if (actualContentType === 'text/html' && request.liveReloadEnabled) {
468
+ reply.header('Cache-Control', 'no-store');
469
+ reply.removeHeader('ETag');
470
+ return reply.send(injectLiveReload(content));
471
+ }
442
472
  return reply.send(content);
443
473
  }
444
474
 
@@ -6,6 +6,8 @@
6
6
  */
7
7
 
8
8
  import { EventEmitter } from 'events';
9
+ import { watch } from 'fs';
10
+ import { join, relative } from 'path';
9
11
 
10
12
  // Singleton event emitter for resource changes
11
13
  export const resourceEvents = new EventEmitter();
@@ -20,3 +22,56 @@ resourceEvents.setMaxListeners(1000);
20
22
  export function emitChange(resourceUrl) {
21
23
  resourceEvents.emit('change', resourceUrl);
22
24
  }
25
+
26
+ /**
27
+ * Start watching filesystem for changes and emit notifications
28
+ * @param {string} rootDir - Directory to watch
29
+ * @param {string} baseUrl - Base URL for constructing resource URLs (e.g., http://localhost:3000)
30
+ */
31
+ export function startFileWatcher(rootDir, baseUrl) {
32
+ // Debounce map to avoid duplicate events (editors often save multiple times)
33
+ const debounceMap = new Map();
34
+ const DEBOUNCE_MS = 100;
35
+
36
+ try {
37
+ const watcher = watch(rootDir, { recursive: true }, (eventType, filename) => {
38
+ if (!filename) return;
39
+
40
+ // Skip hidden files and common temp files
41
+ if (filename.startsWith('.') || filename.endsWith('~') || filename.endsWith('.swp')) {
42
+ return;
43
+ }
44
+
45
+ // Debounce: skip if we just emitted for this file
46
+ const now = Date.now();
47
+ const lastEmit = debounceMap.get(filename);
48
+ if (lastEmit && now - lastEmit < DEBOUNCE_MS) {
49
+ return;
50
+ }
51
+ debounceMap.set(filename, now);
52
+
53
+ // Clean up old debounce entries periodically
54
+ if (debounceMap.size > 1000) {
55
+ for (const [key, time] of debounceMap) {
56
+ if (now - time > 5000) debounceMap.delete(key);
57
+ }
58
+ }
59
+
60
+ // Construct resource URL
61
+ const resourcePath = '/' + filename.replace(/\\/g, '/');
62
+ const resourceUrl = baseUrl.replace(/\/$/, '') + resourcePath;
63
+
64
+ emitChange(resourceUrl);
65
+ });
66
+
67
+ // Handle watcher errors gracefully
68
+ watcher.on('error', (err) => {
69
+ console.error('File watcher error:', err.message);
70
+ });
71
+
72
+ return watcher;
73
+ } catch (err) {
74
+ console.error('Failed to start file watcher:', err.message);
75
+ return null;
76
+ }
77
+ }
package/src/server.js CHANGED
@@ -9,6 +9,7 @@ import * as storage from './storage/filesystem.js';
9
9
  import { getCorsHeaders } from './ldp/headers.js';
10
10
  import { authorize, handleUnauthorized } from './auth/middleware.js';
11
11
  import { notificationsPlugin } from './notifications/index.js';
12
+ import { startFileWatcher } from './notifications/events.js';
12
13
  import { idpPlugin } from './idp/index.js';
13
14
  import { isGitRequest, isGitWriteOperation, handleGit } from './handlers/git.js';
14
15
  import { AccessMode } from './wac/parser.js';
@@ -79,6 +80,8 @@ export function createServer(options = {}) {
79
80
  const defaultQuota = options.defaultQuota ?? 50 * 1024 * 1024;
80
81
  // WebID-TLS client certificate authentication is OFF by default
81
82
  const webidTlsEnabled = options.webidTls ?? false;
83
+ // Live reload - injects script to auto-refresh browser on file changes
84
+ const liveReloadEnabled = options.liveReload ?? false;
82
85
 
83
86
  // Set data root via environment variable if provided
84
87
  if (options.root) {
@@ -136,9 +139,10 @@ export function createServer(options = {}) {
136
139
  fastify.decorateRequest('solidosUiEnabled', null);
137
140
  fastify.decorateRequest('defaultQuota', null);
138
141
  fastify.decorateRequest('config', null);
142
+ fastify.decorateRequest('liveReloadEnabled', null);
139
143
  fastify.addHook('onRequest', async (request) => {
140
144
  request.connegEnabled = connegEnabled;
141
- request.notificationsEnabled = notificationsEnabled;
145
+ request.notificationsEnabled = notificationsEnabled || liveReloadEnabled;
142
146
  request.idpEnabled = idpEnabled;
143
147
  request.subdomainsEnabled = subdomainsEnabled;
144
148
  request.baseDomain = baseDomain;
@@ -148,6 +152,7 @@ export function createServer(options = {}) {
148
152
  request.solidosUiEnabled = solidosUiEnabled;
149
153
  request.defaultQuota = defaultQuota;
150
154
  request.config = { public: options.public, readOnly: options.readOnly };
155
+ request.liveReloadEnabled = liveReloadEnabled;
151
156
 
152
157
  // Extract pod name from subdomain if enabled
153
158
  if (subdomainsEnabled && baseDomain) {
@@ -164,8 +169,8 @@ export function createServer(options = {}) {
164
169
  }
165
170
  });
166
171
 
167
- // Register WebSocket notifications plugin if enabled
168
- if (notificationsEnabled) {
172
+ // Register WebSocket notifications plugin if enabled (or live reload needs it)
173
+ if (notificationsEnabled || liveReloadEnabled) {
169
174
  fastify.register(notificationsPlugin);
170
175
  }
171
176
 
@@ -538,6 +543,16 @@ export function createServer(options = {}) {
538
543
  // Note: Quota not initialized for root-level pods (no user directory)
539
544
  }
540
545
 
546
+ // Start file watcher for live reload (watches filesystem for external changes)
547
+ if (liveReloadEnabled) {
548
+ const dataRoot = options.root || process.env.DATA_ROOT || './data';
549
+ const protocol = options.ssl ? 'https' : 'http';
550
+ // Use configured port, or default; actual URL will be localhost
551
+ const port = options.port || 3000;
552
+ const baseUrl = `${protocol}://localhost:${port}`;
553
+ startFileWatcher(dataRoot, baseUrl);
554
+ }
555
+
541
556
  return fastify;
542
557
  }
543
558