javascript-solid-server 0.0.7 → 0.0.9

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.
@@ -13,7 +13,9 @@
13
13
  "Bash(pkill:*)",
14
14
  "Bash(curl:*)",
15
15
  "Bash(npm test:*)",
16
- "Bash(git add:*)"
16
+ "Bash(git add:*)",
17
+ "WebFetch(domain:solid.github.io)",
18
+ "Bash(node:*)"
17
19
  ]
18
20
  }
19
21
  }
package/README.md CHANGED
@@ -1,209 +1,230 @@
1
- # Vision
1
+ # JavaScript Solid Server
2
2
 
3
- The goal of this project is to create a hyper-modern, performant, and minimalist JavaScript Solid server. While drawing inspiration from Node Solid Server (NSS), this new implementation will address its shortcomings and prioritize scalability, modularity, and developer usability.
3
+ A minimal, fast, JSON-LD native Solid server.
4
4
 
5
- ## Key Objectives
5
+ ## Philosophy: JSON-LD First
6
6
 
7
- - **Performance First:** Capable of handling enterprise-scale loads, targeting thousands to millions of users.
8
- - **Minimalist Design:** Remove unused and experimental features; focus on what matters most.
9
- - **Modularity:** Clear separation of identity, authentication, storage, and onboarding.
10
- - **Developer Friendly:** Clean, well-documented, and extensible codebase that adheres to the Solid specification.
11
- - **Modern Tooling:** Leverage async/await, native modules, fast HTTP servers like Fastify, and cutting-edge JavaScript runtimes.
12
- - **HTTP Simplicity:** Prioritize simple HTTP/1.1 compatibility for maximum interoperability.
13
- - **Frontend Agnostic:** Work with any frontend or application layer via standardized APIs.
14
- - **Testable and CI Ready:** Fully integrated with Solid test suites and modern CI/CD pipelines.
7
+ This is a **JSON-LD native implementation**. Unlike traditional Solid servers that treat Turtle as the primary format and convert to/from it, this server:
15
8
 
16
- ---
9
+ - **Stores everything as JSON-LD** - No RDF parsing overhead for standard operations
10
+ - **Serves JSON-LD by default** - Modern web applications can consume responses directly
11
+ - **Content negotiation is optional** - Enable Turtle support with `{ conneg: true }` when needed
12
+ - **Fast by design** - Skip the RDF parsing tax when you don't need it
17
13
 
18
- # ARCHITECTURE
14
+ ### Why JSON-LD First?
19
15
 
20
- ## Overview
16
+ 1. **Performance**: JSON parsing is native to JavaScript - no external RDF libraries needed for basic operations
17
+ 2. **Simplicity**: JSON-LD is valid JSON - works with any JSON tooling
18
+ 3. **Web-native**: Browsers and web apps understand JSON natively
19
+ 4. **Semantic web ready**: JSON-LD is a W3C standard RDF serialization
21
20
 
22
- The architecture is inspired by NSS but modernized and streamlined. Each subsystem is designed to operate independently and follow the single-responsibility principle.
21
+ ### When to Enable Content Negotiation
23
22
 
24
- ### Components
23
+ Enable `conneg: true` when:
24
+ - Interoperating with Turtle-based Solid apps
25
+ - Serving data to legacy Solid clients
26
+ - Running conformance tests that require Turtle support
25
27
 
