diff --git a/README.md b/README.md index 937170e..e88e7e1 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) @@ -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: diff --git a/bin/jss.js b/bin/jss.js index d55f90d..7c584b5 100755 --- a/bin/jss.js +++ b/bin/jss.js @@ -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 ', 'IdP issuer URL (defaults to server URL)') diff --git a/src/config.js b/src/config.js index 84c055d..d400b5b 100644 --- a/src/config.js +++ b/src/config.js @@ -28,6 +28,7 @@ export const defaults = { multiuser: true, conneg: false, notifications: false, + lwsMode: false, // LWS protocol mode (draft) // Identity Provider idp: false, @@ -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', @@ -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'}`); diff --git a/src/server.js b/src/server.js index 125ab26..e20351a 100644 --- a/src/server.js +++ b/src/server.js @@ -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; @@ -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); @@ -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; diff --git a/test/lws.test.js b/test/lws.test.js new file mode 100644 index 0000000..ae5c597 --- /dev/null +++ b/test/lws.test.js @@ -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': '; 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'); + }); + }); +});