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.
- package/.claude/settings.local.json +5 -1
- package/LICENSE +661 -0
- package/README.md +88 -4
- package/bin/jss.js +12 -0
- package/package.json +2 -2
- package/src/auth/middleware.js +11 -5
- package/src/config.js +14 -0
- package/src/handlers/container.js +41 -13
- package/src/handlers/resource.js +58 -33
- package/src/mashlib/index.js +98 -0
- package/src/server.js +31 -0
- package/src/utils/url.js +52 -0
- package/test-data-idp-accounts/.idp/accounts/_email_index.json +1 -1
- package/test-data-idp-accounts/.idp/accounts/_webid_index.json +1 -1
- package/test-data-idp-accounts/.idp/accounts/ea61c611-2dda-41b8-8787-c6a22c5f33cc.json +9 -0
- package/test-data-idp-accounts/.idp/keys/jwks.json +8 -8
- package/test-data-idp-accounts/.idp/accounts/292738d6-3363-4f40-9a6b-884bfd17830a.json +0 -9
|
@@ -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, '&')
|
|
95
|
+
.replace(/</g, '<')
|
|
96
|
+
.replace(/>/g, '>')
|
|
97
|
+
.replace(/"/g, '"');
|
|
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
|
|
@@ -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": "
|
|
7
|
-
"y": "
|
|
6
|
+
"x": "hn3QE_mBUEFiANsvmP5MpvQWUCvTxfhBUYsJYbtCG_g",
|
|
7
|
+
"y": "0F9jdfVkLit5_xYsHsCstuMsRIa5R6GdciY8ZF1zcgs",
|
|
8
8
|
"crv": "P-256",
|
|
9
|
-
"d": "
|
|
10
|
-
"kid": "
|
|
9
|
+
"d": "ippBE99bkrgDX1EQa3awsciu035kbhukikfYwZYh1Jc",
|
|
10
|
+
"kid": "267aed32-f9f4-4c72-9925-3e025b775bca",
|
|
11
11
|
"use": "sig",
|
|
12
12
|
"alg": "ES256",
|
|
13
|
-
"iat":
|
|
13
|
+
"iat": 1766841163
|
|
14
14
|
}
|
|
15
15
|
]
|
|
16
16
|
},
|
|
17
17
|
"cookieKeys": [
|
|
18
|
-
"
|
|
19
|
-
"
|
|
18
|
+
"Zf-WDQBZkGGOED37Z3NzHsO9Jy1L7AEgjIDfHbFbyp4",
|
|
19
|
+
"8MTMciOb4PQqVcG2SrwmPSAYFrhW8eS46WB7gDqK0CQ"
|
|
20
20
|
],
|
|
21
|
-
"createdAt": "2025-12-
|
|
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
|
-
}
|