26
- - **HTTP Layer**
28
+ ```javascript
29
+ import { createServer } from './src/server.js';
27
30
 
28
- - Fastify server
29
- - Routing and middleware based on HTTP verbs and Solid operations
30
- - Blazingly fast, with benchmarks from the start
31
+ // Default: JSON-LD only (fast)
32
+ const server = createServer();
31
33
 
32
- - **Identity Provider (IDP)**
33
-
34
- - Handles Pod based WebIDs
35
- - Handles external WebIDs
36
- - Minimal by default, extendable via plugins
37
-
38
- - **Authenticaion Module (AUthn)**
39
-
40
- - Handles WebID-based authentication, including WebID-TLS
41
- - OIDC-compliant with modular Authentication
42
- - Single sign-on including WebID-TLS
43
-
44
- - **Authorization Module (Authz)**
45
-
46
- - Supports Web Access Control (WAC)
47
- - Token-based permissions model
48
- - Modular Authorization system
49
-
50
- - **Storage Engine**
51
-
52
- - Modular backend adapters (e.g. file system, S3, memory)
53
- - POD-level quota management (optional)
54
- - Interoperable with existing Cloud
55
-
56
- - **Account and Onboarding**
57
- - API-first registration
58
- - Public, private, invite modes
59
- - Extensible account templates
60
-
61
- ### Deployment Model
62
-
63
- - Works as a single binary or serverless function
64
- - Container-friendly (Docker, Deno, etc.)
65
- - CLI for local dev setup and testing
66
-
67
- ### Separation of Concerns
68
-
69
- - Each subsystem lives in its own module/package
70
- - Clear boundaries between IDP and storage
71
- - Frontend-independent API endpoints
72
-
73
- ### Compatibility
74
-
75
- - Solid-compliant, LWS Compliant
76
- - API parity with NSS where applicable
77
- - API parity with CSS where applicable
78
-
79
- ---
80
-
81
- # MVP Implementation
34
+ // With Turtle support (for interoperability)
35
+ const serverWithConneg = createServer({ conneg: true });
36
+ ```
82
37
 
83
- This is a minimal viable product (MVP) implementation of the JavaScriptSolid server. It includes the core components needed to demonstrate the concept while omitting some of the more complex features for simplicity.
38
+ ## Features
39
+
40
+ ### Implemented (v0.0.8)
41
+
42
+ - **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
43
+ - **N3 Patch** - Solid's native patch format for RDF updates
44
+ - **Container Management** - Create, list, and manage containers
45
+ - **Multi-user Pods** - Create pods at `/<username>/`
46
+ - **WebID Profiles** - JSON-LD structured data in HTML at pod root
47
+ - **Web Access Control (WAC)** - `.acl` file-based authorization
48
+ - **Solid-OIDC Resource Server** - Accept DPoP-bound access tokens from external IdPs
49
+ - **Simple Auth Tokens** - Built-in token authentication for development
50
+ - **Content Negotiation** - Optional Turtle <-> JSON-LD conversion
51
+ - **CORS Support** - Full cross-origin resource sharing
52
+
53
+ ### HTTP Methods
54
+
55
+ | Method | Support |
56
+ |--------|---------|
57
+ | GET | Full - Resources and containers |
58
+ | HEAD | Full |
59
+ | PUT | Full - Create/update resources |
60
+ | POST | Full - Create in containers |
61
+ | DELETE | Full |
62
+ | PATCH | N3 Patch format |
63
+ | OPTIONS | Full with CORS |
84
64
 
85
65
  ## Getting Started
86
66
 
87
67
  ### Prerequisites
88
68
 
89
- - Node.js 18 or higher
69
+ - Node.js 18+
90
70
 
91
71
  ### Installation
92
72
 
93
73
  ```bash
94
- # Clone the repository
95
- git clone https://github.com/yourusername/javascript-solid-server.git
96
- cd javascript-solid-server
97
-
98
- # Install dependencies
99
74
  npm install
100
75
  ```
101
76
 
102
- ### Running the Server
77
+ ### Running
103
78
 
104
79
  ```bash
105
- # Start the server
80
+ # Start server (default port 3000)
106
81
  npm start
107
- ```
108
82
 
