javascript-solid-server 0.0.92 → 0.0.94

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
@@ -130,6 +130,7 @@ jss --help # Show help
130
130
  | `--base-domain <domain>` | Base domain for subdomains | - |
131
131
  | `--mashlib` | Enable Mashlib (local mode) | false |
132
132
  | `--mashlib-cdn` | Enable Mashlib (CDN mode) | false |
133
+ | `--mashlib-module <url>` | Enable ES module data browser from a URL | - |
133
134
  | `--mashlib-version <ver>` | Mashlib CDN version | 2.0.0 |
134
135
  | `--solidos-ui` | Enable modern SolidOS UI (requires --mashlib) | false |
135
136
  | `--git` | Enable Git HTTP backend | false |
@@ -161,6 +162,7 @@ export JSS_CONNEG=true
161
162
  export JSS_SUBDOMAINS=true
162
163
  export JSS_BASE_DOMAIN=example.com
163
164
  export JSS_MASHLIB=true
165
+ export JSS_MASHLIB_MODULE=https://example.com/mashlib.js
164
166
  export JSS_NOSTR=true
165
167
  export JSS_INVITE_ONLY=true
166
168
  export JSS_WEBID_TLS=true
@@ -367,6 +369,12 @@ cd src/mashlib-local
367
369
  npm install && npm run build
368
370
  ```
369
371
 
372
+ **ES Module Mode** (for custom or next-gen mashlib builds):
373
+ ```bash
374
+ jss start --mashlib-module https://example.com/mashlib.js
375
+ ```
376
+ Loads an ES module-based data browser from any URL. Uses `<script type="module">` and `<div id="mashlib">` (self-initializing). CSS is auto-derived by replacing `.js` with `.css`. Content negotiation is auto-enabled.
377
+
370
378
  **How it works:**
371
379
  1. Browser requests `/alice/public/data.ttl` with `Accept: text/html`
372
380
  2. Server returns Mashlib HTML wrapper
package/bin/jss.js CHANGED
@@ -55,6 +55,7 @@ program
55
55
  .option('--base-domain <domain>', 'Base domain for subdomain pods (e.g., "example.com")')
56
56
  .option('--mashlib', 'Enable Mashlib data browser (local mode, requires mashlib in node_modules)')
57
57
  .option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
58
+ .option('--mashlib-module <url>', 'Enable ES module data browser from a URL')
58
59
  .option('--no-mashlib', 'Disable Mashlib data browser')
59
60
  .option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
60
61
  .option('--solidos-ui', 'Enable modern Nextcloud-style UI (requires --mashlib)')
@@ -123,6 +124,7 @@ program
123
124
  mashlib: config.mashlib || config.mashlibCdn,
124
125
  mashlibCdn: config.mashlibCdn,
125
126
  mashlibVersion: config.mashlibVersion,
127
+ mashlibModule: config.mashlibModule,
126
128
  solidosUi: config.solidosUi,
127
129
  git: config.git,
128
130
  nostr: config.nostr,
@@ -158,6 +160,7 @@ program
158
160
  } else if (config.mashlib) {
159
161
  console.log(` Mashlib: local (data browser enabled)`);
160
162
  }
163
+ if (config.mashlibModule) console.log(` Mashlib module: ${config.mashlibModule}`);
161
164
  if (config.solidosUi) console.log(' SolidOS UI: enabled (modern interface)');
162
165
  if (config.git) console.log(' Git: enabled (clone/push support)');
163
166
  if (config.nostr) console.log(` Nostr: enabled (${config.nostrPath})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.92",
3
+ "version": "0.0.94",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -9,7 +9,32 @@ import { checkAccess, getRequiredMode } from '../wac/checker.js';
9
9
  import { AccessMode } from '../wac/parser.js';
10
10
  import * as storage from '../storage/filesystem.js';
11
11
  import { getEffectiveUrlPath } from '../utils/url.js';
