Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,6 @@ test-nostr-acl.js
test-dpop-flow.js
cth-config/
test-data-idp-accounts/

# Local mashlib build (for development)
src/mashlib-local/
34 changes: 24 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ npm run benchmark

## Features

### Implemented (v0.0.17)
### Implemented (v0.0.23)

- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
- **N3 Patch** - Solid's native patch format for RDF updates
Expand All @@ -66,11 +66,13 @@ npm run benchmark
- **Container Management** - Create, list, and manage containers
- **Multi-user Pods** - Path-based (`/alice/`) or subdomain-based (`alice.example.com`)
- **Subdomain Mode** - XSS protection via origin isolation
- **Mashlib Data Browser** - Optional SolidOS UI for browsing RDF resources
- **Mashlib Data Browser** - Optional SolidOS UI (CDN or local hosting)
- **WebID Profiles** - JSON-LD structured data in HTML at pod root
- **Web Access Control (WAC)** - `.acl` file-based authorization
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
- **Solid-OIDC Resource Server** - Accept DPoP-bound access tokens from external IdPs
- **NSS-style Registration** - Username/password auth compatible with Solid apps
- **Nostr Authentication** - NIP-98 HTTP Auth with Schnorr signatures
- **Simple Auth Tokens** - Built-in token authentication for development
- **Content Negotiation** - Optional Turtle <-> JSON-LD conversion
- **CORS Support** - Full cross-origin resource sharing
Expand Down Expand Up @@ -139,8 +141,9 @@ jss --help # Show help
| `--idp-issuer <url>` | IdP issuer URL | (auto) |
| `--subdomains` | Enable subdomain-based pods | false |
| `--base-domain <domain>` | Base domain for subdomains | - |
| `--mashlib` | Enable Mashlib data browser | false |
| `--mashlib-version <ver>` | Mashlib version | 2.0.0 |
| `--mashlib` | Enable Mashlib (local mode) | false |
| `--mashlib-cdn` | Enable Mashlib (CDN mode) | false |
| `--mashlib-version <ver>` | Mashlib CDN version | 2.0.0 |
| `-q, --quiet` | Suppress logs | false |

