javascript-solid-server 0.0.15 → 0.0.17

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.
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Mashlib Data Browser Integration
3
+ *
4
+ * Generates HTML wrapper that loads SolidOS Mashlib from CDN.
5
+ * When a browser requests an RDF resource with Accept: text/html,
6
+ * we return this wrapper which then fetches and renders the data.
7
+ */
8
+
9
+ const CDN_BASE = 'https://unpkg.com/mashlib';
10
+
11
+ /**
12
+ * Generate Mashlib databrowser HTML
13
+ * @param {string} resourceUrl - The URL of the resource being viewed
14
+ * @param {string} version - Mashlib version (default: '2.0.0')
15
+ * @returns {string} HTML content
16
+ */
17
+ export function generateDatabrowserHtml(resourceUrl, version = '2.0.0') {
18
+ const cdnUrl = `${CDN_BASE}@${version}/dist`;
19
+
20
+ return `<!doctype html>
21
+ <html>
22
+ <head>
23
+ <meta charset="utf-8"/>
24
+ <meta name="viewport" content="width=device-width, initial-scale=1">
25
+ <title>SolidOS - ${escapeHtml(resourceUrl)}</title>
26
+ <script defer src="${cdnUrl}/mashlib.min.js"></script>
27
+ <link href="${cdnUrl}/mash.css" rel="stylesheet">
28
+ <script>
29
+ document.addEventListener('DOMContentLoaded', function() {
30
+ // runDataBrowser uses window.location to determine what to fetch
31
+ panes.runDataBrowser();
32
+ });
33
+ </script>
34
+ <style>
35
+ /* Loading indicator */
36
+ body:not(.loaded) #PageBody::before {
37
+ content: 'Loading SolidOS...';
38
+ display: block;
39
+ padding: 2em;
40
+ text-align: center;
41
+ color: #666;
42
+ }
43
+ </style>
44
+ </head>
45
+ <body id="PageBody">
46
+ <header id="PageHeader"></header>
47
+ <div class="TabulatorOutline" id="DummyUUID" role="main">
48
+ <table id="outline"></table>
49
+ <div id="GlobalDashboard"></div>
50
+ </div>
51
+ <footer id="PageFooter"></footer>
52
+ </body>
53
+ </html>`;
54
+ }
55
+
56
+ /**
57
+ * Check if request wants HTML and mashlib should handle it
58
+ * @param {object} request - Fastify request
59
+ * @param {boolean} mashlibEnabled - Whether mashlib is enabled
60
+ * @param {string} contentType - Content type of the resource
61
+ * @returns {boolean}
62
+ */
63
+ export function shouldServeMashlib(request, mashlibEnabled, contentType) {
64
+ if (!mashlibEnabled) {
65
+ return false;
66
+ }
67
+
68
+ const accept = request.headers.accept || '';
69
+
70
+ // Must explicitly accept HTML
71
+ if (!accept.includes('text/html')) {
72
+ return false;
73
+ }
74
+
75
+ // Only serve mashlib for RDF content types
76
+ const rdfTypes = [
77
+ 'text/turtle',
78
+ 'application/ld+json',
79
+ 'application/json',
80
+ 'text/n3',
81
+ 'application/n-triples',
82
+ 'application/rdf+xml'
83
+ ];
84
+
85
+ const baseType = contentType.split(';')[0].trim().toLowerCase();
86
+ return rdfTypes.includes(baseType);
87
+ }
88
+
89
+ /**
90
+ * Escape HTML special characters
91
+ */
92
+ function escapeHtml(str) {
93
+ return str
94
+ .replace(/&/g, '&amp;')
95
+ .replace(/</g, '&lt;')
96
+ .replace(/>/g, '&gt;')
97
+ .replace(/"/g, '&quot;');
98
+ }
package/src/server.js CHANGED
@@ -16,6 +16,8 @@ import { idpPlugin } from './idp/index.js';
16
16
  * @param {string} options.idpIssuer - IdP issuer URL (default: server URL)
17
17
  * @param {object} options.ssl - SSL configuration { key, cert } (default null)
18
18
  * @param {string} options.root - Data directory path (default from env or ./data)
19
+ * @param {boolean} options.subdomains - Enable subdomain-based pods for XSS protection (default false)
20
+ * @param {string} options.baseDomain - Base domain for subdomain pods (e.g., "example.com")
19
21
  */
20
22
  export function createServer(options = {}) {
21
23
  // Content negotiation is OFF by default - we're a JSON-LD native server
@@ -25,6 +27,12 @@ export function createServer(options = {}) {
25
27
  // Identity Provider is OFF by default
26
28
  const idpEnabled = options.idp ?? false;
27
29
  const idpIssuer = options.idpIssuer;
30
+ // Subdomain mode is OFF by default - use path-based pods
31
+ const subdomainsEnabled = options.subdomains ?? false;
32
+ const baseDomain = options.baseDomain || null;
33
+ // Mashlib data browser is OFF by default
34
+ const mashlibEnabled = options.mashlib ?? false;
35
+ const mashlibVersion = options.mashlibVersion ?? '2.0.0';
28
36
 
29
37
  // Set data root via environment variable if provided
30
38
  if (options.root) {
@@ -58,10 +66,33 @@ export function createServer(options = {}) {
58
66
  fastify.decorateRequest('connegEnabled', null);
59
67
  fastify.decorateRequest('notificationsEnabled', null);
60
68
  fastify.decorateRequest('idpEnabled', null);
69
+ fastify.decorateRequest('subdomainsEnabled', null);
70
+ fastify.decorateRequest('baseDomain', null);
71
+ fastify.decorateRequest('podName', null);
72
+ fastify.decorateRequest('mashlibEnabled', null);
73
+ fastify.decorateRequest('mashlibVersion', null);
61
74
  fastify.addHook('onRequest', async (request) => {
62
75
  request.connegEnabled = connegEnabled;
63
76
  request.notificationsEnabled = notificationsEnabled;
64
77
  request.idpEnabled = idpEnabled;
78
+ request.subdomainsEnabled = subdomainsEnabled;
79
+ request.baseDomain = baseDomain;
80
+ request.mashlibEnabled = mashlibEnabled;
81
+ request.mashlibVersion = mashlibVersion;
82
+
83
+ // Extract pod name from subdomain if enabled
84
+ if (subdomainsEnabled && baseDomain) {
85
+ const host = request.hostname;
86
+ // Check if host is a subdomain of baseDomain
87
+ if (host !== baseDomain && host.endsWith('.' + baseDomain)) {
88
+ // Extract subdomain (e.g., "alice.example.com" -> "alice")
89
+ const subdomain = host.slice(0, -(baseDomain.length + 1));
90
+ // Only single-level subdomains (no dots)
91
+ if (!subdomain.includes('.')) {
92
+ request.podName = subdomain;
93
+ }
94
+ }
95
+ }
65
96
  });
66
97
 
67
98
  // Register WebSocket notifications plugin if enabled
package/src/utils/url.js CHANGED
@@ -19,6 +19,58 @@ export function urlToPath(urlPath) {
19
19
  return path.join(DATA_ROOT, normalized);
20
20
  }
21
21
 
22
+ /**
23
+ * Convert URL path to filesystem path in subdomain mode
24
+ * In subdomain mode, the pod is determined by the hostname, not the path
25
+ * @param {string} urlPath - The URL path (e.g., /public/file.txt)
26
+ * @param {string} podName - The pod name from subdomain (e.g., "alice")
27
+ * @returns {string} - Filesystem path (e.g., DATA_ROOT/alice/public/file.txt)
28
+ */
29
+ export function urlToPathWithPod(urlPath, podName) {
30
+ // Normalize: remove leading slash, decode URI
31
+ let normalized = urlPath.startsWith('/') ? urlPath.slice(1) : urlPath;
32
+ normalized = decodeURIComponent(normalized);
33
+
34
+ // Security: prevent path traversal
35
+ normalized = normalized.replace(/\.\./g, '');
36
+
37
+ // Prepend pod name to path
38
+ return path.join(DATA_ROOT, podName, normalized);
39
+ }
40
+
41
+ /**
42
+ * Get the effective path for a request (subdomain-aware)
43
+ * @param {object} request - Fastify request object
44
+ * @returns {string} - Filesystem path
45
+ */
46
+ export function getPathFromRequest(request) {
47
+ const urlPath = request.url.split('?')[0];
48
+
49
+ // In subdomain mode with a recognized pod subdomain
50
+ if (request.subdomainsEnabled && request.podName) {
51
+ return urlToPathWithPod(urlPath, request.podName);
52
+ }
53
+
54
+ // Path-based mode (default)
55
+ return urlToPath(urlPath);
56
+ }
57
+
58
+ /**
59
+ * Get the effective URL path for a request (with pod prefix in subdomain mode)
60
+ * @param {object} request - Fastify request object
61
+ * @returns {string} - URL path with pod prefix if needed
62
+ */
63
+ export function getEffectiveUrlPath(request) {
64
+ const urlPath = request.url.split('?')[0];
65
+
66
+ // In subdomain mode with a recognized pod subdomain, prepend pod name
67
+ if (request.subdomainsEnabled && request.podName) {
68
+ return '/' + request.podName + urlPath;
69
+ }
70
+
71
+ return urlPath;
72
+ }
73
+
22
74
  /**
23
75
  * Check if URL path represents a container (ends with /)
24
76
  * @param {string} urlPath
@@ -1,3 +1,3 @@
1
1
  {
2
- "credtest@example.com": "292738d6-3363-4f40-9a6b-884bfd17830a"
2
+ "credtest@example.com": "ea61c611-2dda-41b8-8787-c6a22c5f33cc"
3
3
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "http://localhost:3101/credtest/#me": "292738d6-3363-4f40-9a6b-884bfd17830a"
2
+ "http://localhost:3101/credtest/#me": "ea61c611-2dda-41b8-8787-c6a22c5f33cc"
3
3
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "ea61c611-2dda-41b8-8787-c6a22c5f33cc",
3
+ "email": "credtest@example.com",
4
+ "passwordHash": "$2b$10$EVWVKsbQ4A6DdswLEK/0ZOclDrBHBo/9GbWeP1s5uvxy4jWjTnY0m",
5
+ "webId": "http://localhost:3101/credtest/#me",
6
+ "podName": "credtest",
7
+ "createdAt": "2025-12-27T13:12:43.961Z",
8
+ "lastLogin": "2025-12-27T13:12:44.207Z"
9
+ }
@@ -3,20 +3,20 @@
3
3
  "keys": [
4
4
  {
5
5
  "kty": "EC",
6
- "x": "1gMgS0xMseqjfq5fA_aYkkq7CMqr6OOQ5ZS4D3MqG6g",
7
- "y": "rtkAdN0tManytaX1QDFRBRE6GXoOlxqj_d3Yt5mpViA",
6
+ "x": "hn3QE_mBUEFiANsvmP5MpvQWUCvTxfhBUYsJYbtCG_g",
7
+ "y": "0F9jdfVkLit5_xYsHsCstuMsRIa5R6GdciY8ZF1zcgs",
8
8
  "crv": "P-256",
9
- "d": "GqEv1nO1PRgrKE7n18iDNow-haou-7B6_dlMqo-ftLQ",
10
- "kid": "102e3c82-7dda-4a6f-a296-d47d9b2e0b59",
9
+ "d": "ippBE99bkrgDX1EQa3awsciu035kbhukikfYwZYh1Jc",
10
+ "kid": "267aed32-f9f4-4c72-9925-3e025b775bca",
11
11
  "use": "sig",
12
12
  "alg": "ES256",
13
- "iat": 1766838193
13
+ "iat": 1766841163
14
14
  }
15
15
  ]
16
16
  },
17
17
  "cookieKeys": [
18
- "PQdsUKa6PcaWNBEUm9G3IZumxoXHnd93rUcyf9VYc0w",
19
- "T4X4hUYp3dE9LakGJX9U5fRux5pyldcrpg_t8AA4FYg"
18
+ "Zf-WDQBZkGGOED37Z3NzHsO9Jy1L7AEgjIDfHbFbyp4",
19
+ "8MTMciOb4PQqVcG2SrwmPSAYFrhW8eS46WB7gDqK0CQ"
20
20
  ],
21
- "createdAt": "2025-12-27T12:23:13.203Z"
21
+ "createdAt": "2025-12-27T13:12:43.907Z"
22
22
  }
@@ -1,9 +0,0 @@
1
- {
2
- "id": "292738d6-3363-4f40-9a6b-884bfd17830a",
3
- "email": "credtest@example.com",
4
- "passwordHash": "$2b$10$tvcMaMvecS7noqe/T/A5Q.VojfNu1FEPAzWhl/.3v7WXrVIH38iYC",
5
- "webId": "http://localhost:3101/credtest/#me",
6
- "podName": "credtest",
7
- "createdAt": "2025-12-27T12:23:13.338Z",
8
- "lastLogin": "2025-12-27T12:23:13.871Z"
9
- }