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.
- package/.claude/settings.local.json +3 -1
- package/README.md +167 -146
- package/package.json +4 -2
- package/src/handlers/container.js +41 -3
- package/src/handlers/resource.js +104 -7
- package/src/ldp/headers.js +16 -9
- package/src/notifications/events.js +22 -0
- package/src/notifications/index.js +49 -0
- package/src/notifications/websocket.js +183 -0
- package/src/rdf/conneg.js +215 -0
- package/src/rdf/turtle.js +411 -0
- package/src/server.js +29 -0
- package/test/conneg.test.js +289 -0
- package/test/helpers.js +4 -2
- package/test/notifications.test.js +348 -0
package/README.md
CHANGED
|
@@ -1,209 +1,230 @@
|
|
|
1
|
-
#
|
|
1
|
+
# JavaScript Solid Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A minimal, fast, JSON-LD native Solid server.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Philosophy: JSON-LD First
|
|
6
6
|
|
|
7
|
-
|
|
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
|
-
|
|
14
|
+
### Why JSON-LD First?
|
|
19
15
|
|
|
20
|
-
|
|
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
|
-
|
|
21
|
+
### When to Enable Content Negotiation
|
|
23
22
|
|
|
24
|
-
|
|
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
|
-
|
|
28
|
+
```javascript
|
|
29
|
+
import { createServer } from './src/server.js';
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
- Blazingly fast, with benchmarks from the start
|
|
31
|
+
// Default: JSON-LD only (fast)
|
|
32
|
+
const server = createServer();
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
77
|
+
### Running
|
|
103
78
|
|
|
104
79
|
```bash
|
|
105
|
-
# Start
|
|
80
|
+
# Start server (default port 3000)
|
|
106
81
|
npm start
|
|
107
|
-
```
|
|
108
82
|
|
|
109
|
-
|
|
83
|
+
# Development mode with watch
|
|
84
|
+
npm dev
|
|
85
|
+
```
|
|
110
86
|
|
|
111
|
-
|
|
87
|
+
### Creating a Pod
|
|
112
88
|
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
-
|
|
116
|
-
-
|
|
117
|
-
|
|
89
|
+
```bash
|
|
90
|
+
curl -X POST http://localhost:3000/.pods \
|
|
91
|
+
-H "Content-Type: application/json" \
|
|
92
|
+
-d '{"name": "alice"}'
|
|
93
|
+
```
|
|
118
94
|
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
107
|
+
```bash
|
|
108
|
+
# Read public profile
|
|
109
|
+
curl http://localhost:3000/alice/
|
|
130
110
|
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
curl
|
|
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
|
-
###
|
|
121
|
+
### PATCH with N3
|
|
140
122
|
|
|
141
123
|
```bash
|
|
142
|
-
curl -X
|
|
143
|
-
-H "
|
|
144
|
-
-
|
|
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
|
-
|
|
132
|
+
## Pod Structure
|
|
148
133
|
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
##
|
|
150
|
+
## Authentication
|
|
162
151
|
|
|
163
|
-
|
|
152
|
+
### Simple Tokens (Development)
|
|
164
153
|
|
|
165
|
-
|
|
154
|
+
Use the token returned from pod creation:
|
|
166
155
|
|
|
167
156
|
```bash
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
+
## Running Tests
|
|
186
180
|
|
|
187
181
|
```bash
|
|
188
|
-
|
|
189
|
-
npm run visualize benchmark-report-[timestamp].json
|
|
182
|
+
npm test
|
|
190
183
|
```
|
|
191
184
|
|
|
192
|
-
|
|
185
|
+
Currently passing: **105 tests**
|
|
186
|
+
|
|
187
|
+
## Project Structure
|
|
193
188
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
219
|
+
## Dependencies
|
|
199
220
|
|
|
200
|
-
|
|
221
|
+
Minimal dependencies for a fast, secure server:
|
|
201
222
|
|
|
202
|
-
-
|
|
203
|
-
-
|
|
204
|
-
-
|
|
205
|
-
-
|
|
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
|
-
##
|
|
228
|
+
## License
|
|
208
229
|
|
|
209
|
-
|
|
230
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
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'] =
|
|
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
|
|
package/src/handlers/resource.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
}
|