Skip to content
Draft
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
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ A minimal, fast, JSON-LD native Solid server.

- **Passkey Authentication** - WebAuthn/FIDO2 passwordless login with Touch ID, Face ID, or security keys
- **HTTP Range Requests** - Partial content delivery for large files and media streaming
- **LWS Protocol Mode (DRAFT)** - Optional W3C Linked Web Storage semantics (`--lws-mode` flag, see #87)
- **Single-User Mode** - Simplified setup for personal pod servers
- **ActivityPub Federation** - Fediverse integration with WebFinger, inbox/outbox, HTTP signatures
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
Expand Down Expand Up @@ -301,6 +302,7 @@ createServer({
logger: true, // Enable Fastify logging (default: true)
conneg: false, // Enable content negotiation (default: false)
notifications: false, // Enable WebSocket notifications (default: false)
lwsMode: false, // Enable LWS protocol mode - DRAFT (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 - local mode (default: false)
Expand Down Expand Up @@ -349,6 +351,50 @@ Serves a modern Nextcloud-style UI shell while reusing mashlib's data layer. The

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.

### LWS Protocol Mode (DRAFT)

⚠️ **Experimental Feature** - See [issue #87](https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues/87) for full details.

Enable W3C Linked Web Storage protocol semantics:

```bash
jss start --lws-mode
```

**Key Differences from Solid/LDP:**

| Aspect | Solid/LDP (Default) | LWS Mode |
|--------|-------------------|----------|
| Resource Creation | PUT or POST | PUT or POST (POST+Slug emphasized) |
| Container Detection | Trailing slash `/` | Link header `rel="type"` (planned) |
| Metadata Updates | N3 Patch, SPARQL | JSON Merge Patch on linkset (planned) |

**Current Status:**

Currently, `--lws-mode` is primarily infrastructure. The PUT/POST semantics are **the same** as Solid/LDP (PUT can create or update).

Future LWS-specific features (when implemented):
- Link header-based container detection (alternative to trailing slash)
- Linkset metadata endpoints (`/resource;linkset`)
- JSON Merge Patch for metadata updates

**Example:**
```bash
# Both work in LWS mode (same as default)
curl -X PUT http://localhost:3000/alice/public/new.json \
-H "Content-Type: application/json" \
-d '{"test": true}'
# 201 Created

curl -X POST http://localhost:3000/alice/public/ \
-H "Content-Type: application/json" \
-H "Slug: another-resource" \
-d '{"test": true}'
# 201 Created
```

**Status:** Early draft implementation. LWS spec is evolving - monitor ecosystem adoption before production use. Per Solid CG clarification, PUT creation is allowed in LWS.

### Profile Pages

Pod profiles (`/alice/`) use HTML with embedded JSON-LD data islands and are rendered using:
Expand Down
2 changes: 2 additions & 0 deletions bin/jss.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ program
.option('--no-conneg', 'Disable content negotiation')
.option('--notifications', 'Enable WebSocket notifications')
.option('--no-notifications', 'Disable WebSocket notifications')
.option('--lws-mode', 'Enable LWS protocol mode (DRAFT - see issue #87)')
.option('--no-lws-mode', 'Use Solid/LDP protocol mode (default)')
.option('--idp', 'Enable built-in Identity Provider')
.option('--no-idp', 'Disable built-in Identity Provider')
.option('--idp-issuer <url>', 'IdP issuer URL (defaults to server URL)')
Expand Down
3 changes: 3 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const defaults = {
multiuser: true,
conneg: false,
notifications: false,
lwsMode: false, // LWS protocol mode (draft)

// Identity Provider
idp: false,
Expand Down Expand Up @@ -89,6 +90,7 @@ const envMap = {
JSS_MULTIUSER: 'multiuser',
JSS_CONNEG: 'conneg',
JSS_NOTIFICATIONS: 'notifications',
JSS_LWS_MODE: 'lwsMode',
JSS_QUIET: 'quiet',
JSS_CONFIG_PATH: 'configPath',
JSS_IDP: 'idp',
Expand Down Expand Up @@ -259,6 +261,7 @@ export function printConfig(config) {
console.log(` Multi-user: ${config.multiuser}`);
console.log(` Conneg: ${config.conneg}`);
console.log(` Notifications: ${config.notifications}`);
console.log(` LWS Mode: ${config.lwsMode ? 'enabled (DRAFT)' : 'disabled'}`);
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
console.log(` Subdomains: ${config.subdomains ? (config.baseDomain || 'enabled') : 'disabled'}`);
console.log(` Mashlib: ${config.mashlibCdn ? `CDN v${config.mashlibVersion}` : config.mashlib ? 'local' : 'disabled'}`);
Expand Down
4 changes: 4 additions & 0 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ export function createServer(options = {}) {
const connegEnabled = options.conneg ?? false;
// WebSocket notifications are OFF by default
const notificationsEnabled = options.notifications ?? false;
// LWS protocol mode is OFF by default - use Solid/LDP semantics
const lwsMode = options.lwsMode ?? false;
// Identity Provider is OFF by default
const idpEnabled = options.idp ?? false;
const idpIssuer = options.idpIssuer;
Expand Down Expand Up @@ -122,6 +124,7 @@ export function createServer(options = {}) {
// Attach server config to requests
fastify.decorateRequest('connegEnabled', null);
fastify.decorateRequest('notificationsEnabled', null);
fastify.decorateRequest('lwsMode', null);
fastify.decorateRequest('idpEnabled', null);
fastify.decorateRequest('subdomainsEnabled', null);
fastify.decorateRequest('baseDomain', null);
Expand All @@ -134,6 +137,7 @@ export function createServer(options = {}) {
fastify.addHook('onRequest', async (request) => {
request.connegEnabled = connegEnabled;
request.notificationsEnabled = notificationsEnabled;
request.lwsMode = lwsMode;
request.idpEnabled = idpEnabled;
request.subdomainsEnabled = subdomainsEnabled;
request.baseDomain = baseDomain;
Expand Down
132 changes: 132 additions & 0 deletions test/lws.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* LWS Protocol Mode Tests (DRAFT)
*
* Tests for W3C Linked Web Storage protocol semantics.
* See issue #87 for full specification and implementation plan.
*/

import { describe, it, before, after } from 'node:test';
import assert from 'node:assert';
import {
startTestServer,
stopTestServer,
request,
createTestPod,
assertStatus
} from './helpers.js';

describe('LWS Protocol Mode (DRAFT)', () => {
let baseUrl;

before(async () => {
// Start server with LWS mode enabled
const result = await startTestServer({ lwsMode: true });
baseUrl = result.baseUrl;
await createTestPod('lwstest');
});

after(async () => {
await stopTestServer();
});

describe('PUT Semantics (Creation and Updates)', () => {
it('should allow PUT for resource creation in LWS mode', async () => {
// LWS allows PUT for creation (clarified by Eric on Solid CG call)
const res = await request('/lwstest/public/new-via-put.json', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ created: 'via PUT' }),
auth: 'lwstest'
});

assertStatus(res, 201);

// Verify created
const verify = await request('/lwstest/public/new-via-put.json');
const data = await verify.json();
assert.strictEqual(data.created, 'via PUT');
});

it('should allow PUT to update existing resource in LWS mode', async () => {
// First create via POST
const createRes = await request('/lwstest/public/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Slug': 'existing-resource'
},
body: JSON.stringify({ version: 1 }),
auth: 'lwstest'
});

assertStatus(createRes, 201);
const location = createRes.headers.get('Location');

// Now update via PUT (should work)
const updateRes = await request(location, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: 2 }),
auth: 'lwstest'
});

assertStatus(updateRes, 204);

// Verify updated
const verify = await request(location);
const data = await verify.json();
assert.strictEqual(data.version, 2);
});
});

describe('POST Creation (LWS Standard)', () => {
it('should create resource via POST with Slug header', async () => {
const res = await request('/lwstest/public/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Slug': 'lws-created'
},
body: JSON.stringify({ created: 'via POST' }),
auth: 'lwstest'
});

assertStatus(res, 201);
const location = res.headers.get('Location');
assert.ok(location, 'Should return Location header');
assert.ok(location.includes('lws-created'), 'Should use Slug in filename');
});

it('should create container via POST with Link header', async () => {
const res = await request('/lwstest/public/', {
method: 'POST',
headers: {
'Slug': 'lws-container',
'Link': '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"'
},
auth: 'lwstest'
});

assertStatus(res, 201);
const location = res.headers.get('Location');
assert.ok(location.endsWith('/'), 'Container should end with /');
});
});

describe('Documentation', () => {
it('should document LWS mode differences', () => {
// This test serves as documentation
const differences = {
PUT: 'Allowed for creation and updates (same as LDP)',
POST: 'Emphasized pattern with Slug for server-assigned URIs',
containerDetection: 'Link header (future: may remove slash semantics)',
metadataUpdates: 'Future: JSON Merge Patch on linkset resources',
etagRequirement: 'Mandatory (already implemented)'
};

// Note: Per Eric on Solid CG call, LWS allows PUT creation
// The main difference is POST+Slug is the emphasized pattern
assert.ok(differences.POST.includes('Slug'), 'POST with Slug emphasized');
});
});
});