javascript-solid-server 0.0.10 → 0.0.12
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 +11 -1
- package/README.md +110 -9
- package/bin/jss.js +226 -0
- package/package.json +11 -4
- package/src/config.js +192 -0
- package/src/handlers/container.js +35 -2
- package/src/handlers/resource.js +3 -1
- package/src/idp/accounts.js +258 -0
- package/src/idp/adapter.js +204 -0
- package/src/idp/index.js +118 -0
- package/src/idp/interactions.js +180 -0
- package/src/idp/keys.js +157 -0
- package/src/idp/provider.js +246 -0
- package/src/idp/views.js +295 -0
- package/src/server.js +39 -12
- package/test/conformance.test.js +349 -0
- package/test/idp.test.js +258 -0
|
@@ -15,7 +15,17 @@
|
|
|
15
15
|
"Bash(npm test:*)",
|
|
16
16
|
"Bash(git add:*)",
|
|
17
17
|
"WebFetch(domain:solid.github.io)",
|
|
18
|
-
"Bash(node:*)"
|
|
18
|
+
"Bash(node:*)",
|
|
19
|
+
"WebFetch(domain:solidservers.org)",
|
|
20
|
+
"WebFetch(domain:solid-contrib.github.io)",
|
|
21
|
+
"Bash(git clone:*)",
|
|
22
|
+
"Bash(chmod:*)",
|
|
23
|
+
"Bash(JSS_PORT=4000 JSS_CONNEG=true node bin/jss.js:*)",
|
|
24
|
+
"Bash(find:*)",
|
|
25
|
+
"Bash(timeout 5 node:*)",
|
|
26
|
+
"Bash(npm view:*)",
|
|
27
|
+
"Bash(npm ls:*)",
|
|
28
|
+
"Bash(timeout 10 node:*)"
|
|
19
29
|
]
|
|
20
30
|
}
|
|
21
31
|
}
|
package/README.md
CHANGED
|
@@ -54,17 +54,20 @@ npm run benchmark
|
|
|
54
54
|
|
|
55
55
|
## Features
|
|
56
56
|
|
|
57
|
-
### Implemented (v0.0.
|
|
57
|
+
### Implemented (v0.0.12)
|
|
58
58
|
|
|
59
59
|
- **LDP CRUD Operations** - GET, PUT, POST, DELETE, HEAD
|
|
60
60
|
- **N3 Patch** - Solid's native patch format for RDF updates
|
|
61
61
|
- **SPARQL Update** - Standard SPARQL UPDATE protocol for PATCH
|
|
62
62
|
- **Conditional Requests** - If-Match/If-None-Match headers (304, 412)
|
|
63
|
+
- **CLI & Config** - `jss` command with config file/env var support
|
|
64
|
+
- **SSL/TLS** - HTTPS support with certificate configuration
|
|
63
65
|
- **WebSocket Notifications** - Real-time updates via solid-0.1 protocol (SolidOS compatible)
|
|
64
66
|
- **Container Management** - Create, list, and manage containers
|
|
65
67
|
- **Multi-user Pods** - Create pods at `/<username>/`
|
|
66
68
|
- **WebID Profiles** - JSON-LD structured data in HTML at pod root
|
|
67
69
|
- **Web Access Control (WAC)** - `.acl` file-based authorization
|
|
70
|
+
- **Solid-OIDC Identity Provider** - Built-in IdP with DPoP, dynamic registration
|
|
68
71
|
- **Solid-OIDC Resource Server** - Accept DPoP-bound access tokens from external IdPs
|
|
69
72
|
- **Simple Auth Tokens** - Built-in token authentication for development
|
|
70
73
|
- **Content Negotiation** - Optional Turtle <-> JSON-LD conversion
|
|
@@ -92,18 +95,77 @@ npm run benchmark
|
|
|
92
95
|
|
|
93
96
|
```bash
|
|
94
97
|
npm install
|
|
98
|
+
|
|
99
|
+
# Or install globally
|
|
100
|
+
npm install -g javascript-solid-server
|
|
95
101
|
```
|
|
96
102
|
|
|
97
|
-
###
|
|
103
|
+
### Quick Start
|
|
98
104
|
|
|
99
105
|
```bash
|
|
100
|
-
#
|
|
101
|
-
|
|
106
|
+
# Initialize configuration (interactive)
|
|
107
|
+
jss init
|
|
108
|
+
|
|
109
|
+
# Start server
|
|
110
|
+
jss start
|
|
111
|
+
|
|
112
|
+
# Or with options
|
|
113
|
+
jss start --port 8443 --ssl-key ./key.pem --ssl-cert ./cert.pem
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### CLI Commands
|
|
102
117
|
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
```bash
|
|
119
|
+
jss start [options] # Start the server
|
|
120
|
+
jss init [options] # Initialize configuration
|
|
121
|
+
jss --help # Show help
|
|
105
122
|
```
|
|
106
123
|
|
|
124
|
+
### Start Options
|
|
125
|
+
|
|
126
|
+
| Option | Description | Default |
|
|
127
|
+
|--------|-------------|---------|
|
|
128
|
+
| `-p, --port <n>` | Port to listen on | 3000 |
|
|
129
|
+
| `-h, --host <addr>` | Host to bind to | 0.0.0.0 |
|
|
130
|
+
| `-r, --root <path>` | Data directory | ./data |
|
|
131
|
+
| `-c, --config <file>` | Config file path | - |
|
|
132
|
+
| `--ssl-key <path>` | SSL private key (PEM) | - |
|
|
133
|
+
| `--ssl-cert <path>` | SSL certificate (PEM) | - |
|
|
134
|
+
| `--conneg` | Enable Turtle support | false |
|
|
135
|
+
| `--notifications` | Enable WebSocket | false |
|
|
136
|
+
| `--idp` | Enable built-in IdP | false |
|
|
137
|
+
| `--idp-issuer <url>` | IdP issuer URL | (auto) |
|
|
138
|
+
| `-q, --quiet` | Suppress logs | false |
|
|
139
|
+
|
|
140
|
+
### Environment Variables
|
|
141
|
+
|
|
142
|
+
All options can be set via environment variables with `JSS_` prefix:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
export JSS_PORT=8443
|
|
146
|
+
export JSS_SSL_KEY=/path/to/key.pem
|
|
147
|
+
export JSS_SSL_CERT=/path/to/cert.pem
|
|
148
|
+
export JSS_CONNEG=true
|
|
149
|
+
jss start
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Config File
|
|
153
|
+
|
|
154
|
+
Create `config.json`:
|
|
155
|
+
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"port": 8443,
|
|
159
|
+
"root": "./data",
|
|
160
|
+
"sslKey": "./ssl/key.pem",
|
|
161
|
+
"sslCert": "./ssl/cert.pem",
|
|
162
|
+
"conneg": true,
|
|
163
|
+
"notifications": true
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Then: `jss start --config config.json`
|
|
168
|
+
|
|
107
169
|
### Creating a Pod
|
|
108
170
|
|
|
109
171
|
```bash
|
|
@@ -215,9 +277,38 @@ Use the token returned from pod creation:
|
|
|
215
277
|
curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:3000/alice/private/
|
|
216
278
|
```
|
|
217
279
|
|
|
218
|
-
###
|
|
280
|
+
### Built-in Identity Provider (v0.0.12+)
|
|
281
|
+
|
|
282
|
+
Enable the built-in Solid-OIDC Identity Provider:
|
|
283
|
+
|
|
284
|
+
```bash
|
|
285
|
+
jss start --idp
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
With IdP enabled, pod creation requires email and password:
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
curl -X POST http://localhost:3000/.pods \
|
|
292
|
+
-H "Content-Type: application/json" \
|
|
293
|
+
-d '{"name": "alice", "email": "alice@example.com", "password": "secret123"}'
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Response:
|
|
297
|
+
```json
|
|
298
|
+
{
|
|
299
|
+
"name": "alice",
|
|
300
|
+
"webId": "http://localhost:3000/alice/#me",
|
|
301
|
+
"podUri": "http://localhost:3000/alice/",
|
|
302
|
+
"idpIssuer": "http://localhost:3000",
|
|
303
|
+
"loginUrl": "http://localhost:3000/idp/auth"
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
OIDC Discovery: `/.well-known/openid-configuration`
|
|
308
|
+
|
|
309
|
+
### Solid-OIDC (External IdP)
|
|
219
310
|
|
|
220
|
-
The server accepts DPoP-bound access tokens from external Solid identity providers:
|
|
311
|
+
The server also accepts DPoP-bound access tokens from external Solid identity providers:
|
|
221
312
|
|
|
222
313
|
```bash
|
|
223
314
|
curl -H "Authorization: DPoP ACCESS_TOKEN" \
|
|
@@ -264,7 +355,7 @@ Server: pub http://localhost:3000/alice/public/data.json (on change)
|
|
|
264
355
|
npm test
|
|
265
356
|
```
|
|
266
357
|
|
|
267
|
-
Currently passing: **
|
|
358
|
+
Currently passing: **174 tests** (including 27 conformance tests)
|
|
268
359
|
|
|
269
360
|
## Project Structure
|
|
270
361
|
|
|
@@ -296,6 +387,14 @@ src/
|
|
|
296
387
|
│ ├── index.js # WebSocket plugin
|
|
297
388
|
│ ├── events.js # Event emitter
|
|
298
389
|
│ └── websocket.js # solid-0.1 protocol
|
|
390
|
+
├── idp/
|
|
391
|
+
│ ├── index.js # Identity Provider plugin
|
|
392
|
+
│ ├── provider.js # oidc-provider config
|
|
393
|
+
│ ├── adapter.js # Filesystem adapter
|
|
394
|
+
│ ├── accounts.js # User account management
|
|
395
|
+
│ ├── keys.js # JWKS key management
|
|
396
|
+
│ ├── interactions.js # Login/consent handlers
|
|
397
|
+
│ └── views.js # HTML templates
|
|
299
398
|
├── rdf/
|
|
300
399
|
│ ├── turtle.js # Turtle <-> JSON-LD
|
|
301
400
|
│ └── conneg.js # Content negotiation
|
|
@@ -313,6 +412,8 @@ Minimal dependencies for a fast, secure server:
|
|
|
313
412
|
- **fs-extra** - Enhanced file operations
|
|
314
413
|
- **jose** - JWT/JWK handling for Solid-OIDC
|
|
315
414
|
- **n3** - Turtle parsing (only used when conneg enabled)
|
|
415
|
+
- **oidc-provider** - OpenID Connect Identity Provider (only when IdP enabled)
|
|
416
|
+
- **bcrypt** - Password hashing (only when IdP enabled)
|
|
316
417
|
|
|
317
418
|
## License
|
|
318
419
|
|
package/bin/jss.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JavaScript Solid Server CLI
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* jss start [options] Start the server
|
|
8
|
+
* jss init Initialize configuration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Command } from 'commander';
|
|
12
|
+
import { createServer } from '../src/server.js';
|
|
13
|
+
import { loadConfig, saveConfig, printConfig, defaults } from '../src/config.js';
|
|
14
|
+
import fs from 'fs-extra';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
import readline from 'readline';
|
|
18
|
+
|
|
19
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const pkg = JSON.parse(await fs.readFile(path.join(__dirname, '../package.json'), 'utf8'));
|
|
21
|
+
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.name('jss')
|
|
26
|
+
.description('JavaScript Solid Server - A minimal, fast, JSON-LD native Solid server')
|
|
27
|
+
.version(pkg.version);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Start command
|
|
31
|
+
*/
|
|
32
|
+
program
|
|
33
|
+
.command('start')
|
|
34
|
+
.description('Start the Solid server')
|
|
35
|
+
.option('-p, --port <number>', 'Port to listen on', parseInt)
|
|
36
|
+
.option('-h, --host <address>', 'Host to bind to')
|
|
37
|
+
.option('-r, --root <path>', 'Data directory')
|
|
38
|
+
.option('-c, --config <file>', 'Config file path')
|
|
39
|
+
.option('--ssl-key <path>', 'Path to SSL private key (PEM)')
|
|
40
|
+
.option('--ssl-cert <path>', 'Path to SSL certificate (PEM)')
|
|
41
|
+
.option('--multiuser', 'Enable multi-user mode')
|
|
42
|
+
.option('--no-multiuser', 'Disable multi-user mode')
|
|
43
|
+
.option('--conneg', 'Enable content negotiation (Turtle support)')
|
|
44
|
+
.option('--no-conneg', 'Disable content negotiation')
|
|
45
|
+
.option('--notifications', 'Enable WebSocket notifications')
|
|
46
|
+
.option('--no-notifications', 'Disable WebSocket notifications')
|
|
47
|
+
.option('--idp', 'Enable built-in Identity Provider')
|
|
48
|
+
.option('--no-idp', 'Disable built-in Identity Provider')
|
|
49
|
+
.option('--idp-issuer <url>', 'IdP issuer URL (defaults to server URL)')
|
|
50
|
+
.option('-q, --quiet', 'Suppress log output')
|
|
51
|
+
.option('--print-config', 'Print configuration and exit')
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
try {
|
|
54
|
+
const config = await loadConfig(options, options.config);
|
|
55
|
+
|
|
56
|
+
if (options.printConfig) {
|
|
57
|
+
printConfig(config);
|
|
58
|
+
process.exit(0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Determine IdP issuer URL
|
|
62
|
+
const protocol = config.ssl ? 'https' : 'http';
|
|
63
|
+
const serverHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
|
|
64
|
+
const baseUrl = `${protocol}://${serverHost}:${config.port}`;
|
|
65
|
+
const idpIssuer = config.idpIssuer || baseUrl;
|
|
66
|
+
|
|
67
|
+
// Create and start server
|
|
68
|
+
const server = createServer({
|
|
69
|
+
logger: config.logger,
|
|
70
|
+
conneg: config.conneg,
|
|
71
|
+
notifications: config.notifications,
|
|
72
|
+
idp: config.idp,
|
|
73
|
+
idpIssuer: idpIssuer,
|
|
74
|
+
ssl: config.ssl ? {
|
|
75
|
+
key: await fs.readFile(config.sslKey),
|
|
76
|
+
cert: await fs.readFile(config.sslCert),
|
|
77
|
+
} : null,
|
|
78
|
+
root: config.root,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
await server.listen({ port: config.port, host: config.host });
|
|
82
|
+
|
|
83
|
+
if (!config.quiet) {
|
|
84
|
+
console.log(`\n JavaScript Solid Server v${pkg.version}`);
|
|
85
|
+
console.log(` ${baseUrl}/`);
|
|
86
|
+
console.log(`\n Data: ${path.resolve(config.root)}`);
|
|
87
|
+
if (config.ssl) console.log(' SSL: enabled');
|
|
88
|
+
if (config.conneg) console.log(' Conneg: enabled');
|
|
89
|
+
if (config.notifications) console.log(' WebSocket: enabled');
|
|
90
|
+
if (config.idp) console.log(` IdP: ${idpIssuer}`);
|
|
91
|
+
console.log('\n Press Ctrl+C to stop\n');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Handle shutdown
|
|
95
|
+
const shutdown = async () => {
|
|
96
|
+
if (!config.quiet) console.log('\n Shutting down...');
|
|
97
|
+
await server.close();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
process.on('SIGINT', shutdown);
|
|
102
|
+
process.on('SIGTERM', shutdown);
|
|
103
|
+
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error(`Error: ${err.message}`);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Init command - interactive configuration
|
|
112
|
+
*/
|
|
113
|
+
program
|
|
114
|
+
.command('init')
|
|
115
|
+
.description('Initialize server configuration')
|
|
116
|
+
.option('-c, --config <file>', 'Config file path', './config.json')
|
|
117
|
+
.option('-y, --yes', 'Accept defaults without prompting')
|
|
118
|
+
.action(async (options) => {
|
|
119
|
+
const configFile = path.resolve(options.config);
|
|
120
|
+
|
|
121
|
+
// Check if config already exists
|
|
122
|
+
if (await fs.pathExists(configFile)) {
|
|
123
|
+
console.log(`Config file already exists: ${configFile}`);
|
|
124
|
+
const overwrite = options.yes ? true : await confirm('Overwrite?');
|
|
125
|
+
if (!overwrite) {
|
|
126
|
+
console.log('Aborted.');
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
let config;
|
|
132
|
+
|
|
133
|
+
if (options.yes) {
|
|
134
|
+
// Use defaults
|
|
135
|
+
config = { ...defaults };
|
|
136
|
+
} else {
|
|
137
|
+
// Interactive prompts
|
|
138
|
+
console.log('\n JavaScript Solid Server Setup\n');
|
|
139
|
+
|
|
140
|
+
config = {
|
|
141
|
+
port: await prompt('Port', defaults.port),
|
|
142
|
+
root: await prompt('Data directory', defaults.root),
|
|
143
|
+
conneg: await confirm('Enable content negotiation (Turtle support)?', defaults.conneg),
|
|
144
|
+
notifications: await confirm('Enable WebSocket notifications?', defaults.notifications),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Ask about SSL
|
|
148
|
+
const useSSL = await confirm('Configure SSL?', false);
|
|
149
|
+
if (useSSL) {
|
|
150
|
+
config.sslKey = await prompt('SSL key path', './ssl/key.pem');
|
|
151
|
+
config.sslCert = await prompt('SSL certificate path', './ssl/cert.pem');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Ask about IdP
|
|
155
|
+
config.idp = await confirm('Enable built-in Identity Provider?', false);
|
|
156
|
+
if (config.idp) {
|
|
157
|
+
const customIssuer = await confirm('Use custom issuer URL?', false);
|
|
158
|
+
if (customIssuer) {
|
|
159
|
+
config.idpIssuer = await prompt('IdP issuer URL', 'https://example.com');
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
console.log('');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Save config
|
|
167
|
+
await saveConfig(config, configFile);
|
|
168
|
+
console.log(`Configuration saved to: ${configFile}`);
|
|
169
|
+
|
|
170
|
+
// Create data directory
|
|
171
|
+
const dataDir = path.resolve(config.root);
|
|
172
|
+
await fs.ensureDir(dataDir);
|
|
173
|
+
console.log(`Data directory created: ${dataDir}`);
|
|
174
|
+
|
|
175
|
+
console.log('\nRun `jss start` to start the server.\n');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Helper: Prompt for input
|
|
180
|
+
*/
|
|
181
|
+
async function prompt(question, defaultValue) {
|
|
182
|
+
const rl = readline.createInterface({
|
|
183
|
+
input: process.stdin,
|
|
184
|
+
output: process.stdout
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return new Promise((resolve) => {
|
|
188
|
+
const defaultStr = defaultValue !== undefined ? ` (${defaultValue})` : '';
|
|
189
|
+
rl.question(` ${question}${defaultStr}: `, (answer) => {
|
|
190
|
+
rl.close();
|
|
191
|
+
const value = answer.trim() || defaultValue;
|
|
192
|
+
// Parse numbers
|
|
193
|
+
if (typeof defaultValue === 'number' && !isNaN(value)) {
|
|
194
|
+
resolve(parseInt(value, 10));
|
|
195
|
+
} else {
|
|
196
|
+
resolve(value);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Helper: Confirm yes/no
|
|
204
|
+
*/
|
|
205
|
+
async function confirm(question, defaultValue = false) {
|
|
206
|
+
const rl = readline.createInterface({
|
|
207
|
+
input: process.stdin,
|
|
208
|
+
output: process.stdout
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return new Promise((resolve) => {
|
|
212
|
+
const hint = defaultValue ? '[Y/n]' : '[y/N]';
|
|
213
|
+
rl.question(` ${question} ${hint}: `, (answer) => {
|
|
214
|
+
rl.close();
|
|
215
|
+
const normalized = answer.trim().toLowerCase();
|
|
216
|
+
if (normalized === '') {
|
|
217
|
+
resolve(defaultValue);
|
|
218
|
+
} else {
|
|
219
|
+
resolve(normalized === 'y' || normalized === 'yes');
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Parse and run
|
|
226
|
+
program.parse();
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "javascript-solid-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.12",
|
|
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
|
+
},
|
|
7
10
|
"repository": {
|
|
8
11
|
"type": "git",
|
|
9
12
|
"url": "git+https://github.com/JavaScriptSolidServer/JavaScriptSolidServer.git"
|
|
@@ -13,17 +16,21 @@
|
|
|
13
16
|
},
|
|
14
17
|
"homepage": "https://github.com/JavaScriptSolidServer/JavaScriptSolidServer#readme",
|
|
15
18
|
"scripts": {
|
|
16
|
-
"start": "node
|
|
17
|
-
"dev": "node --watch
|
|
19
|
+
"start": "node bin/jss.js start",
|
|
20
|
+
"dev": "node --watch bin/jss.js start",
|
|
18
21
|
"test": "node --test --test-concurrency=1",
|
|
19
22
|
"benchmark": "node benchmark.js"
|
|
20
23
|
},
|
|
21
24
|
"dependencies": {
|
|
25
|
+
"@fastify/middie": "^8.3.3",
|
|
22
26
|
"@fastify/websocket": "^8.3.1",
|
|
27
|
+
"bcrypt": "^6.0.0",
|
|
28
|
+
"commander": "^14.0.2",
|
|
23
29
|
"fastify": "^4.25.2",
|
|
24
30
|
"fs-extra": "^11.2.0",
|
|
25
31
|
"jose": "^6.1.3",
|
|
26
|
-
"n3": "^1.26.0"
|
|
32
|
+
"n3": "^1.26.0",
|
|
33
|
+
"oidc-provider": "^9.6.0"
|
|
27
34
|
},
|
|
28
35
|
"engines": {
|
|
29
36
|
"node": ">=18.0.0"
|
package/src/config.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
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
|
+
// Identity Provider
|
|
33
|
+
idp: false,
|
|
34
|
+
idpIssuer: null,
|
|
35
|
+
|
|
36
|
+
// Logging
|
|
37
|
+
logger: true,
|
|
38
|
+
quiet: false,
|
|
39
|
+
|
|
40
|
+
// Paths
|
|
41
|
+
configPath: './.jss',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Map of environment variable names to config keys
|
|
46
|
+
*/
|
|
47
|
+
const envMap = {
|
|
48
|
+
JSS_PORT: 'port',
|
|
49
|
+
JSS_HOST: 'host',
|
|
50
|
+
JSS_ROOT: 'root',
|
|
51
|
+
JSS_SSL_KEY: 'sslKey',
|
|
52
|
+
JSS_SSL_CERT: 'sslCert',
|
|
53
|
+
JSS_MULTIUSER: 'multiuser',
|
|
54
|
+
JSS_CONNEG: 'conneg',
|
|
55
|
+
JSS_NOTIFICATIONS: 'notifications',
|
|
56
|
+
JSS_QUIET: 'quiet',
|
|
57
|
+
JSS_CONFIG_PATH: 'configPath',
|
|
58
|
+
JSS_IDP: 'idp',
|
|
59
|
+
JSS_IDP_ISSUER: 'idpIssuer',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parse a value from environment variable string
|
|
64
|
+
*/
|
|
65
|
+
function parseEnvValue(value, key) {
|
|
66
|
+
if (value === undefined) return undefined;
|
|
67
|
+
|
|
68
|
+
// Boolean values
|
|
69
|
+
if (value.toLowerCase() === 'true') return true;
|
|
70
|
+
if (value.toLowerCase() === 'false') return false;
|
|
71
|
+
|
|
72
|
+
// Numeric values for known numeric keys
|
|
73
|
+
if (key === 'port' && !isNaN(value)) {
|
|
74
|
+
return parseInt(value, 10);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Load configuration from environment variables
|
|
82
|
+
*/
|
|
83
|
+
function loadEnvConfig() {
|
|
84
|
+
const config = {};
|
|
85
|
+
|
|
86
|
+
for (const [envVar, configKey] of Object.entries(envMap)) {
|
|
87
|
+
const value = process.env[envVar];
|
|
88
|
+
if (value !== undefined) {
|
|
89
|
+
config[configKey] = parseEnvValue(value, configKey);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return config;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Load configuration from a JSON file
|
|
98
|
+
*/
|
|
99
|
+
async function loadFileConfig(configFile) {
|
|
100
|
+
if (!configFile) return {};
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
const fullPath = path.resolve(configFile);
|
|
104
|
+
if (await fs.pathExists(fullPath)) {
|
|
105
|
+
const content = await fs.readFile(fullPath, 'utf8');
|
|
106
|
+
return JSON.parse(content);
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
console.error(`Warning: Failed to load config file: ${e.message}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Merge configuration sources
|
|
117
|
+
* @param {object} cliOptions - Options from command line
|
|
118
|
+
* @param {string} configFile - Path to config file (optional)
|
|
119
|
+
* @returns {Promise<object>} Merged configuration
|
|
120
|
+
*/
|
|
121
|
+
export async function loadConfig(cliOptions = {}, configFile = null) {
|
|
122
|
+
// Load from file first
|
|
123
|
+
const fileConfig = await loadFileConfig(configFile || cliOptions.config);
|
|
124
|
+
|
|
125
|
+
// Load from environment
|
|
126
|
+
const envConfig = loadEnvConfig();
|
|
127
|
+
|
|
128
|
+
// Merge in order: defaults < file < env < cli
|
|
129
|
+
const config = {
|
|
130
|
+
...defaults,
|
|
131
|
+
...fileConfig,
|
|
132
|
+
...envConfig,
|
|
133
|
+
...filterUndefined(cliOptions),
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// Derive additional settings
|
|
137
|
+
if (config.quiet) {
|
|
138
|
+
config.logger = false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Validate SSL config
|
|
142
|
+
if ((config.sslKey && !config.sslCert) || (!config.sslKey && config.sslCert)) {
|
|
143
|
+
throw new Error('Both --ssl-key and --ssl-cert must be provided together');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
config.ssl = !!(config.sslKey && config.sslCert);
|
|
147
|
+
|
|
148
|
+
return config;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Filter out undefined values from an object
|
|
153
|
+
*/
|
|
154
|
+
function filterUndefined(obj) {
|
|
155
|
+
const result = {};
|
|
156
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
157
|
+
if (value !== undefined) {
|
|
158
|
+
result[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Save configuration to a file
|
|
166
|
+
*/
|
|
167
|
+
export async function saveConfig(config, configFile) {
|
|
168
|
+
const toSave = { ...config };
|
|
169
|
+
// Remove derived/runtime values
|
|
170
|
+
delete toSave.ssl;
|
|
171
|
+
delete toSave.logger;
|
|
172
|
+
|
|
173
|
+
await fs.ensureDir(path.dirname(configFile));
|
|
174
|
+
await fs.writeFile(configFile, JSON.stringify(toSave, null, 2));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Print configuration (for debugging)
|
|
179
|
+
*/
|
|
180
|
+
export function printConfig(config) {
|
|
181
|
+
console.log('\nConfiguration:');
|
|
182
|
+
console.log('─'.repeat(40));
|
|
183
|
+
console.log(` Port: ${config.port}`);
|
|
184
|
+
console.log(` Host: ${config.host}`);
|
|
185
|
+
console.log(` Root: ${path.resolve(config.root)}`);
|
|
186
|
+
console.log(` SSL: ${config.ssl ? 'enabled' : 'disabled'}`);
|
|
187
|
+
console.log(` Multi-user: ${config.multiuser}`);
|
|
188
|
+
console.log(` Conneg: ${config.conneg}`);
|
|
189
|
+
console.log(` Notifications: ${config.notifications}`);
|
|
190
|
+
console.log(` IdP: ${config.idp ? (config.idpIssuer || 'enabled') : 'disabled'}`);
|
|
191
|
+
console.log('─'.repeat(40));
|
|
192
|
+
}
|