12
- import { generateDatabrowserHtml, generateSolidosUiHtml } from '../mashlib/index.js';
12
+ import { generateDatabrowserHtml, generateModuleDatabrowserHtml, generateSolidosUiHtml } from '../mashlib/index.js';
13
+
14
+ /**
15
+ * Build a resource URL for WAC checking, normalizing path-based pod access
16
+ * to subdomain form so URLs match ACL entries.
17
+ *
18
+ * In subdomain mode, ACLs reference subdomain URLs (e.g. https://alice.example.com/public/).
19
+ * Path-based access on the main domain (e.g. https://example.com/alice/public/) must be
20
+ * normalized to match.
21
+ *
22
+ * @param {object} request - Fastify request
23
+ * @param {string} urlPath - URL path (e.g. /alice/public/file.ttl)
24
+ * @returns {string} Normalized resource URL
25
+ */
26
+ function buildResourceUrl(request, urlPath) {
27
+ if (request.subdomainsEnabled && request.baseDomain &&
28
+ request.hostname === request.baseDomain && !request.podName) {
29
+ const pathMatch = urlPath.match(/^\/([^/]+)(\/.*)?$/);
30
+ if (pathMatch && !pathMatch[1].startsWith('.')) {
31
+ const podName = pathMatch[1];
32
+ const remainder = pathMatch[2] || '/';
33
+ return `${request.protocol}://${podName}.${request.baseDomain}${remainder}`;
34
+ }
35
+ }
36
+ return `${request.protocol}://${request.hostname}${urlPath}`;
37
+ }
13
38
 
