javascript-solid-server 0.0.75 → 0.0.76
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 +23 -1
- package/README.md +13 -0
- package/bin/jss.js +3 -0
- package/package.json +1 -1
- package/src/auth/middleware.js +6 -3
- package/src/config.js +5 -0
- package/src/handlers/resource.js +104 -6
- package/src/ldp/headers.js +3 -2
- package/src/mashlib/index.js +107 -0
- package/src/server.js +37 -1
- package/src/storage/filesystem.js +22 -0
- package/test/range.test.js +145 -0
|
@@ -234,7 +234,29 @@
|
|
|
234
234
|
"Bash(timeout 90 bash -c:*)",
|
|
235
235
|
"Bash(git commit -m \"$\\(cat <<''EOF''\nsecurity: add ACL check on WebSocket subscription requests\n\nCheck WAC read permission before allowing subscription to prevent\ninformation leakage via notifications. Unauthorized subscriptions\nnow receive ''err <url> forbidden'' response.\n\nSecurity improvements:\n- Check ACL read access before allowing subscription\n- Validate URLs are on this server \\(prevents SSRF-like probing\\)\n- Add subscription limit and URL length validation\n\nFixes #62\nEOF\n\\)\")",
|
|
236
236
|
"Bash(gh repo fork:*)",
|
|
237
|
-
"Bash(timeout 180 npm test:*)"
|
|
237
|
+
"Bash(timeout 180 npm test:*)",
|
|
238
|
+
"Bash(git show:*)",
|
|
239
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline -30 -- \"test/integration/acl-tls-test.mjs\" \"test-esm/integration/acl-tls-test.js\")",
|
|
240
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --follow -30 -- \"test/integration/acl-tls-test.mjs\")",
|
|
241
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show 778095ad --stat)",
|
|
242
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show 778095ad)",
|
|
243
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show b183c7a0)",
|
|
244
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --before=\"2019-10-29\" --after=\"2019-10-01\" -20)",
|
|
245
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server show 1a92a912 --stat)",
|
|
246
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --all --grep=jaxoncreed)",
|
|
247
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --author=\"jaxoncreed\" -30)",
|
|
248
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --author=\"[Dd]mitri\" -20)",
|
|
249
|
+
"Bash(git -C /home/melvin/remote/github.com/nodeSolidServer/node-solid-server log --oneline --all --grep=\"oidc\" -20)",
|
|
250
|
+
"Bash(npm install)",
|
|
251
|
+
"Bash(timeout 60 npx mocha:*)",
|
|
252
|
+
"Bash(timeout 120 npx mocha:*)",
|
|
253
|
+
"Bash(timeout 30 npx mocha:*)",
|
|
254
|
+
"Bash(openssl x509:*)",
|
|
255
|
+
"Bash(gh pr checks:*)",
|
|
256
|
+
"Bash(gh run view:*)",
|
|
257
|
+
"Bash(gh pr edit:*)",
|
|
258
|
+
"WebFetch(domain:patch-diff.githubusercontent.com)",
|
|
259
|
+
"Bash(git rebase:*)"
|
|
238
260
|
]
|
|
239
261
|
}
|
|
240
262
|
}
|
package/README.md
CHANGED
|
@@ -122,6 +122,7 @@ jss --help # Show help
|
|
|
122
122
|
| `--mashlib` | Enable Mashlib (local mode) | false |
|
|
123
123
|
| `--mashlib-cdn` | Enable Mashlib (CDN mode) | false |
|
|
124
124
|
| `--mashlib-version <ver>` | Mashlib CDN version | 2.0.0 |
|
|
125
|
+
| `--solidos-ui` | Enable modern SolidOS UI (requires --mashlib) | false |
|
|
125
126
|
| `--git` | Enable Git HTTP backend | false |
|
|
126
127
|
| `--nostr` | Enable Nostr relay | false |
|
|
127
128
|
| `--nostr-path <path>` | Nostr relay WebSocket path | /relay |
|
|
@@ -333,6 +334,18 @@ npm install && npm run build
|
|
|
333
334
|
|
|
334
335
|
**Note:** Mashlib works best with `--conneg` enabled for Turtle support.
|
|
335
336
|
|
|
337
|
+
**Modern UI (SolidOS UI):**
|
|
338
|
+
```bash
|
|
339
|
+
jss start --mashlib --solidos-ui --conneg
|
|
340
|
+
```
|
|
341
|
+
Serves a modern Nextcloud-style UI shell while reusing mashlib's data layer. The `--solidos-ui` flag swaps the classic databrowser interface for a cleaner, mobile-friendly design with:
|
|
342
|
+
- Modern file browser with breadcrumb navigation
|
|
343
|
+
- Profile, Contacts, Sharing, and Settings views
|
|
344
|
+
- Path-based URLs (browser URL reflects current resource)
|
|
345
|
+
- Responsive design for mobile devices
|
|
346
|
+
|
|
347
|
+
Requires solidos-ui dist files in `src/mashlib-local/dist/solidos-ui/`. See [solidos-ui](https://github.com/solidos/solidos/tree/main/workspaces/solidos-ui) for details.
|
|
348
|
+
|
|
336
349
|
### Profile Pages
|
|
337
350
|
|
|
338
351
|
Pod profiles (`/alice/`) use HTML with embedded JSON-LD data islands and are rendered using:
|
package/bin/jss.js
CHANGED
|
@@ -57,6 +57,7 @@ program
|
|
|
57
57
|
.option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
|
|
58
58
|
.option('--no-mashlib', 'Disable Mashlib data browser')
|
|
59
59
|
.option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
|
|
60
|
+
.option('--solidos-ui', 'Enable modern Nextcloud-style UI (requires --mashlib)')
|
|
60
61
|
.option('--git', 'Enable Git HTTP backend (clone/push support)')
|
|
61
62
|
.option('--no-git', 'Disable Git HTTP backend')
|
|
62
63
|
.option('--nostr', 'Enable Nostr relay')
|
|
@@ -114,6 +115,7 @@ program
|
|
|
114
115
|
mashlib: config.mashlib || config.mashlibCdn,
|
|
115
116
|
mashlibCdn: config.mashlibCdn,
|
|
116
117
|
mashlibVersion: config.mashlibVersion,
|
|
118
|
+
solidosUi: config.solidosUi,
|
|
117
119
|
git: config.git,
|
|
118
120
|
nostr: config.nostr,
|
|
119
121
|
nostrPath: config.nostrPath,
|
|
@@ -143,6 +145,7 @@ program
|
|
|
143
145
|
} else if (config.mashlib) {
|
|
144
146
|
console.log(` Mashlib: local (data browser enabled)`);
|
|
145
147
|
}
|
|
148
|
+
if (config.solidosUi) console.log(' SolidOS UI: enabled (modern interface)');
|
|
146
149
|
if (config.git) console.log(' Git: enabled (clone/push support)');
|
|
147
150
|
if (config.nostr) console.log(` Nostr: enabled (${config.nostrPath})`);
|
|
148
151
|
if (config.activitypub) console.log(` ActivityPub: enabled (@${config.apUsername || 'me'})`);
|
package/package.json
CHANGED
package/src/auth/middleware.js
CHANGED
|
@@ -9,7 +9,7 @@ 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 } from '../mashlib/index.js';
|
|
12
|
+
import { generateDatabrowserHtml, generateSolidosUiHtml } from '../mashlib/index.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Check if request is authorized
|
|
@@ -117,8 +117,11 @@ export function handleUnauthorized(request, reply, isAuthenticated, wacAllow, au
|
|
|
117
117
|
// If mashlib is enabled, serve mashlib instead of static error page
|
|
118
118
|
// Mashlib has built-in login functionality via panes.runDataBrowser()
|
|
119
119
|
if (request.mashlibEnabled) {
|
|
120
|
-
|
|
121
|
-
|
|
120
|
+
// Use SolidOS UI if enabled, otherwise fallback to classic mashlib
|
|
121
|
+
const html = request.solidosUiEnabled
|
|
122
|
+
? generateSolidosUiHtml()
|
|
123
|
+
: generateDatabrowserHtml(request.url, request.mashlibCdn ? request.mashlibVersion : null);
|
|
124
|
+
return reply.code(statusCode).type('text/html').send(html);
|
|
122
125
|
}
|
|
123
126
|
return reply.code(statusCode).type('text/html').send(getErrorPage(statusCode, isAuthenticated, request));
|
|
124
127
|
}
|
package/src/config.js
CHANGED
|
@@ -42,6 +42,9 @@ export const defaults = {
|
|
|
42
42
|
mashlibCdn: false,
|
|
43
43
|
mashlibVersion: '2.0.0',
|
|
44
44
|
|
|
45
|
+
// SolidOS UI (modern Nextcloud-style interface)
|
|
46
|
+
solidosUi: false,
|
|
47
|
+
|
|
45
48
|
// Git HTTP backend
|
|
46
49
|
git: false,
|
|
47
50
|
|
|
@@ -95,6 +98,7 @@ const envMap = {
|
|
|
95
98
|
JSS_MASHLIB: 'mashlib',
|
|
96
99
|
JSS_MASHLIB_CDN: 'mashlibCdn',
|
|
97
100
|
JSS_MASHLIB_VERSION: 'mashlibVersion',
|
|
101
|
+
JSS_SOLIDOS_UI: 'solidosUi',
|
|
98
102
|
JSS_GIT: 'git',
|
|
99
103
|
JSS_NOSTR: 'nostr',
|
|
100
104
|
JSS_NOSTR_PATH: 'nostrPath',
|
|
@@ -258,5 +262,6 @@ export function printConfig(config) {
|
|
|
258
262
|
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
259
263
|
console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
|
|
260
264
|
console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
|
|
265
|
+
console.log(` SolidOS UI: ${config.solidosUi ? 'enabled' : 'disabled'}`);
|
|
261
266
|
console.log('─'.repeat(40));
|
|
262
267
|
}
|
package/src/handlers/resource.js
CHANGED
|
@@ -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, shouldServeMashlib } from '../mashlib/index.js';
|
|
18
|
+
import { generateDatabrowserHtml, generateSolidosUiHtml, shouldServeMashlib } from '../mashlib/index.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Get the storage path and resource URL for a request
|
|
@@ -30,6 +30,64 @@ function getRequestPaths(request) {
|
|
|
30
30
|
return { urlPath, storagePath, resourceUrl };
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Parse HTTP Range header
|
|
35
|
+
* @param {string} rangeHeader - The Range header value (e.g., "bytes=0-1023")
|
|
36
|
+
* @param {number} fileSize - Total file size in bytes
|
|
37
|
+
* @returns {{ start: number, end: number } | null}
|
|
38
|
+
*/
|
|
39
|
+
function parseRangeHeader(rangeHeader, fileSize) {
|
|
40
|
+
if (!rangeHeader || !rangeHeader.startsWith('bytes=')) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const range = rangeHeader.slice(6); // Remove 'bytes='
|
|
45
|
+
|
|
46
|
+
// Multi-range requests (e.g., "0-100,200-300") are not supported
|
|
47
|
+
// Per RFC 7233, ignore Range header and serve full content instead of 416
|
|
48
|
+
if (range.includes(',')) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const parts = range.split('-');
|
|
53
|
+
|
|
54
|
+
if (parts.length !== 2) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let start, end;
|
|
59
|
+
|
|
60
|
+
if (parts[0] === '') {
|
|
61
|
+
// Suffix range: bytes=-500 (last 500 bytes)
|
|
62
|
+
const suffix = parseInt(parts[1], 10);
|
|
63
|
+
if (isNaN(suffix) || suffix <= 0) return null;
|
|
64
|
+
start = Math.max(0, fileSize - suffix);
|
|
65
|
+
end = fileSize - 1;
|
|
66
|
+
} else if (parts[1] === '') {
|
|
67
|
+
// Open-ended range: bytes=1024- (from 1024 to end)
|
|
68
|
+
start = parseInt(parts[0], 10);
|
|
69
|
+
if (isNaN(start) || start < 0) return null;
|
|
70
|
+
end = fileSize - 1;
|
|
71
|
+
} else {
|
|
72
|
+
// Normal range: bytes=0-1023
|
|
73
|
+
start = parseInt(parts[0], 10);
|
|
74
|
+
end = parseInt(parts[1], 10);
|
|
75
|
+
if (isNaN(start) || isNaN(end) || start < 0 || end < start) return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Clamp end to file size
|
|
79
|
+
if (end >= fileSize) {
|
|
80
|
+
end = fileSize - 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Check if range is satisfiable
|
|
84
|
+
if (start > end || start >= fileSize) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { start, end };
|
|
89
|
+
}
|
|
90
|
+
|
|
33
91
|
/**
|
|
34
92
|
* Handle GET request
|
|
35
93
|
*/
|
|
@@ -149,8 +207,10 @@ export async function handleGet(request, reply) {
|
|
|
149
207
|
|
|
150
208
|
// Check if we should serve Mashlib data browser for containers
|
|
151
209
|
if (shouldServeMashlib(request, request.mashlibEnabled, 'application/ld+json')) {
|
|
152
|
-
|
|
153
|
-
const html =
|
|
210
|
+
// Use SolidOS UI if enabled, otherwise fallback to classic mashlib
|
|
211
|
+
const html = request.solidosUiEnabled
|
|
212
|
+
? generateSolidosUiHtml()
|
|
213
|
+
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
|
|
154
214
|
const headers = getAllHeaders({
|
|
155
215
|
isContainer: true,
|
|
156
216
|
etag: stats.etag,
|
|
@@ -224,9 +284,10 @@ export async function handleGet(request, reply) {
|
|
|
224
284
|
// Check if we should serve Mashlib data browser
|
|
225
285
|
// Only for RDF resources when Accept: text/html is requested
|
|
226
286
|
if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
|
|
227
|
-
//
|
|
228
|
-
const
|
|
229
|
-
|
|
287
|
+
// Use SolidOS UI if enabled, otherwise fallback to classic mashlib
|
|
288
|
+
const html = request.solidosUiEnabled
|
|
289
|
+
? generateSolidosUiHtml()
|
|
290
|
+
: generateDatabrowserHtml(resourceUrl, request.mashlibCdn ? request.mashlibVersion : null);
|
|
230
291
|
const headers = getAllHeaders({
|
|
231
292
|
isContainer: false,
|
|
232
293
|
etag: stats.etag,
|
|
@@ -245,6 +306,43 @@ export async function handleGet(request, reply) {
|
|
|
245
306
|
return reply.type('text/html').send(html);
|
|
246
307
|
}
|
|
247
308
|
|
|
309
|
+
// Handle Range requests for media files (video, audio, etc.)
|
|
310
|
+
const rangeHeader = request.headers.range;
|
|
311
|
+
if (rangeHeader && !isRdfContentType(storedContentType)) {
|
|
312
|
+
const range = parseRangeHeader(rangeHeader, stats.size);
|
|
313
|
+
|
|
314
|
+
if (range) {
|
|
315
|
+
const { start, end } = range;
|
|
316
|
+
const chunkSize = end - start + 1;
|
|
317
|
+
|
|
318
|
+
const headers = getAllHeaders({
|
|
319
|
+
isContainer: false,
|
|
320
|
+
etag: stats.etag,
|
|
321
|
+
contentType: storedContentType,
|
|
322
|
+
origin,
|
|
323
|
+
resourceUrl,
|
|
324
|
+
connegEnabled
|
|
325
|
+
});
|
|
326
|
+
headers['Content-Range'] = `bytes ${start}-${end}/${stats.size}`;
|
|
327
|
+
headers['Content-Length'] = chunkSize;
|
|
328
|
+
|
|
329
|
+
Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
|
|
330
|
+
|
|
331
|
+
const streamResult = storage.createReadStream(storagePath, { start, end });
|
|
332
|
+
if (!streamResult) {
|
|
333
|
+
return reply.code(500).send({ error: 'Stream error' });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Handle stream errors that occur during response
|
|
337
|
+
streamResult.stream.on('error', (err) => {
|
|
338
|
+
console.error('Stream error during range response:', err.message);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return reply.code(206).send(streamResult.stream);
|
|
342
|
+
}
|
|
343
|
+
// If range is null (unsupported format or multi-range), fall through to serve full content
|
|
344
|
+
}
|
|
345
|
+
|
|
248
346
|
const content = await storage.read(storagePath);
|
|
249
347
|
if (content === null) {
|
|
250
348
|
return reply.code(500).send({ error: 'Read error' });
|
package/src/ldp/headers.js
CHANGED
|
@@ -56,6 +56,7 @@ export function getResponseHeaders({ isContainer = false, etag = null, contentTy
|
|
|
56
56
|
const headers = {
|
|
57
57
|
'Link': getLinkHeader(isContainer, aclUrl),
|
|
58
58
|
'Accept-Patch': 'text/n3, application/sparql-update',
|
|
59
|
+
'Accept-Ranges': isContainer ? 'none' : 'bytes',
|
|
59
60
|
'Allow': 'GET, HEAD, PUT, DELETE, PATCH, OPTIONS' + (isContainer ? ', POST' : ''),
|
|
60
61
|
'Vary': connegEnabled ? 'Accept, Authorization, Origin' : 'Authorization, Origin'
|
|
61
62
|
};
|
|
@@ -94,8 +95,8 @@ export function getCorsHeaders(origin) {
|
|
|
94
95
|
return {
|
|
95
96
|
'Access-Control-Allow-Origin': origin || '*',
|
|
96
97
|
'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
97
|
-
'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type, DPoP, If-Match, If-None-Match, Link, Slug, Origin',
|
|
98
|
-
'Access-Control-Expose-Headers': 'Accept-Patch, Accept-Post, Allow, Content-Type, ETag, Link, Location, Updates-Via, WAC-Allow',
|
|
98
|
+
'Access-Control-Allow-Headers': 'Accept, Authorization, Content-Type, DPoP, If-Match, If-None-Match, Link, Range, Slug, Origin',
|
|
99
|
+
'Access-Control-Expose-Headers': 'Accept-Patch, Accept-Post, Accept-Ranges, Allow, Content-Length, Content-Range, Content-Type, ETag, Link, Location, Updates-Via, WAC-Allow',
|
|
99
100
|
'Access-Control-Allow-Credentials': 'true',
|
|
100
101
|
'Access-Control-Max-Age': '86400'
|
|
101
102
|
};
|
package/src/mashlib/index.js
CHANGED
|
@@ -94,6 +94,113 @@ export function shouldServeMashlib(request, mashlibEnabled, contentType) {
|
|
|
94
94
|
return rdfTypes.includes(baseType);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Generate SolidOS UI HTML (modern Nextcloud-style interface)
|
|
99
|
+
* Uses mashlib for data layer but solidos-ui for the UI shell
|
|
100
|
+
*
|
|
101
|
+
* @returns {string} HTML content
|
|
102
|
+
*/
|
|
103
|
+
export function generateSolidosUiHtml() {
|
|
104
|
+
return `<!DOCTYPE html>
|
|
105
|
+
<html lang="en">
|
|
106
|
+
<head>
|
|
107
|
+
<meta charset="utf-8"/>
|
|
108
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
109
|
+
<title>SolidOS - Modern UI</title>
|
|
110
|
+
<!-- SolidOS UI Styles -->
|
|
111
|
+
<link rel="stylesheet" href="/solidos-ui/styles/variables.css">
|
|
112
|
+
<link rel="stylesheet" href="/solidos-ui/styles/shell.css">
|
|
113
|
+
<link rel="stylesheet" href="/solidos-ui/styles/components.css">
|
|
114
|
+
<link rel="stylesheet" href="/solidos-ui/styles/responsive.css">
|
|
115
|
+
<!-- View-specific styles -->
|
|
116
|
+
<link rel="stylesheet" href="/solidos-ui/views/profile/profile.css">
|
|
117
|
+
<link rel="stylesheet" href="/solidos-ui/views/contacts/contacts.css">
|
|
118
|
+
<link rel="stylesheet" href="/solidos-ui/views/sharing/sharing.css">
|
|
119
|
+
<link rel="stylesheet" href="/solidos-ui/views/settings/settings.css">
|
|
120
|
+
<!-- Bundled styles (contains all component styles) -->
|
|
121
|
+
<link rel="stylesheet" href="/solidos-ui/style.css">
|
|
122
|
+
<style>
|
|
123
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
124
|
+
html, body { height: 100%; }
|
|
125
|
+
#app { height: 100%; }
|
|
126
|
+
</style>
|
|
127
|
+
</head>
|
|
128
|
+
<body>
|
|
129
|
+
<div id="app"></div>
|
|
130
|
+
|
|
131
|
+
<script>
|
|
132
|
+
// Load mashlib first, then solidos-ui
|
|
133
|
+
(function() {
|
|
134
|
+
var mashScript = document.createElement('script');
|
|
135
|
+
mashScript.src = '/mashlib.min.js';
|
|
136
|
+
mashScript.onload = function() {
|
|
137
|
+
// Now load solidos-ui
|
|
138
|
+
import('/solidos-ui/solidos-ui.js').then(function(module) {
|
|
139
|
+
var initSolidOSSkin = module.initSolidOSSkin;
|
|
140
|
+
var SolidLogic = window.SolidLogic;
|
|
141
|
+
var panes = window.panes;
|
|
142
|
+
var store = SolidLogic.store;
|
|
143
|
+
|
|
144
|
+
initSolidOSSkin('#app', {
|
|
145
|
+
store: store,
|
|
146
|
+
fetcher: store.fetcher,
|
|
147
|
+
paneRegistry: panes,
|
|
148
|
+
authn: SolidLogic.authn,
|
|
149
|
+
logic: SolidLogic.solidLogicSingleton,
|
|
150
|
+
}, {
|
|
151
|
+
onNavigate: function(uri) {
|
|
152
|
+
if (uri) {
|
|
153
|
+
// Use path-based navigation - update URL to match resource
|
|
154
|
+
try {
|
|
155
|
+
var url = new URL(uri);
|
|
156
|
+
// Always use the path from the URI, regardless of origin
|
|
157
|
+
// (URIs may use internal hostname like jss:4000 vs localhost:4000)
|
|
158
|
+
var newPath = url.pathname;
|
|
159
|
+
if (newPath !== window.location.pathname) {
|
|
160
|
+
window.history.pushState({ uri: uri }, '', newPath);
|
|
161
|
+
}
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.warn('Invalid URI for navigation:', uri);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
onLogout: function() {
|
|
168
|
+
window.location.reload();
|
|
169
|
+
},
|
|
170
|
+
}).then(function(skin) {
|
|
171
|
+
// Handle browser back/forward
|
|
172
|
+
window.addEventListener('popstate', function(event) {
|
|
173
|
+
// Use the current URL as the resource (not hash-based)
|
|
174
|
+
var resourceUrl = window.location.origin + window.location.pathname;
|
|
175
|
+
skin.goto(resourceUrl);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Navigate to the current URL's resource
|
|
179
|
+
// The URL path IS the resource in JSS (not hash-based routing)
|
|
180
|
+
var currentPath = window.location.pathname;
|
|
181
|
+
if (currentPath && currentPath !== '/') {
|
|
182
|
+
var resourceUrl = window.location.origin + currentPath;
|
|
183
|
+
skin.goto(resourceUrl);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Expose for debugging
|
|
187
|
+
window.solidosSkin = skin;
|
|
188
|
+
});
|
|
189
|
+
}).catch(function(err) {
|
|
190
|
+
console.error('Failed to load solidos-ui:', err);
|
|
191
|
+
document.body.innerHTML = '<p>Failed to load SolidOS UI</p>';
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
mashScript.onerror = function() {
|
|
195
|
+
document.body.innerHTML = '<p>Failed to load Mashlib</p>';
|
|
196
|
+
};
|
|
197
|
+
document.head.appendChild(mashScript);
|
|
198
|
+
})();
|
|
199
|
+
</script>
|
|
200
|
+
</body>
|
|
201
|
+
</html>`;
|
|
202
|
+
}
|
|
203
|
+
|
|
97
204
|
/**
|
|
98
205
|
* Escape HTML special characters
|
|
99
206
|
*/
|
package/src/server.js
CHANGED
|
@@ -55,6 +55,8 @@ export function createServer(options = {}) {
|
|
|
55
55
|
const mashlibEnabled = options.mashlib ?? false;
|
|
56
56
|
const mashlibCdn = options.mashlibCdn ?? false;
|
|
57
57
|
const mashlibVersion = options.mashlibVersion ?? '2.0.0';
|
|
58
|
+
// SolidOS UI (modern Nextcloud-style interface) - requires mashlib
|
|
59
|
+
const solidosUiEnabled = options.solidosUi ?? false;
|
|
58
60
|
// Git HTTP backend is OFF by default - enables clone/push via git protocol
|
|
59
61
|
const gitEnabled = options.git ?? false;
|
|
60
62
|
// Nostr relay is OFF by default
|
|
@@ -127,6 +129,7 @@ export function createServer(options = {}) {
|
|
|
127
129
|
fastify.decorateRequest('mashlibEnabled', null);
|
|
128
130
|
fastify.decorateRequest('mashlibCdn', null);
|
|
129
131
|
fastify.decorateRequest('mashlibVersion', null);
|
|
132
|
+
fastify.decorateRequest('solidosUiEnabled', null);
|
|
130
133
|
fastify.decorateRequest('defaultQuota', null);
|
|
131
134
|
fastify.addHook('onRequest', async (request) => {
|
|
132
135
|
request.connegEnabled = connegEnabled;
|
|
@@ -137,6 +140,7 @@ export function createServer(options = {}) {
|
|
|
137
140
|
request.mashlibEnabled = mashlibEnabled;
|
|
138
141
|
request.mashlibCdn = mashlibCdn;
|
|
139
142
|
request.mashlibVersion = mashlibVersion;
|
|
143
|
+
request.solidosUiEnabled = solidosUiEnabled;
|
|
140
144
|
request.defaultQuota = defaultQuota;
|
|
141
145
|
|
|
142
146
|
// Extract pod name from subdomain if enabled
|
|
@@ -296,7 +300,7 @@ export function createServer(options = {}) {
|
|
|
296
300
|
// Authorization hook - check WAC permissions
|
|
297
301
|
// Skip for pod creation endpoint (needs special handling)
|
|
298
302
|
fastify.addHook('preHandler', async (request, reply) => {
|
|
299
|
-
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib, well-known, notifications, nostr, git, and AP
|
|
303
|
+
// Skip auth for pod creation, OPTIONS, IdP routes, mashlib, solidos-ui, well-known, notifications, nostr, git, and AP
|
|
300
304
|
const mashlibPaths = ['/mashlib.min.js', '/mash.css', '/841.mashlib.min.js'];
|
|
301
305
|
const apPaths = ['/inbox', '/profile/card/inbox', '/profile/card/outbox', '/profile/card/followers', '/profile/card/following'];
|
|
302
306
|
// Check if request wants ActivityPub content for profile
|
|
@@ -308,6 +312,7 @@ export function createServer(options = {}) {
|
|
|
308
312
|
request.method === 'OPTIONS' ||
|
|
309
313
|
request.url.startsWith('/idp/') ||
|
|
310
314
|
request.url.startsWith('/.well-known/') ||
|
|
315
|
+
request.url.startsWith('/solidos-ui/') ||
|
|
311
316
|
(nostrEnabled && request.url.startsWith(nostrPath)) ||
|
|
312
317
|
(gitEnabled && isGitRequest(request.url)) ||
|
|
313
318
|
(activitypubEnabled && apPaths.some(p => request.url === p || request.url.startsWith(p + '?'))) ||
|
|
@@ -381,6 +386,37 @@ export function createServer(options = {}) {
|
|
|
381
386
|
}
|
|
382
387
|
}
|
|
383
388
|
|
|
389
|
+
// SolidOS UI static files (modern Nextcloud-style interface)
|
|
390
|
+
// Serves from /solidos-ui/* - requires mashlib to be enabled as well
|
|
391
|
+
if (solidosUiEnabled && mashlibEnabled) {
|
|
392
|
+
const solidosUiDir = join(__dirname, 'mashlib-local', 'dist', 'solidos-ui');
|
|
393
|
+
|
|
394
|
+
// Serve all files under /solidos-ui/* path
|
|
395
|
+
fastify.get('/solidos-ui/*', async (request, reply) => {
|
|
396
|
+
try {
|
|
397
|
+
// Get the path after /solidos-ui/
|
|
398
|
+
const filePath = request.url.replace('/solidos-ui/', '').split('?')[0];
|
|
399
|
+
const fullPath = join(solidosUiDir, filePath);
|
|
400
|
+
|
|
401
|
+
// Determine content type based on extension
|
|
402
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
403
|
+
const contentTypes = {
|
|
404
|
+
'js': 'application/javascript',
|
|
405
|
+
'css': 'text/css',
|
|
406
|
+
'map': 'application/json',
|
|
407
|
+
'html': 'text/html'
|
|
408
|
+
};
|
|
409
|
+
const contentType = contentTypes[ext] || 'application/octet-stream';
|
|
410
|
+
|
|
411
|
+
const content = await readFile(fullPath);
|
|
412
|
+
return reply.type(contentType).send(content);
|
|
413
|
+
} catch (err) {
|
|
414
|
+
request.log.error(err, 'Failed to serve solidos-ui file');
|
|
415
|
+
return reply.code(404).send({ error: 'Not Found' });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
384
420
|
// Rate limit configuration for write operations
|
|
385
421
|
// Protects against resource exhaustion and abuse
|
|
386
422
|
const writeRateLimit = {
|
|
@@ -51,6 +51,28 @@ export async function read(urlPath) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Create a readable stream for a resource (supports range requests)
|
|
56
|
+
* @param {string} urlPath
|
|
57
|
+
* @param {object} options - { start, end } byte range options
|
|
58
|
+
* @returns {{ stream: ReadStream, filePath: string } | null}
|
|
59
|
+
*/
|
|
60
|
+
export function createReadStream(urlPath, options = {}) {
|
|
61
|
+
const filePath = urlToPath(urlPath);
|
|
62
|
+
|
|
63
|
+
// Check file exists before creating stream (createReadStream doesn't throw sync)
|
|
64
|
+
if (!fs.pathExistsSync(filePath)) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const stream = fs.createReadStream(filePath, options);
|
|
70
|
+
return { stream, filePath };
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
/**
|
|
55
77
|
* Write resource content
|
|
56
78
|
* @param {string} urlPath
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range Request Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests HTTP Range header support for partial content delivery.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, before, after } from 'node:test';
|
|
8
|
+
import assert from 'node:assert';
|
|
9
|
+
import {
|
|
10
|
+
startTestServer,
|
|
11
|
+
stopTestServer,
|
|
12
|
+
request,
|
|
13
|
+
createTestPod,
|
|
14
|
+
assertStatus
|
|
15
|
+
} from './helpers.js';
|
|
16
|
+
|
|
17
|
+
describe('Range Requests', () => {
|
|
18
|
+
const testContent = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; // 36 bytes
|
|
19
|
+
|
|
20
|
+
before(async () => {
|
|
21
|
+
await startTestServer();
|
|
22
|
+
await createTestPod('rangetest');
|
|
23
|
+
|
|
24
|
+
// Create a test file with known content
|
|
25
|
+
await request('/rangetest/public/test.txt', {
|
|
26
|
+
method: 'PUT',
|
|
27
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
28
|
+
body: testContent,
|
|
29
|
+
auth: 'rangetest'
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
after(async () => {
|
|
34
|
+
await stopTestServer();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('Accept-Ranges header', () => {
|
|
38
|
+
it('should include Accept-Ranges: bytes for files', async () => {
|
|
39
|
+
const res = await request('/rangetest/public/test.txt');
|
|
40
|
+
assertStatus(res, 200);
|
|
41
|
+
assert.strictEqual(res.headers.get('Accept-Ranges'), 'bytes');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should include Accept-Ranges: none for containers', async () => {
|
|
45
|
+
const res = await request('/rangetest/public/');
|
|
46
|
+
assertStatus(res, 200);
|
|
47
|
+
assert.strictEqual(res.headers.get('Accept-Ranges'), 'none');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('Range header parsing', () => {
|
|
52
|
+
it('should return 206 for valid range bytes=0-9', async () => {
|
|
53
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
54
|
+
headers: { 'Range': 'bytes=0-9' }
|
|
55
|
+
});
|
|
56
|
+
assertStatus(res, 206);
|
|
57
|
+
|
|
58
|
+
const body = await res.text();
|
|
59
|
+
assert.strictEqual(body, 'ABCDEFGHIJ');
|
|
60
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 0-9/36');
|
|
61
|
+
assert.strictEqual(res.headers.get('Content-Length'), '10');
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should return 206 for open-ended range bytes=30-', async () => {
|
|
65
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
66
|
+
headers: { 'Range': 'bytes=30-' }
|
|
67
|
+
});
|
|
68
|
+
assertStatus(res, 206);
|
|
69
|
+
|
|
70
|
+
const body = await res.text();
|
|
71
|
+
assert.strictEqual(body, '456789');
|
|
72
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 30-35/36');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return 206 for suffix range bytes=-6', async () => {
|
|
76
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
77
|
+
headers: { 'Range': 'bytes=-6' }
|
|
78
|
+
});
|
|
79
|
+
assertStatus(res, 206);
|
|
80
|
+
|
|
81
|
+
const body = await res.text();
|
|
82
|
+
assert.strictEqual(body, '456789');
|
|
83
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 30-35/36');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should clamp end to file size for range exceeding file', async () => {
|
|
87
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
88
|
+
headers: { 'Range': 'bytes=30-1000' }
|
|
89
|
+
});
|
|
90
|
+
assertStatus(res, 206);
|
|
91
|
+
|
|
92
|
+
const body = await res.text();
|
|
93
|
+
assert.strictEqual(body, '456789');
|
|
94
|
+
assert.strictEqual(res.headers.get('Content-Range'), 'bytes 30-35/36');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe('Multi-range requests', () => {
|
|
99
|
+
it('should ignore multi-range and return 200 with full content', async () => {
|
|
100
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
101
|
+
headers: { 'Range': 'bytes=0-5,10-15' }
|
|
102
|
+
});
|
|
103
|
+
// Multi-range is not supported, should fall back to 200
|
|
104
|
+
assertStatus(res, 200);
|
|
105
|
+
|
|
106
|
+
const body = await res.text();
|
|
107
|
+
assert.strictEqual(body, testContent);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('Invalid ranges', () => {
|
|
112
|
+
it('should return 200 for invalid range format', async () => {
|
|
113
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
114
|
+
headers: { 'Range': 'invalid' }
|
|
115
|
+
});
|
|
116
|
+
// Invalid format, ignore Range header
|
|
117
|
+
assertStatus(res, 200);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should return 200 for non-bytes range unit', async () => {
|
|
121
|
+
const res = await request('/rangetest/public/test.txt', {
|
|
122
|
+
headers: { 'Range': 'chars=0-10' }
|
|
123
|
+
});
|
|
124
|
+
assertStatus(res, 200);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('RDF resources', () => {
|
|
129
|
+
it('should ignore Range header for RDF resources', async () => {
|
|
130
|
+
// Create an RDF resource
|
|
131
|
+
await request('/rangetest/public/data.jsonld', {
|
|
132
|
+
method: 'PUT',
|
|
133
|
+
headers: { 'Content-Type': 'application/ld+json' },
|
|
134
|
+
body: JSON.stringify({ '@id': '#test', 'http://example.org/name': 'Test' }),
|
|
135
|
+
auth: 'rangetest'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const res = await request('/rangetest/public/data.jsonld', {
|
|
139
|
+
headers: { 'Range': 'bytes=0-10' }
|
|
140
|
+
});
|
|
141
|
+
// RDF resources don't support range requests
|
|
142
|
+
assertStatus(res, 200);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|