109
- The server will be available at http://localhost:3000 by default.
83
+ # Development mode with watch
84
+ npm dev
85
+ ```
110
86
 
111
- ## Features Included in MVP
87
+ ### Creating a Pod
112
88
 
113
- - **HTTP Server**: Based on Fastify for high performance
114
- - **Basic Identity Provider**: Simple user registration and login with JWT tokens
115
- - **Simple Authorization**: Basic implementation of WAC (Web Access Control)
116
- - **File-based Storage**: Local filesystem storage for Solid resources
117
- - **Basic Solid Protocol Support**: GET, PUT, DELETE, PATCH, and HEAD operations
89
+ ```bash
90
+ curl -X POST http://localhost:3000/.pods \
91
+ -H "Content-Type: application/json" \
92
+ -d '{"name": "alice"}'
93
+ ```
118
94
 
119
- ## Features Omitted in MVP (to be added later)
95
+ Response:
96
+ ```json
97
+ {
98
+ "name": "alice",
99
+ "webId": "http://localhost:3000/alice/#me",
100
+ "podUri": "http://localhost:3000/alice/",
101
+ "token": "eyJ..."
102
+ }
103
+ ```
120
104
 
121
- 1. WebID-TLS Authentication
122
- 2. Full OIDC implementation
123
- 3. Advanced WAC features and ACL file parsing
124
- 4. Quotas and resource limits
125
- 5. Advanced container management
126
- 6. SPARQL and N3 Patch support
127
- 7. Notification systems
105
+ ### Using the Pod
128
106
 
129
- ## API Usage Examples
107
+ ```bash
108
+ # Read public profile
109
+ curl http://localhost:3000/alice/
130
110
 
131
- ### User Registration
111
+ # Write to pod (with token)
112
+ curl -X PUT http://localhost:3000/alice/public/data.json \
113
+ -H "Authorization: Bearer YOUR_TOKEN" \
114
+ -H "Content-Type: application/ld+json" \
115
+ -d '{"@id": "#data", "http://example.org/value": 42}'
132
116
 
133
- ```bash
134
- curl -X POST http://localhost:3000/register \
135
- -H "Content-Type: application/json" \
136
- -d '{"username": "alice", "password": "secret", "email": "alice@example.com"}'
117
+ # Read back
118
+ curl http://localhost:3000/alice/public/data.json
137
119
  ```
138
120
 
139
- ### Login
121
+ ### PATCH with N3
140
122
 
141
123
  ```bash
142
- curl -X POST http://localhost:3000/login \
143
- -H "Content-Type: application/json" \
144
- -d '{"username": "alice", "password": "secret"}'
124
+ curl -X PATCH http://localhost:3000/alice/public/data.json \
125
+ -H "Authorization: Bearer YOUR_TOKEN" \
126
+ -H "Content-Type: text/n3" \
127
+ -d '@prefix solid: <http://www.w3.org/ns/solid/terms#>.
128
+ _:patch a solid:InsertDeletePatch;
129
+ solid:inserts { <#data> <http://example.org/name> "Updated" }.'
145
130
  ```
146
131
 
147
- ### Accessing Resources
132
+ ## Pod Structure
148
133
 
149
- ```bash
150
- # Get a resource
151
- curl -X GET http://localhost:3000/alice/profile \
152
- -H "Authorization: Bearer YOUR_TOKEN_HERE"
153
-
154
- # Create or update a resource
155
- curl -X PUT http://localhost:3000/alice/profile \
156
- -H "Authorization: Bearer YOUR_TOKEN_HERE" \
157
- -H "Content-Type: text/turtle" \
158
- -d '@prefix foaf: <http://xmlns.com/foaf/0.1/>. <#me> a foaf:Person; foaf:name "Alice".'
134
+ ```
135
+ /alice/
136
+ ├── index.html # WebID profile (HTML with JSON-LD)
137
+ ├── .acl # Root ACL (owner + public read)
138
+ ├── inbox/ # Notifications (public append)
139
+ │ └── .acl
140
+ ├── public/ # Public files
141
+ ├── private/ # Private files (owner only)
142
+ │ └── .acl
143
+ └── settings/ # User preferences (owner only)
144
+ ├── .acl
145
+ ├── prefs
146
+ ├── publicTypeIndex
147
+ └── privateTypeIndex
159
148
  ```
160
149
 
161
- ## Performance Benchmarking
150
+ ## Authentication
162
151
 
163
- This project includes a comprehensive benchmarking tool to measure server performance under various loads.
152
+ ### Simple Tokens (Development)
164
153
 
165
- ### Running the Benchmark
154
+ Use the token returned from pod creation:
166
155
 
167
156
  ```bash
