-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathjss.js
More file actions
executable file
·251 lines (220 loc) · 8.34 KB
/
jss.js
File metadata and controls
executable file
·251 lines (220 loc) · 8.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#!/usr/bin/env node
/**
* JavaScript Solid Server CLI
*
* Usage:
* jss start [options] Start the server
* jss init Initialize configuration
*/
import { Command } from 'commander';
import { createServer } from '../src/server.js';
import { loadConfig, saveConfig, printConfig, defaults } from '../src/config.js';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import readline from 'readline';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const pkg = JSON.parse(await fs.readFile(path.join(__dirname, '../package.json'), 'utf8'));
const program = new Command();
program
.name('jss')
.description('JavaScript Solid Server - A minimal, fast, JSON-LD native Solid server')
.version(pkg.version);
/**
* Start command
*/
program
.command('start')
.description('Start the Solid server')
.option('-p, --port <number>', 'Port to listen on', parseInt)
.option('-h, --host <address>', 'Host to bind to')
.option('-r, --root <path>', 'Data directory')
.option('-c, --config <file>', 'Config file path')
.option('--ssl-key <path>', 'Path to SSL private key (PEM)')
.option('--ssl-cert <path>', 'Path to SSL certificate (PEM)')
.option('--multiuser', 'Enable multi-user mode')
.option('--no-multiuser', 'Disable multi-user mode')
.option('--conneg', 'Enable content negotiation (Turtle support)')
.option('--no-conneg', 'Disable content negotiation')
.option('--notifications', 'Enable WebSocket notifications')
.option('--no-notifications', 'Disable WebSocket notifications')
.option('--idp', 'Enable built-in Identity Provider')
.option('--no-idp', 'Disable built-in Identity Provider')
.option('--idp-issuer <url>', 'IdP issuer URL (defaults to server URL)')
.option('--subdomains', 'Enable subdomain-based pods (XSS protection)')
.option('--no-subdomains', 'Disable subdomain-based pods')
.option('--base-domain <domain>', 'Base domain for subdomain pods (e.g., "example.com")')
.option('--mashlib', 'Enable Mashlib data browser (local mode, requires mashlib in node_modules)')
.option('--mashlib-cdn', 'Enable Mashlib data browser (CDN mode, no local files needed)')
.option('--no-mashlib', 'Disable Mashlib data browser')
.option('--mashlib-version <version>', 'Mashlib version for CDN mode (default: 2.0.0)')
.option('-q, --quiet', 'Suppress log output')
.option('--print-config', 'Print configuration and exit')
.action(async (options) => {
try {
const config = await loadConfig(options, options.config);
// Set DATA_ROOT env var so all modules use the same data directory
process.env.DATA_ROOT = path.resolve(config.root);
if (options.printConfig) {
printConfig(config);
process.exit(0);
}
// Determine IdP issuer URL
const protocol = config.ssl ? 'https' : 'http';
const serverHost = config.host === '0.0.0.0' ? 'localhost' : config.host;
const baseUrl = `${protocol}://${serverHost}:${config.port}`;
// Ensure issuer has trailing slash for CTH compatibility
let idpIssuer = config.idpIssuer || baseUrl;
if (idpIssuer && !idpIssuer.endsWith('/')) {
idpIssuer = idpIssuer + '/';
}
// Create and start server
const server = createServer({
logger: config.logger,
conneg: config.conneg,
notifications: config.notifications,
idp: config.idp,
idpIssuer: idpIssuer,
ssl: config.ssl ? {
key: await fs.readFile(config.sslKey),
cert: await fs.readFile(config.sslCert),
} : null,
root: config.root,
subdomains: config.subdomains,
baseDomain: config.baseDomain,
mashlib: config.mashlib || config.mashlibCdn,
mashlibCdn: config.mashlibCdn,
mashlibVersion: config.mashlibVersion,
});
await server.listen({ port: config.port, host: config.host });
if (!config.quiet) {
console.log(`\n JavaScript Solid Server v${pkg.version}`);
console.log(` ${baseUrl}/`);
console.log(`\n Data: ${path.resolve(config.root)}`);
if (config.ssl) console.log(' SSL: enabled');
if (config.conneg) console.log(' Conneg: enabled');
if (config.notifications) console.log(' WebSocket: enabled');
if (config.idp) console.log(` IdP: ${idpIssuer}`);
if (config.subdomains) console.log(` Subdomains: ${config.baseDomain} (XSS protection enabled)`);
if (config.mashlibCdn) {
console.log(` Mashlib: v${config.mashlibVersion} (CDN mode)`);
} else if (config.mashlib) {
console.log(` Mashlib: local (data browser enabled)`);
}
console.log('\n Press Ctrl+C to stop\n');
}
// Handle shutdown
const shutdown = async () => {
if (!config.quiet) console.log('\n Shutting down...');
await server.close();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
} catch (err) {
console.error(`Error: ${err.message}`);
process.exit(1);
}
});
/**
* Init command - interactive configuration
*/
program
.command('init')
.description('Initialize server configuration')
.option('-c, --config <file>', 'Config file path', './config.json')
.option('-y, --yes', 'Accept defaults without prompting')
.action(async (options) => {
const configFile = path.resolve(options.config);
// Check if config already exists
if (await fs.pathExists(configFile)) {
console.log(`Config file already exists: ${configFile}`);
const overwrite = options.yes ? true : await confirm('Overwrite?');
if (!overwrite) {
console.log('Aborted.');
process.exit(0);
}
}
let config;
if (options.yes) {
// Use defaults
config = { ...defaults };
} else {
// Interactive prompts
console.log('\n JavaScript Solid Server Setup\n');
config = {
port: await prompt('Port', defaults.port),
root: await prompt('Data directory', defaults.root),
conneg: await confirm('Enable content negotiation (Turtle support)?', defaults.conneg),
notifications: await confirm('Enable WebSocket notifications?', defaults.notifications),
};
// Ask about SSL
const useSSL = await confirm('Configure SSL?', false);
if (useSSL) {
config.sslKey = await prompt('SSL key path', './ssl/key.pem');
config.sslCert = await prompt('SSL certificate path', './ssl/cert.pem');
}
// Ask about IdP
config.idp = await confirm('Enable built-in Identity Provider?', false);
if (config.idp) {
const customIssuer = await confirm('Use custom issuer URL?', false);
if (customIssuer) {
config.idpIssuer = await prompt('IdP issuer URL', 'https://example.com');
}
}
console.log('');
}
// Save config
await saveConfig(config, configFile);
console.log(`Configuration saved to: ${configFile}`);
// Create data directory
const dataDir = path.resolve(config.root);
await fs.ensureDir(dataDir);
console.log(`Data directory created: ${dataDir}`);
console.log('\nRun `jss start` to start the server.\n');
});
/**
* Helper: Prompt for input
*/
async function prompt(question, defaultValue) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
const defaultStr = defaultValue !== undefined ? ` (${defaultValue})` : '';
rl.question(` ${question}${defaultStr}: `, (answer) => {
rl.close();
const value = answer.trim() || defaultValue;
// Parse numbers
if (typeof defaultValue === 'number' && !isNaN(value)) {
resolve(parseInt(value, 10));
} else {
resolve(value);
}
});
});
}
/**
* Helper: Confirm yes/no
*/
async function confirm(question, defaultValue = false) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise((resolve) => {
const hint = defaultValue ? '[Y/n]' : '[y/N]';
rl.question(` ${question} ${hint}: `, (answer) => {
rl.close();
const normalized = answer.trim().toLowerCase();
if (normalized === '') {
resolve(defaultValue);
} else {
resolve(normalized === 'y' || normalized === 'yes');
}
});
});
}
// Parse and run
program.parse();