### Environment Variables
Expand Down Expand Up @@ -407,24 +410,35 @@ createServer({
notifications: false, // Enable WebSocket notifications (default: false)
subdomains: false, // Enable subdomain-based pods (default: false)
baseDomain: null, // Base domain for subdomains (e.g., "example.com")
mashlib: false, // Enable Mashlib data browser (default: false)
mashlibVersion: '2.0.0', // Mashlib version to use
mashlib: false, // Enable Mashlib data browser - local mode (default: false)
mashlibCdn: false, // Enable Mashlib data browser - CDN mode (default: false)
mashlibVersion: '2.0.0', // Mashlib version for CDN mode
});
```

### Mashlib Data Browser

Enable the [SolidOS Mashlib](https://github.com/SolidOS/mashlib) data browser for RDF resources:
Enable the [SolidOS Mashlib](https://github.com/SolidOS/mashlib) data browser for RDF resources. Two modes are available:

**CDN Mode** (recommended for getting started):
```bash
jss start --mashlib --conneg
jss start --mashlib-cdn --conneg
```
Loads mashlib from unpkg.com CDN. Zero footprint - no local files needed.

When enabled, requesting an RDF resource with `Accept: text/html` returns an interactive data browser UI instead of raw data. Mashlib is loaded from the unpkg CDN.
**Local Mode** (for production/offline):
```bash
jss start --mashlib --conneg
```
Serves mashlib from `src/mashlib-local/dist/`. Requires building mashlib locally:
```bash
cd src/mashlib-local
npm install && npm run build
```

**How it works:**
1. Browser requests `/alice/public/data.ttl` with `Accept: text/html`
2. Server returns Mashlib HTML wrapper (loads JS/CSS from CDN)
2. Server returns Mashlib HTML wrapper
3. Mashlib fetches the actual data via content negotiation
4. Mashlib renders an interactive, editable view

Expand Down
14 changes: 10 additions & 4 deletions bin/jss.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,10 @@ program
.option('--subdomains', 'Enable subdomain-based pods (XSS protection)')
.option('--no-subdomains', 'Disable subdomain-based pods')
.option('--base-domain <domain>', 'Base domain for subdomain pods (e.g., "example.com")')
.option('--mashlib', 'Enable Mashlib data browser for RDF resources')
.option('--mashlib', 'Enable Mashlib data browser (local mode, requires mashlib in node_modules)')
.option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
.option('--no-mashlib', 'Disable Mashlib data browser')
.option('--mashlib-version <version>', 'Mashlib version to use (default: 2.0.0)')
.option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
.option('-q, --quiet', 'Suppress log output')
.option('--print-config', 'Print configuration and exit')
.action(async (options) => {
Expand Down Expand Up @@ -91,7 +92,8 @@ program
root: config.root,
subdomains: config.subdomains,
baseDomain: config.baseDomain,
mashlib: config.mashlib,
mashlib: config.mashlib || config.mashlibCdn,
mashlibCdn: config.mashlibCdn,
mashlibVersion: config.mashlibVersion,
});

Expand All @@ -106,7 +108,11 @@ program
if (config.notifications) console.log(' WebSocket: enabled');
if (config.idp) console.log(` IdP: ${idpIssuer}`);
if (config.subdomains) console.log(` Subdomains: ${config.baseDomain} (XSS protection enabled)`);
if (config.mashlib) console.log(` Mashlib: v${config.mashlibVersion} (data browser enabled)`);
if (config.mashlibCdn) {
console.log(` Mashlib: v${config.mashlibVersion} (CDN mode)`);
} else if (config.mashlib) {
console.log(` Mashlib: local (data browser enabled)`);
}
console.log('\n Press Ctrl+C to stop\n');
}

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "javascript-solid-server",
"version": "0.0.22",
"version": "0.0.23",
"description": "A minimal, fast Solid server",
"main": "src/index.js",
"type": "module",
Expand Down
4 changes: 3 additions & 1 deletion src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const defaults = {

// Mashlib data browser
mashlib: false,
mashlibCdn: false,
mashlibVersion: '2.0.0',

// Logging
Expand Down Expand Up @@ -68,6 +69,7 @@ const envMap = {
JSS_SUBDOMAINS: 'subdomains',
JSS_BASE_DOMAIN: 'baseDomain',
JSS_MASHLIB: 'mashlib',
JSS_MASHLIB_CDN: 'mashlibCdn',
JSS_MASHLIB_VERSION: 'mashlibVersion',
};

Expand Down Expand Up @@ -201,6 +203,6 @@ export function printConfig(config) {
console.log(` Notifications: ${config.notifications}`);
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
console.log(` Mashlib: ${config.mashlib ? `v${config.mashlibVersion}` : 'disabled'}`);
console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
console.log('─'.repeat(40));
}
14 changes: 10 additions & 4 deletions src/handlers/resource.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,9 @@ export async function handleGet(request, reply) {
// Check if we should serve Mashlib data browser
// Only for RDF resources when Accept: text/html is requested
if (shouldServeMashlib(request, request.mashlibEnabled, storedContentType)) {
const html = generateDatabrowserHtml(resourceUrl, request.mashlibVersion);
// Pass CDN version if using CDN mode, null for local mode
const cdnVersion = request.mashlibCdn ? request.mashlibVersion : null;
const html = generateDatabrowserHtml(resourceUrl, cdnVersion);
const headers = getAllHeaders({
isContainer: false,
etag: stats.etag,
Expand All @@ -155,6 +157,10 @@ export async function handleGet(request, reply) {
connegEnabled
});
headers['Vary'] = 'Accept';
headers['X-Frame-Options'] = 'DENY';
headers['Content-Security-Policy'] = "frame-ancestors 'none'";
// Don't cache the HTML wrapper - always negotiate fresh
headers['Cache-Control'] = 'no-store';

Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
return reply.type('text/html').send(html);
Expand Down Expand Up @@ -191,7 +197,7 @@ export async function handleGet(request, reply) {
resourceUrl,
connegEnabled
});
headers['Vary'] = getVaryHeader(connegEnabled);
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);

Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
return reply.send(outputContent);
Expand All @@ -209,7 +215,7 @@ export async function handleGet(request, reply) {
resourceUrl,
connegEnabled
});
headers['Vary'] = getVaryHeader(connegEnabled);
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);

Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
return reply.send(content);
Expand Down Expand Up @@ -353,7 +359,7 @@ export async function handlePut(request, reply) {
const origin = request.headers.origin;
const headers = getAllHeaders({ isContainer: false, origin, resourceUrl, connegEnabled });
headers['Location'] = resourceUrl;
headers['Vary'] = getVaryHeader(connegEnabled);
headers['Vary'] = getVaryHeader(connegEnabled, request.mashlibEnabled);

Object.entries(headers).forEach(([k, v]) => reply.header(k, v));

Expand Down
75 changes: 34 additions & 41 deletions src/mashlib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,38 @@
* we return this wrapper which then fetches and renders the data.
*/

const CDN_BASE = 'https://unpkg.com/mashlib';

/**
* Generate Mashlib databrowser HTML
* @param {string} resourceUrl - The URL of the resource being viewed
* @param {string} version - Mashlib version (default: '2.0.0')
*
* @param {string} resourceUrl - The URL of the resource being viewed (unused, kept for API compatibility)
* @param {string} cdnVersion - If provided, load mashlib from unpkg CDN (e.g., "2.0.0")
* @returns {string} HTML content
*/
export function generateDatabrowserHtml(resourceUrl, version = '2.0.0') {
const cdnUrl = `${CDN_BASE}@${version}/dist`;
export function generateDatabrowserHtml(resourceUrl, cdnVersion = null) {
if (cdnVersion) {
// CDN mode - use script.onload to ensure mashlib is fully loaded before init
// This avoids race conditions with defer + DOMContentLoaded
const cdnBase = `https://unpkg.com/mashlib@${cdnVersion}/dist`;
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title>
<link href="${cdnBase}/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>
<script>
(function() {
var s = document.createElement('script');
s.src = '${cdnBase}/mashlib.min.js';
s.onload = function() { panes.runDataBrowser(); };
s.onerror = function() { document.body.innerHTML = '<p>Failed to load Mashlib from CDN</p>'; };
document.head.appendChild(s);
})();
</script></body></html>`;
}

return `<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SolidOS - ${escapeHtml(resourceUrl)}</title>
<script defer src="${cdnUrl}/mashlib.min.js"></script>
<link href="${cdnUrl}/mash.css" rel="stylesheet">
<script>
document.addEventListener('DOMContentLoaded', function() {
// runDataBrowser uses window.location to determine what to fetch
panes.runDataBrowser();
});
</script>
<style>
/* Loading indicator */
body:not(.loaded) #PageBody::before {
content: 'Loading SolidOS...';
display: block;
padding: 2em;
text-align: center;
color: #666;
}
</style>
</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>`;
// Local mode - use defer (reliable when served locally)
return `<!doctype html><html><head><meta charset="utf-8"/><title>SolidOS Web App</title><script>document.addEventListener('DOMContentLoaded', function() {
panes.runDataBrowser()
})</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>`;
}

/**
Expand All @@ -61,11 +48,17 @@ export function generateDatabrowserHtml(resourceUrl, version = '2.0.0') {
* @returns {boolean}
*/
export function shouldServeMashlib(request, mashlibEnabled, contentType) {
const accept = request.headers.accept || '';
const secFetchDest = request.headers['sec-fetch-dest'] || '';

if (!mashlibEnabled) {
return false;
}

const accept = request.headers.accept || '';
// Don't serve mashlib for iframe/embed requests (prevents recursive loop)
if (secFetchDest === 'iframe' || secFetchDest === 'embed' || secFetchDest === 'object') {
return false;
}

// Must explicitly accept HTML
if (!accept.includes('text/html')) {
Expand Down
5 changes: 3 additions & 2 deletions src/rdf/conneg.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,10 @@ export async function fromJsonLd(jsonLd, targetType, baseUri, connegEnabled = fa

/**
* Get Vary header value for content negotiation
* Include Accept when conneg or mashlib is enabled (response varies by Accept header)
*/
export function getVaryHeader(connegEnabled) {
return connegEnabled ? 'Accept, Origin' : 'Origin';
export function getVaryHeader(connegEnabled, mashlibEnabled = false) {
return (connegEnabled || mashlibEnabled) ? 'Accept, Origin' : 'Origin';
}

/**
Expand Down
Loading