168
- # Make sure the server is running in a separate terminal
169
- npm start
170
-
171
- # In another terminal, run the benchmark
172
- npm run benchmark
157
+ curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/alice/private/
173
158
  ```
174
159
 
175
- The benchmark will:
160
+ ### Solid-OIDC (Production)
161
+
162
+ The server accepts DPoP-bound access tokens from external Solid identity providers:
163
+
164
+ ```bash
165
+ curl -H "Authorization: DPoP ACCESS_TOKEN" \
166
+ -H "DPoP: DPOP_PROOF" \
167
+ http://localhost:3000/alice/private/
168
+ ```
176
169
 
177
- 1. Create multiple test users
178
- 2. Execute various operations (register, login, read, write, delete)
179
- 3. Measure response times for each operation type
180
- 4. Test different concurrency levels (1, 5, 10, 50, 100 users)
181
- 5. Calculate throughput (operations per second)
170
+ ## Configuration
182
171
 
183
- ### Visualizing Results
172
+ ```javascript
173
+ createServer({
174
+ logger: true, // Enable Fastify logging (default: true)
175
+ conneg: false // Enable content negotiation (default: false)
176
+ });
177
+ ```
184
178
 
185
- After running the benchmark, you can generate a visual report:
179
+ ## Running Tests
186
180
 
187
181
  ```bash