14
39
  /**
15
40
  * Check if request is authorized
@@ -55,8 +80,8 @@ export async function authorize(request, reply, options = {}) {
55
80
  const resourceExists = stats !== null;
56
81
  const isContainer = stats?.isDirectory || urlPath.endsWith('/');
57
82
 
58
- // Build resource URL (uses actual request hostname which may be subdomain)
59
- const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
83
+ // Build resource URL, normalizing path-based pod access to subdomain form for WAC
84
+ const resourceUrl = buildResourceUrl(request, urlPath);
60
85
 
61
86
  // Get required access mode - use override if provided, otherwise derive from method
62
87
  const requiredMode = options.requiredMode || getRequiredMode(method);
@@ -70,9 +95,9 @@ export async function authorize(request, reply, options = {}) {
70
95
  // Check write permission on parent container
71
96
  const parentPath = getParentPath(storagePath);
72
97
  checkPath = parentPath;
73
- // For URL, also need to get parent
98
+ // For URL, also need to get parent (normalized for subdomain WAC matching)
74
99
  const parentUrlPath = getParentPath(urlPath);
75
- checkUrl = `${request.protocol}://${request.hostname}${parentUrlPath}`;
100
+ checkUrl = buildResourceUrl(request, parentUrlPath);
76
101
  checkIsContainer = true;
77
102
  }
78
103
 
@@ -123,10 +148,12 @@ export function handleUnauthorized(request, reply, isAuthenticated, wacAllow, au
123
148
  // If mashlib is enabled, serve mashlib instead of static error page
124
149
  // Mashlib has built-in login functionality via panes.runDataBrowser()
125
150
  if (request.mashlibEnabled) {
126
- // Use SolidOS UI if enabled, otherwise fallback to classic mashlib
151
+ // Use SolidOS UI if enabled, ES module if configured, otherwise classic mashlib
127
152
  const html = request.solidosUiEnabled
128
153
  ? generateSolidosUiHtml()
129
- : generateDatabrowserHtml(request.url, request.mashlibCdn ? request.mashlibVersion : null);
154
+ : request.mashlibModule
155
+ ? generateModuleDatabrowserHtml(request.mashlibModule)
156
+ : generateDatabrowserHtml(request.url, request.mashlibCdn ? request.mashlibVersion : null);
130
157
  return reply.code(statusCode).type('text/html').send(html);
131
158
  }
132
159
  return reply.code(statusCode).type('text/html').send(getErrorPage(statusCode, isAuthenticated, request));
@@ -379,7 +406,7 @@ async function authorizeAclAccess(request, urlPath, method, webId, authError) {
379
406
  // /foo/bar.acl protects /foo/bar (resource)
380
407
  const protectedPath = urlPath.replace(/\.acl$/, '');
381
408
  const isProtectedContainer = protectedPath.endsWith('/');
382
- const protectedUrl = `${request.protocol}://${request.hostname}${protectedPath}`;
409
+ const protectedUrl = buildResourceUrl(request, protectedPath);
383
410
 
384
411
  // Get storage path for the protected resource
385
412
  const storagePath = getEffectiveUrlPath(request).replace(/\.acl$/, '');
package/src/config.js CHANGED
@@ -41,6 +41,7 @@ export const defaults = {
41
41
  mashlib: false,
42
42
  mashlibCdn: false,
43
43
  mashlibVersion: '2.0.0',
44
+ mashlibModule: false,
44
45
 
45
46
  // SolidOS UI (modern Nextcloud-style interface)
46
47
  solidosUi: false,
@@ -113,6 +114,7 @@ const envMap = {
113
114
  JSS_MASHLIB: 'mashlib',
114
115
  JSS_MASHLIB_CDN: 'mashlibCdn',
115
116
  JSS_MASHLIB_VERSION: 'mashlibVersion',
117
+ JSS_MASHLIB_MODULE: 'mashlibModule',
116
118
  JSS_SOLIDOS_UI: 'solidosUi',
117
119
  JSS_GIT: 'git',
118
120
  JSS_NOSTR: 'nostr',
@@ -238,7 +240,7 @@ export async function loadConfig(cliOptions = {}, configFile = null) {
238
240
  }
239
241
 
240
242
  // Mashlib requires content negotiation for Turtle support
241
- if (config.mashlib || config.mashlibCdn) {
243
+ if (config.mashlib || config.mashlibCdn || config.mashlibModule) {
242
244
  config.conneg = true;
243
245
  }
244
246
 
@@ -293,7 +295,7 @@ export function printConfig(config) {
293
295
  console.log(` Notifications: ${config.notifications}`);
294
296
  console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
295
297
  console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
296
- console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
298
+ console.log(` Mashlib: ${config.mashlibModule ? `module (${config.mashlibModule})` : config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
297
299
  console.log(` SolidOS UI: ${config.solidosUi ? 'enabled' : 'disabled'}`);
298
300
  console.log('─'.repeat(40));
299
301
  }
@@ -15,7 +15,7 @@ import {
15
15
  } from '../rdf/conneg.js';
16
16
  import { emitChange } from '../notifications/events.js';
17
17
  import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
18
- import { generateDatabrowserHtml, generateSolidosUiHtml, shouldServeMashlib } from '../mashlib/index.js';
18
+ import { generateDatabrowserHtml, generateModuleDatabrowserHtml, generateSolidosUiHtml, shouldServeMashlib } from '../mashlib/index.js';
19
19
 
20
20
  /**
21
21
  * Live reload script - injected into HTML when --live-reload is enabled
@@ -230,10 +230,12 @@ export async function handleGet(request, reply) {
230
230
 
231
231
  // Check if we should serve Mashlib data browser for containers
232
232
  if (shouldServeMashlib(request, request.mashlibEnabled, 'application/ld+json')) {
233
- // Use SolidOS UI if enabled, otherwise fallback to classic mashlib
233
+ // Use SolidOS UI if enabled, ES module if configured, otherwise classic mashlib
234
234
  const html = request.solidosUiEnabled
235
235
  ? generateSolidosUiHtml()
236
- : generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
236
+ : request.mashlibModule
237
+ ? generateModuleDatabrowserHtml(request.mashlibModule)
238
+ : generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
237
239
  const headers = getAllHeaders({
238
240
  isContainer: true,
239
241
  etag: stats.etag,
@@ -307,10 +309,12 @@ export async function handleGet(request, reply) {
307
309
  // Check if we should serve Mashlib data browser
308
310
  // Only for RDF resources when Accept: text/html is requested
309
311
  if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
310
- // Use SolidOS UI if enabled, otherwise fallback to classic mashlib
312
+ // Use SolidOS UI if enabled, ES module if configured, otherwise classic mashlib
311
313
  const html = request.solidosUiEnabled
312
314
  ? generateSolidosUiHtml()
313
- : generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
315
+ : request.mashlibModule
316
+ ? generateModuleDatabrowserHtml(request.mashlibModule)
317
+ : generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
314
318
  const headers = getAllHeaders({
315
319
  isContainer: false,
316
320
  etag: stats.etag,
@@ -140,9 +140,9 @@ export async function handleLogin(request, reply, provider) {
140
140
  // Show passkey registration prompt before completing login
141
141
  // Store the pending login in the interaction
142
142
  interaction.result = {
143
+ passkeyPromptPending: true,
143
144
  login: { accountId: account.id, remember: true }
144
145
  };
145
- interaction.passkeyPromptPending = true;
146
146
  await interaction.save(interaction.exp - Math.floor(Date.now() / 1000));
147
147
  return reply.type('text/html').send(passkeyPromptPage(uid, account.id));
148
148
  }
@@ -473,7 +473,7 @@ export async function handlePasskeyComplete(request, reply, provider) {
473
473
 
474
474
  // If this is a post-login passkey registration flow, validate accountId matches
475
475
  // the already-authenticated user to prevent account takeover
476
- if (interaction.passkeyPromptPending && interaction.result?.login?.accountId) {
476
+ if (interaction.result?.passkeyPromptPending && interaction.result?.login?.accountId) {
477
477
  if (interaction.result.login.accountId !== accountId) {
478
478
  request.log.warn({ expected: interaction.result.login.accountId, provided: accountId }, 'AccountId mismatch in passkey complete');
479
479
  return reply.code(403).type('text/html').send(errorPage('Access denied', 'Account mismatch.'));
@@ -520,7 +520,7 @@ export async function handlePasskeySkip(request, reply, provider) {
520
520
  }
521
521
 
522
522
  // Validate the interaction is in the passkey prompt state
523
- if (!interaction.passkeyPromptPending) {
523
+ if (!interaction.result?.passkeyPromptPending) {
524
524
  return reply.code(400).type('text/html').send(errorPage('Invalid state', 'Not in passkey prompt flow.'));
525
525
  }
526
526
 
@@ -40,6 +40,23 @@ export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
40
40
  })</script><script defer="defer" src="/mashlib.min.js"></script><link href="/mash.css" rel="stylesheet"></head><body id="PageBody"><header id="PageHeader"></header><div class="TabulatorOutline" id="DummyUUID" role="main"><table id="outline"></table><div id="GlobalDashboard"></div></div><footer id="PageFooter"></footer></body></html>`;
41
41
  }
42
42
 
43
+ /**
44
+ * Generate ES module-based databrowser HTML
45
+ *
46
+ * @param {string} moduleUrl - URL to the ES module entry point
47
+ * @returns {string} HTML content
48
+ */
49
+ export function generateModuleDatabrowserHtml(moduleUrl) {
50
+ const cssUrl = moduleUrl.replace(/\.js$/, '.css');
51
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"/>
52
+ <meta name="viewport" content="width=device-width, initial-scale=1">
53
+ <title>Solid Data Browser</title>
54
+ <link rel="stylesheet" href="${cssUrl}"></head>
55
+ <body><div id="mashlib"></div>
56
+ <script type="module" src="${moduleUrl}"></script>
57
+ </body></html>`;
58
+ }
59
+
43
60
  /**
44
61
  * Check if request wants HTML and mashlib should handle it
45
62
  * @param {object} request - Fastify request
package/src/server.js CHANGED
@@ -54,7 +54,9 @@ export function createServer(options = {}) {
54
54
  const baseDomain = options.baseDomain || null;
55
55
  // Mashlib data browser is OFF by default
56
56
  // mashlibCdn: if true, load from CDN; if false, serve locally
57
- const mashlibEnabled = options.mashlib ?? false;
57
+ // mashlibModule: URL to ES module entry point (alternative to classic mashlib)
58
+ const mashlibModule = options.mashlibModule ?? false;
59
+ const mashlibEnabled = options.mashlib || !!mashlibModule;
58
60
  const mashlibCdn = options.mashlibCdn ?? false;
59
61
  const mashlibVersion = options.mashlibVersion ?? '2.0.0';
60
62
  // SolidOS UI (modern Nextcloud-style interface) - requires mashlib
@@ -147,6 +149,7 @@ export function createServer(options = {}) {
147
149
  fastify.decorateRequest('mashlibEnabled', null);
148
150
  fastify.decorateRequest('mashlibCdn', null);
149
151
  fastify.decorateRequest('mashlibVersion', null);
152
+ fastify.decorateRequest('mashlibModule', null);
150
153
  fastify.decorateRequest('solidosUiEnabled', null);
151
154
  fastify.decorateRequest('defaultQuota', null);
152
155
  fastify.decorateRequest('config', null);
@@ -160,6 +163,7 @@ export function createServer(options = {}) {
160
163
  request.mashlibEnabled = mashlibEnabled;
161
164
  request.mashlibCdn = mashlibCdn;
162
165
  request.mashlibVersion = mashlibVersion;
166
+ request.mashlibModule = mashlibModule;
163
167
  request.solidosUiEnabled = solidosUiEnabled;
164
168
  request.defaultQuota = defaultQuota;
165
169
  request.config = { public: options.public, readOnly: options.readOnly };
package/test/auth.test.js CHANGED
@@ -149,6 +149,69 @@ describe('Authentication', () => {
149
149
  const res = await request('/inboxread/inbox/');
150
150
  assertStatus(res, 401);
151
151
  });
152
+
153
+ it('should allow any authenticated user with acl:AuthenticatedAgent', async () => {
154
+ await createTestPod('authuser1');
155
+ await createTestPod('authuser2');
156
+
157
+ // Create a test resource with acl:AuthenticatedAgent ACL
158
+ const baseUrl = getBaseUrl();
159
+
160
+ // First, create a resource (this will create parent containers)
161
+ await request('/authuser1/authenticated-only/test.txt', {
162
+ method: 'PUT',
163
+ headers: { 'Content-Type': 'text/plain' },
164
+ body: 'authenticated content',
165
+ auth: 'authuser1'
166
+ });
167
+
168
+ // Now create a custom ACL for the container with acl:AuthenticatedAgent
169
+ // Include owner with Control so they can manage the ACL
170
+ const acl = {
171
+ '@context': { 'acl': 'http://www.w3.org/ns/auth/acl#' },
172
+ '@graph': [
173
+ {
174
+ '@id': '#owner',
175
+ '@type': 'acl:Authorization',
176
+ 'acl:agent': { '@id': `${baseUrl}/authuser1/profile/card#me` },
177
+ 'acl:accessTo': { '@id': `${baseUrl}/authuser1/authenticated-only/` },
178
+ 'acl:default': { '@id': `${baseUrl}/authuser1/authenticated-only/` },
179
+ 'acl:mode': [
180
+ { '@id': 'acl:Read' },
181
+ { '@id': 'acl:Write' },
182
+ { '@id': 'acl:Control' }
183
+ ]
184
+ },
185
+ {
186
+ '@id': '#authenticated',
187
+ '@type': 'acl:Authorization',
188
+ 'acl:agentClass': { '@id': 'acl:AuthenticatedAgent' },
189
+ 'acl:accessTo': { '@id': `${baseUrl}/authuser1/authenticated-only/` },
190
+ 'acl:default': { '@id': `${baseUrl}/authuser1/authenticated-only/` },
191
+ 'acl:mode': [{ '@id': 'acl:Read' }]
192
+ }
193
+ ]
194
+ };
195
+
196
+ await request('/authuser1/authenticated-only/.acl', {
197
+ method: 'PUT',
198
+ headers: { 'Content-Type': 'application/json' },
199
+ body: JSON.stringify(acl),
200
+ auth: 'authuser1'
201
+ });
202
+
203
+ // Test 1: Anonymous access should be denied
204
+ const res1 = await request('/authuser1/authenticated-only/test.txt');
205
+ assertStatus(res1, 401);
206
+
207
+ // Test 2: Owner should have access
208
+ const res2 = await request('/authuser1/authenticated-only/test.txt', { auth: 'authuser1' });
209
+ assertStatus(res2, 200);
210
+
211
+ // Test 3: Different authenticated user should also have access (key test!)
212
+ const res3 = await request('/authuser1/authenticated-only/test.txt', { auth: 'authuser2' });
213
+ assertStatus(res3, 200);
214
+ });
152
215
  });
153
216
 
154
217
  describe('WAC-Allow Header', () => {