javascript-solid-server 0.0.78 → 0.0.79
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/README.md +27 -2
- package/bin/jss.js +6 -1
- package/package.json +1 -1
- package/src/config.js +6 -0
- package/src/idp/index.js +36 -17
- package/src/server.js +91 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A minimal, fast, JSON-LD native Solid server.
|
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
9
|
-
### Implemented (v0.0.
|
|
9
|
+
### Implemented (v0.0.79)
|
|
10
10
|
|
|
11
11
|
- **Schnorr SSO** - Passwordless login via BIP-340 Schnorr signatures using NIP-07 browser extensions (Podkey, nos2x, Alby)
|
|
12
12
|
- **Passkey Authentication** - WebAuthn/FIDO2 passwordless login with Touch ID, Face ID, or security keys
|
|
@@ -181,6 +181,31 @@ Then: `jss start --config config.json`
|
|
|
181
181
|
|
|
182
182
|
### Creating a Pod
|
|
183
183
|
|
|
184
|
+
### Single-User Mode
|
|
185
|
+
|
|
186
|
+
For personal pod servers where only one user needs access:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Basic single-user mode (creates pod at /me/)
|
|
190
|
+
jss start --single-user --idp
|
|
191
|
+
|
|
192
|
+
# Custom username
|
|
193
|
+
jss start --single-user --single-user-name alice --idp
|
|
194
|
+
|
|
195
|
+
# Root-level pod (pod at /, WebID at /profile/card#me)
|
|
196
|
+
jss start --single-user --single-user-name '' --idp
|
|
197
|
+
|
|
198
|
+
# Via environment
|
|
199
|
+
JSS_SINGLE_USER=true jss start --idp
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
**Features:**
|
|
203
|
+
- Pod auto-created on first startup with full structure (inbox, public, private, profile, Settings)
|
|
204
|
+
- Registration endpoint disabled (returns 403)
|
|
205
|
+
- Login still works for the single user
|
|
206
|
+
- Proper ACLs generated automatically
|
|
207
|
+
|
|
208
|
+
|
|
184
209
|
```bash
|
|
185
210
|
curl -X POST http://localhost:3000/.pods \
|
|
186
211
|
-H "Content-Type: application/json" \
|
|
@@ -694,7 +719,7 @@ jss start --idp
|
|
|
694
719
|
|
|
695
720
|
Passkeys are stored per-account and work across devices via platform sync (iCloud Keychain, Google Password Manager, etc.).
|
|
696
721
|
|
|
697
|
-
### Schnorr SSO (v0.0.
|
|
722
|
+
### Schnorr SSO (v0.0.79+)
|
|
698
723
|
|
|
699
724
|
Sign in with your Nostr key using NIP-07 browser extensions:
|
|
700
725
|
|
package/bin/jss.js
CHANGED
|
@@ -72,6 +72,8 @@ program
|
|
|
72
72
|
.option('--ap-nostr-pubkey <hex>', 'Nostr pubkey for identity linking')
|
|
73
73
|
.option('--invite-only', 'Require invite code for registration')
|
|
74
74
|
.option('--no-invite-only', 'Allow open registration')
|
|
75
|
+
.option('--single-user', 'Single-user mode (creates pod on startup, disables registration)')
|
|
76
|
+
.option('--single-user-name <name>', 'Username for single-user mode (default: me)')
|
|
75
77
|
.option('--webid-tls', 'Enable WebID-TLS client certificate authentication')
|
|
76
78
|
.option('--no-webid-tls', 'Disable WebID-TLS authentication')
|
|
77
79
|
.option('-q, --quiet', 'Suppress log output')
|
|
@@ -127,6 +129,8 @@ program
|
|
|
127
129
|
apNostrPubkey: config.apNostrPubkey,
|
|
128
130
|
inviteOnly: config.inviteOnly,
|
|
129
131
|
webidTls: config.webidTls,
|
|
132
|
+
singleUser: config.singleUser,
|
|
133
|
+
singleUserName: config.singleUserName,
|
|
130
134
|
});
|
|
131
135
|
|
|
132
136
|
await server.listen({ port: config.port, host: config.host });
|
|
@@ -149,7 +153,8 @@ program
|
|
|
149
153
|
if (config.git) console.log(' Git: enabled (clone/push support)');
|
|
150
154
|
if (config.nostr) console.log(` Nostr: enabled (${config.nostrPath})`);
|
|
151
155
|
if (config.activitypub) console.log(` ActivityPub: enabled (@${config.apUsername || 'me'})`);
|
|
152
|
-
if (config.
|
|
156
|
+
if (config.singleUser) console.log(` Single-user: ${config.singleUserName || 'me'} (registration disabled)`);
|
|
157
|
+
else if (config.inviteOnly) console.log(' Registration: invite-only');
|
|
153
158
|
if (config.webidTls) console.log(' WebID-TLS: enabled (client certificate auth)');
|
|
154
159
|
console.log('\n Press Ctrl+C to stop\n');
|
|
155
160
|
}
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -63,6 +63,10 @@ export const defaults = {
|
|
|
63
63
|
// Invite-only registration
|
|
64
64
|
inviteOnly: false,
|
|
65
65
|
|
|
66
|
+
// Single-user mode (personal pod server)
|
|
67
|
+
singleUser: false,
|
|
68
|
+
singleUserName: 'me',
|
|
69
|
+
|
|
66
70
|
// WebID-TLS client certificate authentication
|
|
67
71
|
webidTls: false,
|
|
68
72
|
|
|
@@ -109,6 +113,8 @@ const envMap = {
|
|
|
109
113
|
JSS_AP_SUMMARY: 'apSummary',
|
|
110
114
|
JSS_AP_NOSTR_PUBKEY: 'apNostrPubkey',
|
|
111
115
|
JSS_INVITE_ONLY: 'inviteOnly',
|
|
116
|
+
JSS_SINGLE_USER: 'singleUser',
|
|
117
|
+
JSS_SINGLE_USER_NAME: 'singleUserName',
|
|
112
118
|
JSS_WEBID_TLS: 'webidTls',
|
|
113
119
|
JSS_DEFAULT_QUOTA: 'defaultQuota',
|
|
114
120
|
};
|
package/src/idp/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { addTrustedIssuer } from '../auth/solid-oidc.js';
|
|
|
32
32
|
* @param {string} options.issuer - The issuer URL
|
|
33
33
|
*/
|
|
34
34
|
export async function idpPlugin(fastify, options) {
|
|
35
|
-
const { issuer, inviteOnly = false } = options;
|
|
35
|
+
const { issuer, inviteOnly = false, singleUser = false } = options;
|
|
36
36
|
|
|
37
37
|
if (!issuer) {
|
|
38
38
|
throw new Error('IdP requires issuer URL');
|
|
@@ -277,23 +277,41 @@ export async function idpPlugin(fastify, options) {
|
|
|
277
277
|
return handleAbort(request, reply, provider);
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
-
// Registration routes
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
280
|
+
// Registration routes (disabled in single-user mode)
|
|
281
|
+
if (singleUser) {
|
|
282
|
+
// Single-user mode: registration disabled
|
|
283
|
+
fastify.get('/idp/register', async (request, reply) => {
|
|
284
|
+
return reply.code(403).type('text/html').send(`
|
|
285
|
+
<!DOCTYPE html>
|
|
286
|
+
<html><head><title>Registration Disabled</title></head>
|
|
287
|
+
<body style="font-family: system-ui; padding: 2rem; text-align: center;">
|
|
288
|
+
<h1>Registration Disabled</h1>
|
|
289
|
+
<p>This server is running in single-user mode. Registration is not available.</p>
|
|
290
|
+
<p><a href="/idp/login">Login</a></p>
|
|
291
|
+
</body></html>
|
|
292
|
+
`);
|
|
293
|
+
});
|
|
294
|
+
fastify.post('/idp/register', async (request, reply) => {
|
|
295
|
+
return reply.code(403).send({ error: 'Registration disabled in single-user mode' });
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
fastify.get('/idp/register', async (request, reply) => {
|
|
299
|
+
return handleRegisterGet(request, reply, inviteOnly);
|
|
300
|
+
});
|
|
284
301
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
302
|
+
// Registration - rate limited to prevent spam accounts
|
|
303
|
+
fastify.post('/idp/register', {
|
|
304
|
+
config: {
|
|
305
|
+
rateLimit: {
|
|
306
|
+
max: 5,
|
|
307
|
+
timeWindow: '1 hour',
|
|
308
|
+
keyGenerator: (request) => request.ip
|
|
309
|
+
}
|
|
292
310
|
}
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
311
|
+
}, async (request, reply) => {
|
|
312
|
+
return handleRegisterPost(request, reply, issuer, inviteOnly);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
297
315
|
|
|
298
316
|
// Passkey routes
|
|
299
317
|
// Registration options - rate limited to prevent DoS
|
|
@@ -373,7 +391,8 @@ export async function idpPlugin(fastify, options) {
|
|
|
373
391
|
return handleSchnorrComplete(request, reply, provider);
|
|
374
392
|
});
|
|
375
393
|
|
|
376
|
-
|
|
394
|
+
const modeInfo = singleUser ? ' (single-user mode, registration disabled)' : inviteOnly ? ' (invite-only)' : '';
|
|
395
|
+
fastify.log.info(`IdP initialized with issuer: ${issuer}${modeInfo}`);
|
|
377
396
|
}
|
|
378
397
|
|
|
379
398
|
export default idpPlugin;
|
package/src/server.js
CHANGED
|
@@ -4,7 +4,8 @@ import { readFile } from 'fs/promises';
|
|
|
4
4
|
import { join, dirname } from 'path';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { handleGet, handleHead, handlePut, handleDelete, handleOptions, handlePatch } from './handlers/resource.js';
|
|
7
|
-
import { handlePost, handleCreatePod } from './handlers/container.js';
|
|
7
|
+
import { handlePost, handleCreatePod, createPodStructure } from './handlers/container.js';
|
|
8
|
+
import * as storage from './storage/filesystem.js';
|
|
8
9
|
import { getCorsHeaders } from './ldp/headers.js';
|
|
9
10
|
import { authorize, handleUnauthorized } from './auth/middleware.js';
|
|
10
11
|
import { notificationsPlugin } from './notifications/index.js';
|
|
@@ -71,6 +72,9 @@ export function createServer(options = {}) {
|
|
|
71
72
|
const apNostrPubkey = options.apNostrPubkey ?? null;
|
|
72
73
|
// Invite-only registration is OFF by default - open registration
|
|
73
74
|
const inviteOnly = options.inviteOnly ?? false;
|
|
75
|
+
// Single-user mode - creates pod on startup, disables registration
|
|
76
|
+
const singleUser = options.singleUser ?? false;
|
|
77
|
+
const singleUserName = options.singleUserName ?? 'me';
|
|
74
78
|
// Default storage quota per pod (50MB default, 0 = unlimited)
|
|
75
79
|
const defaultQuota = options.defaultQuota ?? 50 * 1024 * 1024;
|
|
76
80
|
// WebID-TLS client certificate authentication is OFF by default
|
|
@@ -165,7 +169,7 @@ export function createServer(options = {}) {
|
|
|
165
169
|
|
|
166
170
|
// Register Identity Provider plugin if enabled
|
|
167
171
|
if (idpEnabled) {
|
|
168
|
-
fastify.register(idpPlugin, { issuer: idpIssuer, inviteOnly });
|
|
172
|
+
fastify.register(idpPlugin, { issuer: idpIssuer, inviteOnly, singleUser });
|
|
169
173
|
}
|
|
170
174
|
|
|
171
175
|
// Register Nostr relay if enabled
|
|
@@ -447,6 +451,91 @@ export function createServer(options = {}) {
|
|
|
447
451
|
fastify.options('/', handleOptions);
|
|
448
452
|
fastify.post('/', writeRateLimit, handlePost);
|
|
449
453
|
|
|
454
|
+
// Single-user mode: create pod on startup if it doesn't exist
|
|
455
|
+
if (singleUser) {
|
|
456
|
+
fastify.addHook('onReady', async () => {
|
|
457
|
+
// Determine base URL for pod URIs
|
|
458
|
+
const protocol = options.ssl ? 'https' : 'http';
|
|
459
|
+
const host = options.host === '0.0.0.0' ? 'localhost' : (options.host || 'localhost');
|
|
460
|
+
const port = options.port || 3000;
|
|
461
|
+
const baseUrl = idpIssuer?.replace(/\/$/, '') || `${protocol}://${host}:${port}`;
|
|
462
|
+
const issuer = idpIssuer || `${baseUrl}/`;
|
|
463
|
+
|
|
464
|
+
// Root-level pod (empty or '/' name) vs named pod
|
|
465
|
+
const isRootPod = !singleUserName || singleUserName === '/';
|
|
466
|
+
const podPath = isRootPod ? '/' : `/${singleUserName}/`;
|
|
467
|
+
const podUri = isRootPod ? `${baseUrl}/` : `${baseUrl}/${singleUserName}/`;
|
|
468
|
+
const webId = `${podUri}profile/card#me`;
|
|
469
|
+
const displayName = isRootPod ? 'me' : singleUserName;
|
|
470
|
+
|
|
471
|
+
// Check if pod already exists (profile/card is the indicator)
|
|
472
|
+
const profileExists = await storage.exists(`${podPath}profile/card`);
|
|
473
|
+
|
|
474
|
+
if (!profileExists) {
|
|
475
|
+
fastify.log.info(`Creating single-user pod at ${podUri}...`);
|
|
476
|
+
|
|
477
|
+
if (isRootPod) {
|
|
478
|
+
// Root-level pod - create structure directly at /
|
|
479
|
+
await createRootPodStructure(webId, podUri, issuer, displayName);
|
|
480
|
+
} else {
|
|
481
|
+
// Named pod at /{name}/
|
|
482
|
+
await createPodStructure(singleUserName, webId, podUri, issuer, defaultQuota);
|
|
483
|
+
}
|
|
484
|
+
fastify.log.info(`Single-user pod created at ${podUri}`);
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Create root-level pod structure (for single-user mode with pod at /)
|
|
491
|
+
*/
|
|
492
|
+
async function createRootPodStructure(webId, podUri, issuer, displayName) {
|
|
493
|
+
const { generateProfile, generatePreferences, generateTypeIndex, serialize } = await import('./webid/profile.js');
|
|
494
|
+
const { generateOwnerAcl, generatePrivateAcl, generateInboxAcl, generatePublicFolderAcl, serializeAcl } = await import('./wac/parser.js');
|
|
495
|
+
|
|
496
|
+
// Create directories at root
|
|
497
|
+
await storage.createContainer('/inbox/');
|
|
498
|
+
await storage.createContainer('/public/');
|
|
499
|
+
await storage.createContainer('/private/');
|
|
500
|
+
await storage.createContainer('/Settings/');
|
|
501
|
+
await storage.createContainer('/profile/');
|
|
502
|
+
|
|
503
|
+
// Generate profile
|
|
504
|
+
const profileHtml = generateProfile({ webId, name: displayName, podUri, issuer });
|
|
505
|
+
await storage.write('/profile/card', profileHtml);
|
|
506
|
+
|
|
507
|
+
// Preferences and type indexes
|
|
508
|
+
const prefs = generatePreferences({ webId, podUri });
|
|
509
|
+
await storage.write('/Settings/Preferences.ttl', serialize(prefs));
|
|
510
|
+
|
|
511
|
+
const publicTypeIndex = generateTypeIndex(`${podUri}Settings/publicTypeIndex.ttl`);
|
|
512
|
+
await storage.write('/Settings/publicTypeIndex.ttl', serialize(publicTypeIndex));
|
|
513
|
+
|
|
514
|
+
const privateTypeIndex = generateTypeIndex(`${podUri}Settings/privateTypeIndex.ttl`);
|
|
515
|
+
await storage.write('/Settings/privateTypeIndex.ttl', serialize(privateTypeIndex));
|
|
516
|
+
|
|
517
|
+
// ACL files
|
|
518
|
+
const rootAcl = generateOwnerAcl(podUri, webId, true);
|
|
519
|
+
await storage.write('/.acl', serializeAcl(rootAcl));
|
|
520
|
+
|
|
521
|
+
const privateAcl = generatePrivateAcl(`${podUri}private/`, webId);
|
|
522
|
+
await storage.write('/private/.acl', serializeAcl(privateAcl));
|
|
523
|
+
|
|
524
|
+
const settingsAcl = generatePrivateAcl(`${podUri}Settings/`, webId);
|
|
525
|
+
await storage.write('/Settings/.acl', serializeAcl(settingsAcl));
|
|
526
|
+
|
|
527
|
+
const inboxAcl = generateInboxAcl(`${podUri}inbox/`, webId);
|
|
528
|
+
await storage.write('/inbox/.acl', serializeAcl(inboxAcl));
|
|
529
|
+
|
|
530
|
+
const publicAcl = generatePublicFolderAcl(`${podUri}public/`, webId);
|
|
531
|
+
await storage.write('/public/.acl', serializeAcl(publicAcl));
|
|
532
|
+
|
|
533
|
+
const profileAcl = generatePublicFolderAcl(`${podUri}profile/`, webId);
|
|
534
|
+
await storage.write('/profile/.acl', serializeAcl(profileAcl));
|
|
535
|
+
|
|
536
|
+
// Note: Quota not initialized for root-level pods (no user directory)
|
|
537
|
+
}
|
|
538
|
+
|
|
450
539
|
return fastify;
|
|
451
540
|
}
|
|
452
541
|
|