188
- # Generate an HTML report with charts
189
- npm run visualize benchmark-report-[timestamp].json
182
+ npm test
190
183
  ```
191
184
 
192
- Open the generated HTML file in a browser to see:
185
+ Currently passing: **105 tests**
186
+
187
+ ## Project Structure
193
188
 
194
- - Average response times for each operation type
195
- - Throughput metrics at different concurrency levels
196
- - Visual charts for easy performance analysis
189
+ ```
190
+ src/
191
+ ├── index.js # Entry point
192
+ ├── server.js # Fastify setup
193
+ ├── handlers/
194
+ │ ├── resource.js # GET, PUT, DELETE, HEAD, PATCH
195
+ │ └── container.js # POST, pod creation
196
+ ├── storage/
197
+ │ └── filesystem.js # File operations
198
+ ├── auth/
199
+ │ ├── middleware.js # Auth hook
200
+ │ ├── token.js # Simple token auth
201
+ │ └── solid-oidc.js # DPoP verification
202
+ ├── wac/
203
+ │ ├── parser.js # ACL parsing
204
+ │ └── checker.js # Permission checking
205
+ ├── ldp/
206
+ │ ├── headers.js # LDP Link headers
207
+ │ └── container.js # Container JSON-LD
208
+ ├── webid/
209
+ │ └── profile.js # WebID generation
210
+ ├── patch/
211
+ │ └── n3-patch.js # N3 Patch support
212
+ ├── rdf/
213
+ │ ├── turtle.js # Turtle <-> JSON-LD
214
+ │ └── conneg.js # Content negotiation
215
+ └── utils/
216
+ └── url.js # URL utilities
217
+ ```
197
218
 
198
- ### Customizing Benchmarks
219
+ ## Dependencies
199
220
 
200
- You can customize the benchmark parameters in `benchmark.js`:
221
+ Minimal dependencies for a fast, secure server:
201
222
 
202
- - Concurrent users levels
203
- - Operations per user
204
- - Test duration
205
- - Test user credentials
223
+ - **fastify** - High-performance HTTP server
224
+ - **fs-extra** - Enhanced file operations
225
+ - **jose** - JWT/JWK handling for Solid-OIDC
226
+ - **n3** - Turtle parsing (only used when conneg enabled)
206
227
 
207
- ## Contributing
228
+ ## License
208
229
 
209
- Contributions are welcome! Please feel free to submit a Pull Request.
230
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -10,9 +10,11 @@
10
10
  "test": "node --test --test-concurrency=1"
11
11
  },
12
12
  "dependencies": {
13
+ "@fastify/websocket": "^8.3.1",
13
14
  "fastify": "^4.25.2",
14
15
  "fs-extra": "^11.2.0",
15
- "jose": "^6.1.3"
16
+ "jose": "^6.1.3",
17
+ "n3": "^1.26.0"
16
18
  },
17
19
  "engines": {
18
20
  "node": ">=18.0.0"
@@ -4,6 +4,8 @@ import { isContainer } from '../utils/url.js';
4
4
  import { generateProfile, generatePreferences, generateTypeIndex, serialize } from '../webid/profile.js';
5
5
  import { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, serializeAcl } from '../wac/parser.js';
6
6
  import { createToken } from '../auth/token.js';
7
+ import { canAcceptInput, toJsonLd, getVaryHeader, RDF_TYPES } from '../rdf/conneg.js';
8
+ import { emitChange } from '../notifications/events.js';
7
9
 
8
10
  /**
9
11
  * Handle POST request to container (create new resource)
@@ -16,6 +18,19 @@ export async function handlePost(request, reply) {
16
18
  return reply.code(405).send({ error: 'POST only allowed on containers' });
17
19
  }
18
20
 
21
+ const connegEnabled = request.connegEnabled || false;
22
+ const contentType = request.headers['content-type'] || '';
23
+
24
+ // Check if we can accept this input type
25
+ if (!canAcceptInput(contentType, connegEnabled)) {
26
+ return reply.code(415).send({
27
+ error: 'Unsupported Media Type',
28
+ message: connegEnabled
29
+ ? 'Supported types: application/ld+json, text/turtle, text/n3'
30
+ : 'Supported type: application/ld+json (enable conneg for Turtle support)'
31
+ });
32
+ }
33
+
19
34
  // Check container exists
20
35
  const stats = await storage.stat(urlPath);
21
36
  if (!stats || !stats.isDirectory) {
@@ -33,6 +48,7 @@ export async function handlePost(request, reply) {
33
48
  // Generate unique filename
34
49
  const filename = await storage.generateUniqueFilename(urlPath, slug, isCreatingContainer);
35
50
  const newPath = urlPath + filename + (isCreatingContainer ? '/' : '');
51
+ const resourceUrl = `${request.protocol}://${request.hostname}${newPath}`;
36
52
 
37
53
  let success;
38
54
  if (isCreatingContainer) {
@@ -49,6 +65,21 @@ export async function handlePost(request, reply) {
49
65
  } else {
50
66
  content = Buffer.from('');
51
67
  }
68
+
69
+ // Convert Turtle/N3 to JSON-LD if conneg enabled
70
+ const inputType = contentType.split(';')[0].trim().toLowerCase();
71
+ if (connegEnabled && (inputType === RDF_TYPES.TURTLE || inputType === RDF_TYPES.N3)) {
72
+ try {
73
+ const jsonLd = await toJsonLd(content, contentType, resourceUrl, connegEnabled);
74
+ content = Buffer.from(JSON.stringify(jsonLd, null, 2));
75
+ } catch (e) {
76
+ return reply.code(400).send({
77
+ error: 'Bad Request',
78
+ message: 'Invalid Turtle/N3 format: ' + e.message
79
+ });
80
+ }
81
+ }
82
+
52
83
  success = await storage.write(newPath, content);
53
84
  }
54
85
 
@@ -56,16 +87,23 @@ export async function handlePost(request, reply) {
56
87
  return reply.code(500).send({ error: 'Create failed' });
57
88
  }
58
89
 
59
- const location = `${request.protocol}://${request.hostname}${newPath}`;
60
90
  const origin = request.headers.origin;
61
91
 
