javascript-solid-server 0.0.9 → 0.0.11

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/package.json CHANGED
@@ -1,16 +1,29 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
+ "bin": {
8
+ "jss": "./bin/jss.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/JavaScriptSolidServer/JavaScriptSolidServer.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/JavaScriptSolidServer/JavaScriptSolidServer/issues"
16
+ },
17
+ "homepage": "https://github.com/JavaScriptSolidServer/JavaScriptSolidServer#readme",
7
18
  "scripts": {
8
- "start": "node src/index.js",
9
- "dev": "node --watch src/index.js",
10
- "test": "node --test --test-concurrency=1"
19
+ "start": "node bin/jss.js start",
20
+ "dev": "node --watch bin/jss.js start",
21
+ "test": "node --test --test-concurrency=1",
22
+ "benchmark": "node benchmark.js"
11
23
  },
12
24
  "dependencies": {
13
25
  "@fastify/websocket": "^8.3.1",
26
+ "commander": "^14.0.2",
14
27
  "fastify": "^4.25.2",
15
28
  "fs-extra": "^11.2.0",
16
29
  "jose": "^6.1.3",
@@ -25,5 +38,8 @@
25
38
  "linked-data",
26
39
  "decentralized"
27
40
  ],
28
- "license": "MIT"
41
+ "license": "MIT",
42
+ "devDependencies": {
43
+ "autocannon": "^8.0.0"
44
+ }
29
45
  }
