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 CHANGED
@@ -6,7 +6,7 @@ A minimal, fast, JSON-LD native Solid server.
6
6
 
7
7
  ## Features
8
8
 
9
- ### Implemented (v0.0.78)
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.78+)
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.inviteOnly) console.log(' Registration: invite-only');
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.78",
3
+ "version": "0.0.79",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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
- fastify.get('/idp/register', async (request, reply) => {
282
- return handleRegisterGet(request, reply, inviteOnly);
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
- // Registration - rate limited to prevent spam accounts
286
- fastify.post('/idp/register', {
287
- config: {
288
- rateLimit: {
289
- max: 5,
290
- timeWindow: '1 hour',
291
- keyGenerator: (request) => request.ip
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
- }, async (request, reply) => {
295
- return handleRegisterPost(request, reply, issuer, inviteOnly);
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
- fastify.log.info(`IdP initialized with issuer: ${issuer}`);
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