62
92
  const headers = getAllHeaders({
63
93
  isContainer: isCreatingContainer,
64
- origin
94
+ origin,
95
+ connegEnabled
65
96
  });
66
- headers['Location'] = location;
97
+ headers['Location'] = resourceUrl;
98
+ headers['Vary'] = getVaryHeader(connegEnabled);
67
99
 
68
100
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
101
+
102
+ // Emit change notification for WebSocket subscribers
103
+ if (request.notificationsEnabled) {
104
+ emitChange(resourceUrl);
105
+ }
106
+
69
107
  return reply.code(201).send();
70
108
  }
71
109
 
@@ -3,6 +3,15 @@ import { getAllHeaders } from '../ldp/headers.js';
3
3
  import { generateContainerJsonLd, serializeJsonLd } from '../ldp/container.js';
4
4
  import { isContainer, getContentType, isRdfContentType } from '../utils/url.js';
5
5
  import { parseN3Patch, applyN3Patch, validatePatch } from '../patch/n3-patch.js';
6
+ import {
7
+ selectContentType,
8
+ canAcceptInput,
9
+ toJsonLd,
10
+ fromJsonLd,
11
+ getVaryHeader,
12
+ RDF_TYPES
13
+ } from '../rdf/conneg.js';
14
+ import { emitChange } from '../notifications/events.js';
6
15
 
