javascript-solid-server 0.0.163 → 0.0.164
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/bin/jss.js +1 -1
- package/docs/configuration.md +19 -5
- package/package.json +1 -1
- package/src/config.js +15 -14
- package/src/server.js +33 -6
- package/test/config.test.js +32 -0
- package/test/idp.test.js +78 -0
- package/test/url.test.js +8 -0
package/bin/jss.js
CHANGED
|
@@ -126,7 +126,7 @@ program
|
|
|
126
126
|
.option('--invite-only', 'Require invite code for registration')
|
|
127
127
|
.option('--no-invite-only', 'Allow open registration')
|
|
128
128
|
.option('--single-user', 'Single-user mode (creates pod on startup, disables registration)')
|
|
129
|
-
.option('--single-user-name <name>', '
|
|
129
|
+
.option('--single-user-name <name>', 'Mount the pod at /<name>/ instead of at the server root (default: root pod at /)')
|
|
130
130
|
.option('--single-user-password <pw>', 'Initial IDP password to seed when creating the single-user pod (or set JSS_SINGLE_USER_PASSWORD)')
|
|
131
131
|
.option('--webid-tls', 'Enable WebID-TLS client certificate authentication')
|
|
132
132
|
.option('--no-webid-tls', 'Disable WebID-TLS authentication')
|
package/docs/configuration.md
CHANGED
|
@@ -258,19 +258,21 @@ Response:
|
|
|
258
258
|
For personal pod servers where only one user needs access:
|
|
259
259
|
|
|
260
260
|
```bash
|
|
261
|
-
#
|
|
262
|
-
#
|
|
261
|
+
# Default: pod served at server root (#348). WebID is
|
|
262
|
+
# /profile/card.jsonld#me; the IDP login username is "me". On first
|
|
263
|
+
# run JSS will prompt for an initial password (TTY only).
|
|
263
264
|
jss start --single-user --idp
|
|
264
265
|
|
|
265
266
|
# Provide the initial IDP password non-interactively (systemd, containers, CI):
|
|
266
267
|
jss start --single-user --idp --single-user-password 'choose-a-good-one'
|
|
267
268
|
JSS_SINGLE_USER_PASSWORD='choose-a-good-one' jss start --single-user --idp
|
|
268
269
|
|
|
269
|
-
#
|
|
270
|
+
# Mount the pod at a named path instead of the origin. WebID becomes
|
|
271
|
+
# /alice/profile/card.jsonld#me; login as "alice".
|
|
270
272
|
jss start --single-user --single-user-name alice --idp
|
|
271
273
|
|
|
272
|
-
#
|
|
273
|
-
jss start --single-user --single-user-name
|
|
274
|
+
# Legacy /me/ pod — same as the old default before #348.
|
|
275
|
+
jss start --single-user --single-user-name me --idp
|
|
274
276
|
|
|
275
277
|
# Via environment
|
|
276
278
|
JSS_SINGLE_USER=true jss start --idp
|
|
@@ -283,6 +285,18 @@ JSS_SINGLE_USER=true jss start --idp
|
|
|
283
285
|
- Login works for the single user via password (`POST /idp/credentials`) or any other configured method
|
|
284
286
|
- Proper ACLs generated automatically
|
|
285
287
|
|
|
288
|
+
**Upgrading from a pre-#348 install:** if your existing pod was created with the old default (data lives under `<root>/me/`), JSS no longer auto-detects it — restarting plain `jss start --single-user` will start seeding a fresh empty root pod alongside your legacy `/me/` data, and your existing IDP account will keep authenticating against `/me/`. Pick one path on the next restart:
|
|
289
|
+
- **Keep the legacy layout:** add `--single-user-name me` to your launch command. No data movement needed.
|
|
290
|
+
- **Migrate to root pod:** move the *entire* contents of `<root>/me/` (including dotfiles like `.acl`, `.meta`, `.quota.json` — a plain `mv <root>/me/* <root>/` skips them) to `<root>/`, delete the IDP account for `me` (so the new root pod's `me` account can be seeded), then restart without the name flag. Use one of:
|
|
291
|
+
|
|
292
|
+
```bash
|
|
293
|
+
# Option A: rsync handles dotfiles correctly with the trailing slash.
|
|
294
|
+
rsync -a <root>/me/ <root>/ && rm -rf <root>/me
|
|
295
|
+
|
|
296
|
+
# Option B: bash with dotglob enabled so * matches dotfiles too.
|
|
297
|
+
shopt -s dotglob && mv <root>/me/* <root>/ && rmdir <root>/me
|
|
298
|
+
```
|
|
299
|
+
|
|
286
300
|
**Initial password sources, in priority order:**
|
|
287
301
|
1. `--single-user-password <pw>` CLI flag
|
|
288
302
|
2. `JSS_SINGLE_USER_PASSWORD` env var
|
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -74,7 +74,11 @@ export const defaults = {
|
|
|
74
74
|
|
|
75
75
|
// Single-user mode (personal pod server)
|
|
76
76
|
singleUser: false,
|
|
77
|
-
|
|
77
|
+
// null = root pod (mounted at server origin, WebID at
|
|
78
|
+
// /profile/card.jsonld#me). A string mounts the pod at /<name>/ —
|
|
79
|
+
// useful when more than one Solid identity coexists on the same
|
|
80
|
+
// origin, or when the operator wants the pre-#348 /me/ shape.
|
|
81
|
+
singleUserName: null,
|
|
78
82
|
// Initial IDP password seeded on first single-user pod creation. If
|
|
79
83
|
// unset and --idp is enabled, the server prompts on a TTY or logs a
|
|
80
84
|
// warning and continues startup on non-TTY (so the pod is created but
|
|
@@ -399,20 +403,17 @@ export function printConfig(config) {
|
|
|
399
403
|
console.log(` SSL: ${config.ssl ? 'enabled' : 'disabled'}`);
|
|
400
404
|
console.log(` Multi-user: ${config.multiuser}`);
|
|
401
405
|
if (config.singleUser) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
406
|
+
const isRootPod = config.singleUserName === '/' || !config.singleUserName;
|
|
407
|
+
let details = isRootPod ? '/ (root pod)' : config.singleUserName;
|
|
408
|
+
// The "login as me" hint and password line only make sense when
|
|
409
|
+
// the built-in IdP is on. With --no-idp / external issuer there's
|
|
410
|
+
// no built-in login form, so don't imply one exists.
|
|
407
411
|
if (config.idp) {
|
|
408
|
-
if (
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
: (process.stdin.isTTY ? 'will prompt at startup' : 'missing — login disabled');
|
|
414
|
-
details += ` (password: ${pwSource})`;
|
|
415
|
-
}
|
|
412
|
+
if (isRootPod) details += ', login as "me"';
|
|
413
|
+
const pwSource = config.singleUserPassword
|
|
414
|
+
? 'provided'
|
|
415
|
+
: (process.stdin.isTTY ? 'will prompt at startup' : 'missing — login disabled');
|
|
416
|
+
details += ` (password: ${pwSource})`;
|
|
416
417
|
}
|
|
417
418
|
console.log(` Single-user: ${details}`);
|
|
418
419
|
}
|
package/src/server.js
CHANGED
|
@@ -93,7 +93,22 @@ export function createServer(options = {}) {
|
|
|
93
93
|
const inviteOnly = options.inviteOnly ?? false;
|
|
94
94
|
// Single-user mode - creates pod on startup, disables registration
|
|
95
95
|
const singleUser = options.singleUser ?? false;
|
|
96
|
-
|
|
96
|
+
// Default null = root pod (#348). Pass an explicit singleUserName
|
|
97
|
+
// to mount the pod at /<name>/ instead. Normalize the
|
|
98
|
+
// historical `'/'` / `''` forms to null up front so downstream
|
|
99
|
+
// code (remoteStoragePlugin, decorators, etc.) doesn't have to
|
|
100
|
+
// re-check for the same three shapes.
|
|
101
|
+
//
|
|
102
|
+
// Pre-#348 installs (default 'me') that upgrade in place will see
|
|
103
|
+
// a fresh empty root pod alongside their /me/ data. The fix is to
|
|
104
|
+
// pass `--single-user-name me` on restart (or move data/me/* out
|
|
105
|
+
// to the data root). At v0.0.x we accept that one-time
|
|
106
|
+
// intervention rather than carrying detection magic in the code.
|
|
107
|
+
const rawSingleUserName = options.singleUserName ?? null;
|
|
108
|
+
const singleUserName =
|
|
109
|
+
(rawSingleUserName === '/' || rawSingleUserName === '')
|
|
110
|
+
? null
|
|
111
|
+
: rawSingleUserName;
|
|
97
112
|
const singleUserPassword = options.singleUserPassword ?? null;
|
|
98
113
|
// Default storage quota per pod (50MB default, 0 = unlimited)
|
|
99
114
|
const defaultQuota = options.defaultQuota ?? 50 * 1024 * 1024;
|
|
@@ -558,8 +573,10 @@ export function createServer(options = {}) {
|
|
|
558
573
|
const baseUrl = idpIssuer?.replace(/\/$/, '') || `${protocol}://${host}:${port}`;
|
|
559
574
|
const issuer = idpIssuer || `${baseUrl}/`;
|
|
560
575
|
|
|
561
|
-
// Root
|
|
562
|
-
|
|
576
|
+
// Root pod (no name) vs named pod. After the singleUserName
|
|
577
|
+
// normalization at the top of createServer(), null is the only
|
|
578
|
+
// root-pod shape we need to recognize here.
|
|
579
|
+
const isRootPod = !singleUserName;
|
|
563
580
|
const podPath = isRootPod ? '/' : `/${singleUserName}/`;
|
|
564
581
|
const podUri = isRootPod ? `${baseUrl}/` : `${baseUrl}/${singleUserName}/`;
|
|
565
582
|
const displayName = isRootPod ? 'me' : singleUserName;
|
|
@@ -595,12 +612,22 @@ export function createServer(options = {}) {
|
|
|
595
612
|
// this, single-user + --idp produces a pod but no credential, and
|
|
596
613
|
// registration is intentionally disabled in single-user mode — so
|
|
597
614
|
// the pod is unloggable until a password is set externally (#323).
|
|
598
|
-
|
|
615
|
+
//
|
|
616
|
+
// Root pods (#348) need this too: the pod has no name, but the IDP
|
|
617
|
+
// still needs *some* username for the login form. Default to 'me'
|
|
618
|
+
// — matches the WebID fragment, fits the historical convention.
|
|
619
|
+
if (idpEnabled) {
|
|
620
|
+
// The IDP also persists `podName` and surfaces it as the
|
|
621
|
+
// `name` claim under the OIDC `profile` scope (see
|
|
622
|
+
// src/idp/accounts.js). For root pods we use 'me' here too —
|
|
623
|
+
// a null podName would leak through as a null/missing
|
|
624
|
+
// profile.name on every login, which OIDC clients expect to
|
|
625
|
+
// be a non-empty human-readable string.
|
|
599
626
|
await seedSingleUserIdpAccount({
|
|
600
627
|
fastify,
|
|
601
|
-
username: singleUserName,
|
|
628
|
+
username: isRootPod ? 'me' : singleUserName,
|
|
602
629
|
webId,
|
|
603
|
-
podName: singleUserName,
|
|
630
|
+
podName: isRootPod ? 'me' : singleUserName,
|
|
604
631
|
providedPassword: singleUserPassword
|
|
605
632
|
});
|
|
606
633
|
}
|
package/test/config.test.js
CHANGED
|
@@ -158,3 +158,35 @@ describe('config — --single-user implies --idp (#331)', () => {
|
|
|
158
158
|
'--no-idp without --single-user should not trigger the #331 warning');
|
|
159
159
|
});
|
|
160
160
|
});
|
|
161
|
+
|
|
162
|
+
// #348: the user-visible default change — `jss start --single-user`
|
|
163
|
+
// (no name flag) must produce a config where singleUserName is null,
|
|
164
|
+
// so createServer() takes the root-pod path. createServer() has its
|
|
165
|
+
// own tests but a future refactor of loadConfig() could silently
|
|
166
|
+
// restore the old `'me'` default and only the server-level tests
|
|
167
|
+
// would catch it via behaviour, not the config layer directly.
|
|
168
|
+
describe('config — singleUserName default (#348)', () => {
|
|
169
|
+
it('loadConfig() returns singleUserName=null when no flag/env is set', async () => {
|
|
170
|
+
delete process.env.JSS_SINGLE_USER_NAME;
|
|
171
|
+
const cfg = await loadConfig({}, null);
|
|
172
|
+
assert.strictEqual(cfg.singleUserName, null,
|
|
173
|
+
'default must be null (= root pod), not the legacy "me"');
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('loadConfig() preserves an explicit singleUserName CLI arg', async () => {
|
|
177
|
+
delete process.env.JSS_SINGLE_USER_NAME;
|
|
178
|
+
const cfg = await loadConfig({ singleUserName: 'alice' }, null);
|
|
179
|
+
assert.strictEqual(cfg.singleUserName, 'alice');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('loadConfig() respects JSS_SINGLE_USER_NAME from env', async () => {
|
|
183
|
+
process.env.JSS_SINGLE_USER_NAME = 'me';
|
|
184
|
+
try {
|
|
185
|
+
const cfg = await loadConfig({}, null);
|
|
186
|
+
assert.strictEqual(cfg.singleUserName, 'me',
|
|
187
|
+
'env var should restore the legacy "me" pod path on demand');
|
|
188
|
+
} finally {
|
|
189
|
+
delete process.env.JSS_SINGLE_USER_NAME;
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
package/test/idp.test.js
CHANGED
|
@@ -458,6 +458,84 @@ describe('Identity Provider - Root pod type index ACLs', () => {
|
|
|
458
458
|
});
|
|
459
459
|
});
|
|
460
460
|
|
|
461
|
+
// #348: --single-user with no name flag now defaults to a root pod
|
|
462
|
+
// (was '/me/' historically). The server-side seed must land the
|
|
463
|
+
// profile at /profile/card.jsonld, not /me/profile/card.jsonld.
|
|
464
|
+
describe('Single-user default — root pod (#348)', () => {
|
|
465
|
+
let server;
|
|
466
|
+
let baseUrl;
|
|
467
|
+
const DEFAULT_DATA_DIR = './test-data-348-default-root';
|
|
468
|
+
const ROOT_POD_PASSWORD = 'root-pod-test-pw';
|
|
469
|
+
|
|
470
|
+
before(async () => {
|
|
471
|
+
await fs.remove(DEFAULT_DATA_DIR);
|
|
472
|
+
await fs.ensureDir(DEFAULT_DATA_DIR);
|
|
473
|
+
|
|
474
|
+
const port = await getAvailablePort();
|
|
475
|
+
baseUrl = `http://${TEST_HOST}:${port}`;
|
|
476
|
+
|
|
477
|
+
server = createServer({
|
|
478
|
+
logger: false,
|
|
479
|
+
root: DEFAULT_DATA_DIR,
|
|
480
|
+
idp: true,
|
|
481
|
+
idpIssuer: baseUrl,
|
|
482
|
+
singleUser: true,
|
|
483
|
+
// singleUserName intentionally omitted — exercises the new default.
|
|
484
|
+
// Provide a password so the seeding path runs non-interactively.
|
|
485
|
+
singleUserPassword: ROOT_POD_PASSWORD,
|
|
486
|
+
forceCloseConnections: true,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
await server.listen({ port, host: TEST_HOST });
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
after(async () => {
|
|
493
|
+
await server.close();
|
|
494
|
+
await fs.remove(DEFAULT_DATA_DIR);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it('seeds the profile at /profile/card.jsonld (not /me/profile/...)', async () => {
|
|
498
|
+
const root = await fetch(`${baseUrl}/profile/card.jsonld`);
|
|
499
|
+
assert.strictEqual(root.status, 200,
|
|
500
|
+
'--single-user with no name should default to a root pod');
|
|
501
|
+
// Check the filesystem directly — an HTTP-only check could pass
|
|
502
|
+
// on a 401 even if /me/ data was somehow seeded, which would
|
|
503
|
+
// hide the regression we care about (root vs /me/ pod).
|
|
504
|
+
assert.strictEqual(await fs.pathExists(path.join(DEFAULT_DATA_DIR, 'me/profile/card.jsonld')), false,
|
|
505
|
+
'no /me/ pod files should be created when singleUserName is unset');
|
|
506
|
+
assert.strictEqual(await fs.pathExists(path.join(DEFAULT_DATA_DIR, 'me/profile/card')), false,
|
|
507
|
+
'no legacy /me/ pod files should be created either');
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('WebID resolves at the server origin', async () => {
|
|
511
|
+
const res = await fetch(`${baseUrl}/profile/card.jsonld`);
|
|
512
|
+
const body = await res.json();
|
|
513
|
+
const webId = `${baseUrl}/profile/card.jsonld#me`;
|
|
514
|
+
const matches = Array.isArray(body)
|
|
515
|
+
? body.some(n => n['@id'] === webId)
|
|
516
|
+
: body['@id'] === webId || (body['@graph'] || []).some(n => n['@id'] === webId);
|
|
517
|
+
assert.ok(matches, `profile should declare WebID ${webId}, got: ${JSON.stringify(body).slice(0, 200)}`);
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
it('seeds an IDP account for "me" so the root pod is loggable', async () => {
|
|
521
|
+
// Round-2 review of #348: a regression here would mean a fresh
|
|
522
|
+
// `jss start --single-user --idp` produces a pod nobody can log
|
|
523
|
+
// in to (registration is disabled in single-user mode, so there
|
|
524
|
+
// would be no recovery path other than out-of-band account
|
|
525
|
+
// creation). Use the credentials endpoint as a black-box login
|
|
526
|
+
// probe — if it issues a token, the seed worked.
|
|
527
|
+
const res = await fetch(`${baseUrl}/idp/credentials`, {
|
|
528
|
+
method: 'POST',
|
|
529
|
+
headers: { 'Content-Type': 'application/json' },
|
|
530
|
+
body: JSON.stringify({ username: 'me', password: ROOT_POD_PASSWORD }),
|
|
531
|
+
});
|
|
532
|
+
assert.strictEqual(res.status, 200,
|
|
533
|
+
`login as "me" should succeed for the default root pod (got ${res.status})`);
|
|
534
|
+
const body = await res.json();
|
|
535
|
+
assert.ok(body.access_token, 'response should carry an access token');
|
|
536
|
+
});
|
|
537
|
+
});
|
|
538
|
+
|
|
461
539
|
describe('Identity Provider - Accounts', () => {
|
|
462
540
|
let server;
|
|
463
541
|
let accountsUrl;
|
package/test/url.test.js
CHANGED
|
@@ -33,6 +33,14 @@ describe('getPodName', () => {
|
|
|
33
33
|
assert.strictEqual(getPodName(req), '.');
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it("returns '.' for a root pod (singleUserName null — #348 default)", () => {
|
|
37
|
+
// server.js normalizes '/' and '' to null at the top of
|
|
38
|
+
// createServer, so most root-pod requests now reach getPodName
|
|
39
|
+
// with singleUserName === null. Pin that path explicitly.
|
|
40
|
+
const req = { singleUser: true, singleUserName: null, url: '/index.html' };
|
|
41
|
+
assert.strictEqual(getPodName(req), '.');
|
|
42
|
+
});
|
|
43
|
+
|
|
36
44
|
it('returns singleUserName for a named pod, regardless of URL', () => {
|
|
37
45
|
const req = { singleUser: true, singleUserName: 'me', url: '/index.html' };
|
|
38
46
|
assert.strictEqual(getPodName(req), 'me');
|