package/src/config.js ADDED
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Configuration Loading
3
+ *
4
+ * Loads config from (in order of precedence):
5
+ * 1. CLI arguments (highest)
6
+ * 2. Environment variables (JSS_*)
7
+ * 3. Config file (config.json)
8
+ * 4. Defaults (lowest)
9
+ */
10
+
11
+ import fs from 'fs-extra';
12
+ import path from 'path';
13
+
14
+ /**
15
+ * Default configuration values
16
+ */
17
+ export const defaults = {
18
+ // Server
19
+ port: 3000,
20
+ host: '0.0.0.0',
21
+ root: './data',
22
+
23
+ // SSL
24
+ sslKey: null,
25
+ sslCert: null,
26
+
27
+ // Features
28
+ multiuser: true,
29
+ conneg: false,
30
+ notifications: false,
31
+
32
+ // Logging
33
+ logger: true,
34
+ quiet: false,
35
+
36
+ // Paths
37
+ configPath: './.jss',
38
+ };
39
+
40
+ /**
41
+ * Map of environment variable names to config keys
42
+ */
43
+ const envMap = {
44
+ JSS_PORT: 'port',
45
+ JSS_HOST: 'host',
46
+ JSS_ROOT: 'root',
47
+ JSS_SSL_KEY: 'sslKey',
48
+ JSS_SSL_CERT: 'sslCert',
49
+ JSS_MULTIUSER: 'multiuser',
50
+ JSS_CONNEG: 'conneg',
51
+ JSS_NOTIFICATIONS: 'notifications',
52
+ JSS_QUIET: 'quiet',
53
+ JSS_CONFIG_PATH: 'configPath',
54
+ };
55
+
56
+ /**
57
+ * Parse a value from environment variable string
58
+ */
59
+ function parseEnvValue(value, key) {
60
+ if (value === undefined) return undefined;
61
+
62
+ // Boolean values
63
+ if (value.toLowerCase() === 'true') return true;
64
+ if (value.toLowerCase() === 'false') return false;
65
+
66
+ // Numeric values for known numeric keys
67
+ if (key === 'port' && !isNaN(value)) {
68
+ return parseInt(value, 10);
69
+ }
70
+
71
+ return value;
72
+ }
73
+
74
+ /**
75
+ * Load configuration from environment variables
76
+ */
77
+ function loadEnvConfig() {
78
+ const config = {};
79
+
80
+ for (const [envVar, configKey] of Object.entries(envMap)) {
81
+ const value = process.env[envVar];
82
+ if (value !== undefined) {
83
+ config[configKey] = parseEnvValue(value, configKey);
84
+ }
85
+ }
86
+
87
+ return config;
88
+ }
89
+
90
+ /**
91
+ * Load configuration from a JSON file
92
+ */
93
+ async function loadFileConfig(configFile) {
94
+ if (!configFile) return {};
95
+
96
+ try {
97
+ const fullPath = path.resolve(configFile);
98
+ if (await fs.pathExists(fullPath)) {
99
+ const content = await fs.readFile(fullPath, 'utf8');
100
+ return JSON.parse(content);
101
+ }
102
+ } catch (e) {
103
+ console.error(`Warning: Failed to load config file: ${e.message}`);
104
+ }
105
+
106
+ return {};
107
+ }
108
+
109
+ /**
110
+ * Merge configuration sources
111
+ * @param {object} cliOptions - Options from command line
112
+ * @param {string} configFile - Path to config file (optional)
113
+ * @returns {Promise<object>} Merged configuration
114
+ */
115
+ export async function loadConfig(cliOptions = {}, configFile = null) {
116
+ // Load from file first
117
+ const fileConfig = await loadFileConfig(configFile || cliOptions.config);
118
+
119
+ // Load from environment
120
+ const envConfig = loadEnvConfig();
121
+
122
+ // Merge in order: defaults < file < env < cli
123
+ const config = {
124
+ ...defaults,
125
+ ...fileConfig,
126
+ ...envConfig,
127
+ ...filterUndefined(cliOptions),
128
+ };
129
+
130
+ // Derive additional settings
131
+ if (config.quiet) {
132
+ config.logger = false;
133
+ }
134
+
135
+ // Validate SSL config
136
+ if ((config.sslKey && !config.sslCert) || (!config.sslKey && config.sslCert)) {
137
+ throw new Error('Both --ssl-key and --ssl-cert must be provided together');
138
+ }
139
+
140
+ config.ssl = !!(config.sslKey && config.sslCert);
141
+
142
+ return config;
143
+ }
144
+
145
+ /**
146
+ * Filter out undefined values from an object
147
+ */
148
+ function filterUndefined(obj) {
149
+ const result = {};
150
+ for (const [key, value] of Object.entries(obj)) {
151
+ if (value !== undefined) {
152
+ result[key] = value;
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+
158
+ /**
159
+ * Save configuration to a file
160
+ */
161
+ export async function saveConfig(config, configFile) {
162
+ const toSave = { ...config };
163
+ // Remove derived/runtime values
164
+ delete toSave.ssl;
165
+ delete toSave.logger;
166
+
167
+ await fs.ensureDir(path.dirname(configFile));
168
+ await fs.writeFile(configFile, JSON.stringify(toSave, null, 2));
169
+ }
170
+
171
+ /**
172
+ * Print configuration (for debugging)
173
+ */
174
+ export function printConfig(config) {
175
+ console.log('\nConfiguration:');
176
+ console.log('─'.repeat(40));
177
+ console.log(` Port: ${config.port}`);
178
+ console.log(` Host: ${config.host}`);
179
+ console.log(` Root: ${path.resolve(config.root)}`);
180
+ console.log(` SSL: ${config.ssl ? 'enabled' : 'disabled'}`);
181
+ console.log(` Multi-user: ${config.multiuser}`);
182
+ console.log(` Conneg: ${config.conneg}`);
183
+ console.log(` Notifications: ${config.notifications}`);
184
+ console.log('─'.repeat(40));
185
+ }
@@ -3,6 +3,7 @@ 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 { parseSparqlUpdate, applySparqlUpdate } from '../patch/sparql-update.js';
6
7
  import {
7
8
  selectContentType,
8
9
  canAcceptInput,
@@ -12,6 +13,7 @@ import {
12
13
  RDF_TYPES
13
14
  } from '../rdf/conneg.js';
14
15
  import { emitChange } from '../notifications/events.js';
16
+ import { checkIfMatch, checkIfNoneMatchForGet, checkIfNoneMatchForWrite } from '../utils/conditional.js';
15
17
 
16
18
  /**
17
19
  * Handle GET request
@@ -24,6 +26,15 @@ export async function handleGet(request, reply) {
24
26
  return reply.code(404).send({ error: 'Not Found' });
25
27
  }
26
28
 
29
+ // Check If-None-Match for conditional GET (304 Not Modified)
30
+ const ifNoneMatch = request.headers['if-none-match'];
31
+ if (ifNoneMatch) {
32
+ const check = checkIfNoneMatchForGet(ifNoneMatch, stats.etag);
33
+ if (!check.ok && check.notModified) {
34
+ return reply.code(304).send();
35
+ }
36
+ }
37
+
27
38
  const origin = request.headers.origin;
28
39
  const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
29
40
 
@@ -185,8 +196,28 @@ export async function handlePut(request, reply) {
185
196
  });
186
197
  }
187
198
 
188
- // Check if resource already exists
189
- const existed = await storage.exists(urlPath);
199
+ // Check if resource already exists and get current ETag
200
+ const stats = await storage.stat(urlPath);
201
+ const existed = stats !== null;
202
+ const currentEtag = stats?.etag || null;
203
+
204
+ // Check If-Match header (for safe updates)
205
+ const ifMatch = request.headers['if-match'];
206
+ if (ifMatch) {
207
+ const check = checkIfMatch(ifMatch, currentEtag);
208
+ if (!check.ok) {
209
+ return reply.code(check.status).send({ error: check.error });
210
+ }
211
+ }
212
+
213
+ // Check If-None-Match header (for create-only semantics)
214
+ const ifNoneMatch = request.headers['if-none-match'];
215
+ if (ifNoneMatch) {
216
+ const check = checkIfNoneMatchForWrite(ifNoneMatch, currentEtag);
217
+ if (!check.ok) {
218
+ return reply.code(check.status).send({ error: check.error });
219
+ }
220
+ }
190
221
 
191
222
  // Get content from request body
192
223
  let content = request.body;
@@ -242,11 +273,21 @@ export async function handlePut(request, reply) {
242
273
  export async function handleDelete(request, reply) {
243
274
  const urlPath = request.url.split('?')[0];
244
275
 
245
- const existed = await storage.exists(urlPath);
246
- if (!existed) {
276
+ // Check if resource exists and get current ETag
277
+ const stats = await storage.stat(urlPath);
278
+ if (!stats) {
247
279
  return reply.code(404).send({ error: 'Not Found' });
248
280
  }
249
281
 
282
+ // Check If-Match header (for safe deletes)
283
+ const ifMatch = request.headers['if-match'];
284
+ if (ifMatch) {
285
+ const check = checkIfMatch(ifMatch, stats.etag);
286
+ if (!check.ok) {
287
+ return reply.code(check.status).send({ error: check.error });
288
+ }
289
+ }
290
+
250
291
  const success = await storage.remove(urlPath);
251
292
  if (!success) {
252
293
  return reply.code(500).send({ error: 'Delete failed' });
@@ -274,10 +315,12 @@ export async function handleOptions(request, reply) {
274
315
 
275
316
  const origin = request.headers.origin;
276
317
  const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
318
+ const connegEnabled = request.connegEnabled || false;
277
319
  const headers = getAllHeaders({
278
320
  isContainer: stats?.isDirectory || isContainer(urlPath),
279
321
  origin,
280
- resourceUrl
322
+ resourceUrl,
323
+ connegEnabled
281
324
  });
282
325
 
283
326
  Object.entries(headers).forEach(([k, v]) => reply.header(k, v));
@@ -286,7 +329,7 @@ export async function handleOptions(request, reply) {
286
329
 
287
330
  /**
288
331
  * Handle PATCH request
289
- * Supports N3 Patch format (text/n3) for updating RDF resources
332
+ * Supports N3 Patch format (text/n3) and SPARQL Update for updating RDF resources
290
333
  */
291
334
  export async function handlePatch(request, reply) {
292
335
  const urlPath = request.url.split('?')[0];
@@ -298,14 +341,13 @@ export async function handlePatch(request, reply) {
298
341
 
299
342
  // Check content type
300
343
  const contentType = request.headers['content-type'] || '';
301
- const isN3Patch = contentType.includes('text/n3') ||
302
- contentType.includes('application/n3') ||
303
- contentType.includes('application/sparql-update');
344
+ const isN3Patch = contentType.includes('text/n3') || contentType.includes('application/n3');
345
+ const isSparqlUpdate = contentType.includes('application/sparql-update');
304
346
 
305
- if (!isN3Patch) {
347
+ if (!isN3Patch && !isSparqlUpdate) {
306
348
  return reply.code(415).send({
307
349
  error: 'Unsupported Media Type',
308
- message: 'PATCH requires Content-Type: text/n3 for N3 Patch format'
350
+ message: 'PATCH requires Content-Type: text/n3 (N3 Patch) or application/sparql-update (SPARQL Update)'
309
351
  });
310
352
  }
311
353
 
@@ -315,6 +357,15 @@ export async function handlePatch(request, reply) {
315
357
  return reply.code(404).send({ error: 'Not Found' });
316
358
  }
317
359
 
360
+ // Check If-Match header (for safe updates)
361
+ const ifMatch = request.headers['if-match'];
362
+ if (ifMatch) {
363
+ const check = checkIfMatch(ifMatch, stats.etag);
364
+ if (!check.ok) {
365
+ return reply.code(check.status).send({ error: check.error });
366
+ }
367
+ }
368
+
318
369
  // Read existing content
319
370
  const existingContent = await storage.read(urlPath);
320
371
  if (existingContent === null) {
@@ -338,31 +389,49 @@ export async function handlePatch(request, reply) {
338
389
  : request.body;
339
390
 
340
391
  const resourceUrl = `${request.protocol}://${request.hostname}${urlPath}`;
341
- let patch;
342
- try {
343
- patch = parseN3Patch(patchContent, resourceUrl);
344
- } catch (e) {
345
- return reply.code(400).send({
346
- error: 'Bad Request',
347
- message: 'Invalid N3 Patch format: ' + e.message
348
- });
349
- }
350
392
 
351
- // Validate that deletes exist (optional strict mode)
352
- // const validation = validatePatch(document, patch, resourceUrl);
353
- // if (!validation.valid) {
354
- // return reply.code(409).send({ error: 'Conflict', message: validation.error });
355
- // }
356
-
357
- // Apply the patch
358
393
  let updatedDocument;
359
- try {
360
- updatedDocument = applyN3Patch(document, patch, resourceUrl);
361
- } catch (e) {
362
- return reply.code(409).send({
363
- error: 'Conflict',
364
- message: 'Failed to apply patch: ' + e.message
365
- });
394
+
395
+ if (isSparqlUpdate) {
396
+ // Handle SPARQL Update
397
+ let update;
398
+ try {
399
+ update = parseSparqlUpdate(patchContent, resourceUrl);
400
+ } catch (e) {
401
+ return reply.code(400).send({
402
+ error: 'Bad Request',
403
+ message: 'Invalid SPARQL Update: ' + e.message
404
+ });
405
+ }
406
+
407
+ try {
408
+ updatedDocument = applySparqlUpdate(document, update, resourceUrl);
409
+ } catch (e) {
410
+ return reply.code(409).send({
411
+ error: 'Conflict',
412
+ message: 'Failed to apply SPARQL Update: ' + e.message
413
+ });
414
+ }
415
+ } else {
416
+ // Handle N3 Patch
417
+ let patch;
418
+ try {
419
+ patch = parseN3Patch(patchContent, resourceUrl);
420
+ } catch (e) {
421
+ return reply.code(400).send({
422
+ error: 'Bad Request',
423
+ message: 'Invalid N3 Patch format: ' + e.message
424
+ });
425
+ }
426
+
427
+ try {
428
+ updatedDocument = applyN3Patch(document, patch, resourceUrl);
429
+ } catch (e) {
430
+ return reply.code(409).send({
431
+ error: 'Conflict',
432
+ message: 'Failed to apply patch: ' + e.message
433
+ });
434
+ }
366
435
  }
367
436
 
368
437
  // Write updated document