7
16
  /**
8
17
  * Handle GET request
@@ -20,6 +29,8 @@ export async function handleGet(request, reply) {
20
29
 
21
30
  // Handle container
22
31
  if (stats.isDirectory) {
32
+ const connegEnabled = request.connegEnabled || false;
33
+
23
34
  // Check for index.html (serves as both profile and container representation)
24
35
  const indexPath = urlPath.endsWith('/') ? `${urlPath}index.html` : `${urlPath}/index.html`;
25
36
  const indexExists = await storage.exists(indexPath);
@@ -34,7 +45,8 @@ export async function handleGet(request, reply) {
34
45
  etag: indexStats?.etag || stats.etag,
35
46
  contentType: 'text/html',
36
47
  origin,
37
- resourceUrl
48
+ resourceUrl,
49
+ connegEnabled
38
50
  });
39
51
 
40
52
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -50,7 +62,8 @@ export async function handleGet(request, reply) {
50
62
  etag: stats.etag,
51
63
  contentType: 'application/ld+json',
52
64
  origin,
53
- resourceUrl
65
+ resourceUrl,
66
+ connegEnabled
54
67
  });
55
68
 
56
69
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -63,14 +76,54 @@ export async function handleGet(request, reply) {
63
76
  return reply.code(500).send({ error: 'Read error' });
64
77
  }
65
78
 
66
- const contentType = getContentType(urlPath);
79
+ const storedContentType = getContentType(urlPath);
80
+ const connegEnabled = request.connegEnabled || false;
81
+
82
+ // Content negotiation for RDF resources
83
+ if (connegEnabled && isRdfContentType(storedContentType)) {
84
+ try {
85
+ // Parse stored content as JSON-LD
86
+ const jsonLd = JSON.parse(content.toString());
87
+
88
+ // Select output format based on Accept header
89
+ const acceptHeader = request.headers.accept;
90
+ const targetType = selectContentType(acceptHeader, connegEnabled);
91
+
92
+ // Convert to requested format
93
+ const { content: outputContent, contentType: outputType } = await fromJsonLd(
94
+ jsonLd,
95
+ targetType,
96
+ resourceUrl,
97
+ connegEnabled
98
+ );
99
+
100
+ const headers = getAllHeaders({
101
+ isContainer: false,
102
+ etag: stats.etag,
103
+ contentType: outputType,
104
+ origin,
105
+ resourceUrl,
106
+ connegEnabled
107
+ });
108
+ headers['Vary'] = getVaryHeader(connegEnabled);
109
+
110
+ Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
111
+ return reply.send(outputContent);
112
+ } catch (e) {
113
+ // If not valid JSON-LD, serve as-is
114
+ }
115
+ }
116
+
117
+ // Serve content as-is (no conneg or non-RDF resource)
67
118
  const headers = getAllHeaders({
68
119
  isContainer: false,
69
120
  etag: stats.etag,
70
- contentType,
121
+ contentType: storedContentType,
71
122
  origin,
72
- resourceUrl
123
+ resourceUrl,
124
+ connegEnabled
73
125
  });
126
+ headers['Vary'] = getVaryHeader(connegEnabled);
74
127
 
75
128
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
76
129
  return reply.send(content);
@@ -118,6 +171,20 @@ export async function handlePut(request, reply) {
118
171
  return reply.code(409).send({ error: 'Cannot PUT to container. Use POST instead.' });
119
172
  }
120
173
 
174
+ const connegEnabled = request.connegEnabled || false;
175
+ const contentType = request.headers['content-type'] || '';
176
+ const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
177
+
178
+ // Check if we can accept this input type
179
+ if (!canAcceptInput(contentType, connegEnabled)) {
180
+ return reply.code(415).send({
181
+ error: 'Unsupported Media Type',
182
+ message: connegEnabled
183
+ ? 'Supported types: application/ld+json, text/turtle, text/n3'
184
+ : 'Supported type: application/ld+json (enable conneg for Turtle support)'
185
+ });
186
+ }
187
+
121
188
  // Check if resource already exists
122
189
  const existed = await storage.exists(urlPath);
123
190
 
@@ -135,17 +202,37 @@ export async function handlePut(request, reply) {
135
202
  content = Buffer.from('');
136
203
  }
137
204
 
205
+ // Convert Turtle/N3 to JSON-LD if conneg enabled
206
+ const inputType = contentType.split(';')[0].trim().toLowerCase();
207
+ if (connegEnabled && (inputType === RDF_TYPES.TURTLE || inputType === RDF_TYPES.N3)) {
208
+ try {
209
+ const jsonLd = await toJsonLd(content, contentType, resourceUrl, connegEnabled);
210
+ content = Buffer.from(JSON.stringify(jsonLd, null, 2));
211
+ } catch (e) {
212
+ return reply.code(400).send({
213
+ error: 'Bad Request',
214
+ message: 'Invalid Turtle/N3 format: ' + e.message
215
+ });
216
+ }
217
+ }
218
+
138
219
  const success = await storage.write(urlPath, content);
139
220
  if (!success) {
140
221
  return reply.code(500).send({ error: 'Write failed' });
141
222
  }
142
223
 
143
224
  const origin = request.headers.origin;
144
- const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
145
- const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
225
+ const headers = getAllHeaders({ isContainer: false, origin, resourceUrl, connegEnabled });
146
226
  headers['Location'] = resourceUrl;
227
+ headers['Vary'] = getVaryHeader(connegEnabled);
147
228
 
148
229
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
230
+
231
+ // Emit change notification for WebSocket subscribers
232
+ if (request.notificationsEnabled) {
233
+ emitChange(resourceUrl);
234
+ }
235
+
149
236
  return reply.code(existed ? 204 : 201).send();
150
237
  }
151
238
 
@@ -170,6 +257,11 @@ export async function handleDelete(request, reply) {
170
257
  const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
171
258
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
172
259
 
260
+ // Emit change notification for WebSocket subscribers
261
+ if (request.notificationsEnabled) {
262
+ emitChange(resourceUrl);
263
+ }
264
+
173
265
  return reply.code(204).send();
174
266
  }
175
267
 
@@ -285,5 +377,10 @@ export async function handlePatch(request, reply) {
285
377
  const headers = getAllHeaders({ isContainer: false, origin, resourceUrl });
286
378
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
287
379
 
380
+ // Emit change notification for WebSocket subscribers
381
+ if (request.notificationsEnabled) {
382
+ emitChange(resourceUrl);
383
+ }
384
+
288
385
  return reply.code(204).send();
289
386
  }