Skip to content

Commit 5acc530

Browse files
Phase 2: enable IDP, seed me/me, localhost-only by default (#7)
* feat: enable IDP, seed me/me, localhost-only by default (#1 phase 2) The welcome page is HEAD-adaptive: it reveals "Sign in" or "Sign up" based on what the IDP advertises. With the IDP off (jspod's previous default), neither button rendered and the only visible CTA was a docs link going off-site. Turning the IDP on with single-user seeding flips that page from "read the docs" to "sign in here." - Flip default `--host` to 127.0.0.1 (was 0.0.0.0). Personal pods almost always want localhost; making this the default also lets us safely seed deliberately-weak rung-1 credentials. - Replace `--no-multiuser`-only single-user behaviour with full single-user mode: `--no-multiuser --single-user --idp --single-user-password me`. JSS hardcodes the username on root pods to 'me' (server.js:970), so credentials end up `me` / `me` — symmetric, memorable, clearly a placeholder. - Banner adds a "Sign In (rung 1 of the auth ladder)" section showing username, password, and the climb hint. References the ladder framing from issue #6. - Loud red warning if user passes `--host 0.0.0.0` (or any non-loopback host) telling them the well-known credentials are now reachable beyond localhost. - README: First Run Guide rewritten around the ladder. Rungs 0-4 table, climb instructions, and JSS_SINGLE_USER_PASSWORD escape hatch for users who want a custom rung-1 password. - Multi-user mode (`--multiuser`) still works: passes `--idp` so the IDP is available for registration, but no `--single-user-*` flags so registration stays open. - Help text: --host default updated to 127.0.0.1. - Bump jspod to 0.0.10. This addresses issue #1 phase 2, executed through the lens of issue #3 (single-user positioning) and issue #6 (auth ladder). Refs #1 #3 #6 * fix: address Copilot review on PR #7 Four legitimate catches from the review: 1. `--no-auth` was a no-op against JSS. Now forwards JSS's `--public` flag so the pod actually skips WAC and accepts unauthenticated reads/writes (verified: JSS prints "PUBLIC MODE ENABLED"). 2. `JSS_SINGLE_USER_PASSWORD` env override was documented in the README but not honoured — the CLI always passed `--single-user- password me`, drowning out the env. Now the env value is read in jspod, passed to JSS via the CLI flag, AND surfaced in the banner ("Sign In (password from JSS_SINGLE_USER_PASSWORD)") so the displayed credentials match reality. 3. Loopback detection was too strict — only 127.0.0.1 / localhost / ::1 were treated as safe. Expanded to the full 127.0.0.0/8 IPv4 range (covers Ubuntu's default 127.0.1.1 in /etc/hosts) and the bracketed `[::1]` form a user might paste in by accident. 4. README First Run Guide had duplicated Step 3 / Step 4 left over from the previous version. Renumbered and removed the obsolete "Register with a passkey" step (registration is disabled in single-user mode anyway). Refs #1 #3 #6 * fix: address Copilot review 2 on PR #7 Four follow-ups from the second pass: 1. Banner no longer prints the env-provided password verbatim. When the credential comes from JSS_SINGLE_USER_PASSWORD, the banner confirms "(hidden — set via JSS_SINGLE_USER_PASSWORD)" instead of echoing the value to stdout. Default rung-1 'me' still prints (the user doesn't know it yet, and it has no secrecy property). Avoids leaking real passwords into terminal scrollback, shell history capture, CI logs, and shared sessions. 2. `--host [::1]` (bracketed IPv6) now parses cleanly. Brackets are stripped at CLI parse time, so options.host stays canonical ('::1'), and formatUrl re-adds brackets only where URLs need them. Previously the bracketed form would round-trip through formatUrl as http://[[::1]]:PORT (invalid). 3. LAN-exposure warning no longer references a `--single-user- password` jspod flag that doesn't exist. It now points users to JSS_SINGLE_USER_PASSWORD=... or binding to 127.0.0.1. 4. README's "Default Configuration" code block previously claimed TOKEN_SECRET was "(auto)" / "auto-generated". The code actually uses a static fallback ('jspod-default-secret-change-in- production'). Documentation now matches reality. A real per-data-dir secret-generator can land in a follow-up PR. Refs #1 #3 #6 * fix: address Copilot review 3 on PR #7 Four follow-ups from the third pass: 1. Missing-value validation for flags that take an argument. `jspod --host` (or --port, --root) used to read undefined off the args array and crash with a cryptic TypeError on the next operation. Now exits cleanly with "Missing value for --host" and a pointer to --help. 2. IPv6 zone identifiers (e.g. fe80::1%lo0) are now rejected at CLI parse time with a clear error. The previous %25-encoding attempt was correct per RFC 6874 but Node's WHATWG URL parser doesn't accept zone IDs regardless — any URL we built (banner, auto-open, readiness probe) would be unparseable. Failing fast beats shipping silently broken auto-open. 3. TOKEN_SECRET now auto-generates a 48-byte random secret on first run and persists it at <root>/.token-secret with mode 0600. Subsequent restarts read the same secret so sessions and refresh tokens survive process bounces. Per-data-dir, so different pods on the same machine get different secrets. The previous static fallback ('jspod-default-secret-change-in-production') was remotely exploitable on any non-loopback bind — anyone who knew the string could forge JWTs. Env TOKEN_SECRET still wins when set, for operator-managed deployments. 4. README: replaced the literal fallback secret string with accurate docs about auto-generation, persistence path, file mode, and the env override use-case (operator-managed deployments / rotation / secret managers). Refs #1 #3 #6 * fix: address Copilot review 4 on PR #7 Four follow-ups from the fourth pass: 1. Validate --port: reject non-numeric, out-of-range (<1, >65535), and trailing-garbage values at parse time. parseInt('abc') was silently producing NaN, which then propagated to JSS as `--port NaN` and crashed the server with a confusing error. 2. Validate the persisted JWT secret on read. If <root>/.token-secret exists but is empty / whitespace-only / too short (<32 chars), warn and regenerate rather than silently weakening signing. Empty file could happen via a botched edit, a failed `cp`, or a partial write. 3. Differentiate the LAN-exposure warning based on whether the password is env-supplied. Previously the warning always claimed the server was exposing "well-known me/me credentials," even when the user had supplied a strong password via env — a false statement and a confusing one. Now: default rung-1 keeps the original well-known-credentials wording; env-supplied gets a more neutral "exposes single-user sign-in" warning that suggests strong-password discipline and HTTPS for production. 4. Stop re-exposing the env-supplied password on the JSS subprocess argv. `ps`, service-manager logs, and other local users could read what we'd carefully hidden from the banner. The fix: pass `--single-user-password` on argv only when it's the literal rung-1 placeholder ('me' has no secrecy property). For env- supplied passwords, JSS picks up JSS_SINGLE_USER_PASSWORD from process.env directly (already inherited from jspod's env spread). Verified via `ps` — JSS argv now: `--port ... --host ... --root ... --notifications --conneg --no-multiuser --single-user --idp`, no password. Refs #1 #3 #6 * fix: address Copilot review 5 on PR #7 Three follow-ups from the fifth pass: 1. requireValue() now rejects values that look like another option. `jspod --host --no-auth` previously consumed `--no-auth` as the host string and silently dropped the intended flag, then bound the server to a literal '--no-auth' hostname. Now exits cleanly with "Got: --no-auth (looks like another option, not a value)." `--port -1` is caught here too (slightly less specific than the range-check error, but still rejected before propagating). 2. .token-secret file mode is now enforced on every write, not just on creation. writeFileSync's `mode` option is silently ignored when overwriting an existing file (regeneration path), so a previously-too-loose secret file could survive a regeneration with its old permissions. New ensureMode0600() helper stats the file, warns if the mode differs, and chmod's it to 0600. 3. .token-secret mode is also tightened on every read, not just on write. A file created (or touched) before jspod 0.0.10 — or by a different process — could be group/world-readable; jspod now stats it on each start, warns the operator about the previous mode, and chmod's it. README claim of "persisted with mode 0600" now matches reality without doc edits. ensureMode0600 swallows errors so platforms without unix mode semantics (Windows) or exotic filesystems don't crash startup. Refs #1 #3 #6
1 parent daec447 commit 5acc530

3 files changed

Lines changed: 242 additions & 23 deletions

File tree

README.md

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jspod --multiuser
7979
```
8080
Options:
8181
-p, --port <number> Port to listen on (default: 5444)
82-
-h, --host <address> Host to bind to (default: 0.0.0.0)
82+
-h, --host <address> Host to bind to (default: 127.0.0.1)
8383
-r, --root <path> Data directory (default: ./pod-data)
8484
--multiuser Enable multi-user mode
8585
--no-auth Disable authentication
@@ -91,8 +91,9 @@ Options:
9191
### Environment Variables
9292

9393
```bash
94-
# Set JWT secret (recommended for production)
95-
export TOKEN_SECRET="your-secret-key-here"
94+
# JWT secret — auto-generated and persisted at <root>/.token-secret on
95+
# first run. Override here only for operator-managed deployments.
96+
export TOKEN_SECRET="$(openssl rand -base64 32)"
9697

9798
# Set environment
9899
export NODE_ENV="production"
@@ -105,7 +106,7 @@ jspod
105106

106107
**⚠️ Important**: Before deploying to production:
107108

108-
1. **Set TOKEN_SECRET**
109+
1. **TOKEN_SECRET** is auto-generated on first run and persisted at `<root>/.token-secret` (mode 0600). For operator-managed deployments — secret rotation, distributed setups, secret managers — set it explicitly via env:
109110
```bash
110111
export TOKEN_SECRET="$(openssl rand -base64 32)"
111112
```
@@ -198,10 +199,12 @@ Under the hood, jspod runs JavaScriptSolidServer with these options:
198199
```javascript
199200
{
200201
port: 5444, // Memorable, low collision with common dev servers
201-
host: '0.0.0.0', // Accept connections from anywhere
202+
host: '127.0.0.1', // Localhost-only by default (rung-1 credentials)
202203
root: './pod-data', // Local data directory
203204
multiuser: false, // Single pod per server
204-
TOKEN_SECRET: (auto) // JWT secret (auto-generated, change for production)
205+
TOKEN_SECRET: (per-pod) // Auto-generated on first run and persisted at
206+
// <root>/.token-secret (mode 0600). Override via
207+
// the TOKEN_SECRET env var for operator control.
205208
}
206209
```
207210

@@ -216,15 +219,39 @@ npx jspod
216219

217220
> Running over SSH, in CI, or in a non-interactive terminal? jspod skips auto-open and prints the URL instead. You can also pass `--no-open` to disable it explicitly.
218221
219-
**Step 3**: Register with a passkey
220-
- Click "Register" or "Sign Up"
221-
- Use your device's biometric auth (fingerprint, Face ID, etc.)
222-
- Your WebID will be created automatically
222+
**Step 3**: Sign in (rung 1 of the auth ladder)
223223

224-
**Step 4**: Start using your pod!
225-
- Upload files, create resources
226-
- Use Solid apps to connect to your pod
224+
The first time you start jspod, an IDP account is seeded with deliberately weak default credentials:
225+
226+
| Field | Value |
227+
| -------- | ----- |
228+
| Username | `me` |
229+
| Password | `me` |
230+
231+
Click **Sign in** on the welcome page, then point a Solid app (like [Pilot](https://solid-apps.github.io/pilot/)) at `http://localhost:5444` and sign in with `me` / `me`.
232+
233+
> **Why are the defaults so weak?** jspod ships you onto the first rung of the auth ladder in under a minute, then guides you up. Rung 1 is **only safe on localhost** — jspod binds to `127.0.0.1` by default for exactly this reason. Once you're in, change the password (rung 2) or add a passkey (rung 3) from your pod's account settings. See [issue #6](https://github.com/JavaScriptSolidServer/jspod/issues/6) for the ladder rationale.
234+
235+
**Step 4**: Climb the ladder
236+
237+
| Rung | Auth | How |
238+
| ---- | --------------------- | -------------------------------------------- |
239+
| 0 | None | `npx jspod --no-auth` (demos / dev only) |
240+
| 1 | `me` / `me` | **Default.** Localhost-only. |
241+
| 2 | Your password | Change it from your pod's account settings |
242+
| 3 | Passkey | Add a passkey from account settings |
243+
| 4 | Hardware key / MFA | Power-user setup |
244+
245+
Override the default password without going through the UI:
246+
247+
```bash
248+
JSS_SINGLE_USER_PASSWORD='your-password' npx jspod
249+
```
250+
251+
**Step 5**: Start using your pod
252+
- Upload files, create resources from a Solid app
227253
- Your data stays on your server
254+
- Connect more apps — they all authenticate against your IDP
228255

229256
**Troubleshooting**:
230257
- **Port in use?** Run `jspod --port 5445`

index.js

Lines changed: 201 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,19 @@ import { spawn } from 'child_process';
99
import { fileURLToPath } from 'url';
1010
import { dirname, join, delimiter } from 'path';
1111
import chalk from 'chalk';
12-
import { existsSync, mkdirSync, readFileSync } from 'fs';
12+
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync, statSync } from 'fs';
13+
import { randomBytes } from 'crypto';
1314

1415
const __dirname = dirname(fileURLToPath(import.meta.url));
1516
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
1617

1718
// Build a browser-friendly URL from a host/port pair. Normalizes wildcard
1819
// addresses (0.0.0.0, ::) to localhost and brackets IPv6 literals so the
1920
// result is always a valid URL the user (and the browser) can open.
21+
// IPv6 zone identifiers (e.g. `fe80::1%lo0`) are rejected at CLI parse
22+
// time — the WHATWG URL spec doesn't support them, so any URL we built
23+
// with one would be unparseable by Node and by the browser regardless
24+
// of `%` encoding.
2025
function formatUrl(host, port) {
2126
if (host === '0.0.0.0' || host === '::' || host === '*') {
2227
return `http://localhost:${port}`;
@@ -31,22 +36,79 @@ function formatUrl(host, port) {
3136
const args = process.argv.slice(2);
3237
const options = {
3338
port: 5444,
34-
host: '0.0.0.0',
39+
host: '127.0.0.1',
3540
root: './pod-data',
3641
multiuser: false,
3742
auth: true,
3843
open: true
3944
};
4045

46+
// Auth-ladder rung-1 credentials. See issue #6: jspod ships a deliberately
47+
// weak default sign-in so the new user is on a working pod within seconds,
48+
// with a clearly-marked path to climb (change password / add a passkey).
49+
// Safe because the default host is localhost-only (127.0.0.1).
50+
// Username is fixed by JSS for root pods (server.js:970). Password defaults
51+
// to 'me' but can be overridden via JSS_SINGLE_USER_PASSWORD so the env
52+
// override documented in the README actually takes effect (and the banner
53+
// shows the effective password, not a stale default).
54+
const RUNG_1_USERNAME = 'me';
55+
const RUNG_1_PASSWORD = process.env.JSS_SINGLE_USER_PASSWORD || 'me';
56+
const RUNG_1_PASSWORD_FROM_ENV = !!process.env.JSS_SINGLE_USER_PASSWORD;
57+
58+
// Require a value after a value-taking flag. Without this guard, a stray
59+
// `jspod --host` (no value) reads `undefined` from args[++i] and the next
60+
// .replace() call throws a cryptic TypeError. We also reject values that
61+
// look like another option (`-`-prefixed) — otherwise `jspod --host
62+
// --no-auth` would silently consume `--no-auth` as the host value, drop
63+
// the intended flag, and bind the server to a literal string '--no-auth'.
64+
function requireValue(flag, value) {
65+
if (value === undefined) {
66+
console.error(chalk.red(`✗ Missing value for ${flag}`));
67+
console.error(chalk.dim('Use --help for usage information'));
68+
process.exit(1);
69+
}
70+
if (value.startsWith('-')) {
71+
console.error(chalk.red(`✗ Missing value for ${flag}`));
72+
console.error(chalk.dim(` Got: ${value} (looks like another option, not a value)`));
73+
console.error(chalk.dim('Use --help for usage information'));
74+
process.exit(1);
75+
}
76+
return value;
77+
}
78+
4179
for (let i = 0; i < args.length; i++) {
4280
const arg = args[i];
4381

4482
if (arg === '--port' || arg === '-p') {
45-
options.port = parseInt(args[++i], 10);
83+
const raw = requireValue(arg, args[++i]);
84+
const parsed = parseInt(raw, 10);
85+
// Reject non-numeric / out-of-range / privileged ports. parseInt('abc')
86+
// returns NaN, which would silently propagate to JSS as `--port NaN`
87+
// and produce a confusing crash deep in the server.
88+
if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535 || String(parsed) !== raw.trim()) {
89+
console.error(chalk.red(`✗ Invalid port: ${raw}`));
90+
console.error(chalk.dim('Port must be an integer in the range 1-65535.'));
91+
process.exit(1);
92+
}
93+
options.port = parsed;
4694
} else if (arg === '--host' || arg === '-h') {
47-
options.host = args[++i];
95+
// Strip optional brackets from IPv6 literals so a user-friendly
96+
// `--host [::1]` paste-in stays canonical. formatUrl re-adds the
97+
// brackets where they belong in URLs; the raw host going to JSS
98+
// and to comparisons remains the unbracketed literal.
99+
const rawHost = requireValue(arg, args[++i]).replace(/^\[|\]$/g, '');
100+
// Reject IPv6 zone identifiers — WHATWG URL spec doesn't support
101+
// them, so any URL we built (banner, browser auto-open, readiness
102+
// probe) would be unparseable. Better to fail fast with a clear
103+
// message than to ship a broken auto-open silently.
104+
if (rawHost.includes('%')) {
105+
console.error(chalk.red(`✗ IPv6 zone identifiers are not supported: ${rawHost}`));
106+
console.error(chalk.dim('Bind to a non-zoned address (e.g. ::1, 127.0.0.1, or your LAN IP) instead.'));
107+
process.exit(1);
108+
}
109+
options.host = rawHost;
48110
} else if (arg === '--root' || arg === '-r') {
49-
options.root = args[++i];
111+
options.root = requireValue(arg, args[++i]);
50112
} else if (arg === '--multiuser') {
51113
options.multiuser = true;
52114
} else if (arg === '--no-auth') {
@@ -66,7 +128,7 @@ for (let i = 0; i < args.length; i++) {
66128
console.log(chalk.yellow(' jspod') + chalk.dim(' [options]\n'));
67129
console.log(chalk.white('Options:'));
68130
console.log(chalk.green(' -p, --port ') + chalk.yellow('<number>') + chalk.dim(' Port to listen on (default: 5444)'));
69-
console.log(chalk.green(' -h, --host ') + chalk.yellow('<address>') + chalk.dim(' Host to bind to (default: 0.0.0.0)'));
131+
console.log(chalk.green(' -h, --host ') + chalk.yellow('<address>') + chalk.dim(' Host to bind to (default: 127.0.0.1)'));
70132
console.log(chalk.green(' -r, --root ') + chalk.yellow('<path>') + chalk.dim(' Data directory (default: ./pod-data)'));
71133
console.log(chalk.green(' --multiuser') + chalk.dim(' Enable multi-user mode'));
72134
console.log(chalk.green(' --no-auth') + chalk.dim(' Disable authentication'));
@@ -99,6 +161,61 @@ if (!existsSync(options.root)) {
99161
mkdirSync(options.root, { recursive: true });
100162
}
101163

164+
// Resolve the JWT signing secret. Priority:
165+
// 1. TOKEN_SECRET env var (operator-controlled)
166+
// 2. Persisted random secret at <root>/.token-secret (generated on
167+
// first run, mode 0600). Same data dir always produces the same
168+
// effective secret across restarts — sessions and refresh tokens
169+
// survive process bounces.
170+
// Generating per-data-dir avoids the previous footgun of a hardcoded
171+
// fallback string that anyone could use to forge JWTs against a
172+
// non-loopback deployment.
173+
function resolveTokenSecret(rootDir) {
174+
if (process.env.TOKEN_SECRET) return process.env.TOKEN_SECRET;
175+
const secretFile = join(rootDir, '.token-secret');
176+
if (existsSync(secretFile)) {
177+
// Tighten perms on every read: writeFileSync's `mode` option only
178+
// applies to *creation*, so a regenerated file (overwritten in
179+
// place) or a manually-touched file may have inherited broader
180+
// permissions. Stat-then-chmod also warns the operator if the
181+
// file was previously group/world-readable.
182+
ensureMode0600(secretFile);
183+
const loaded = readFileSync(secretFile, 'utf8').trim();
184+
// Guard against a truncated / empty / accidentally-overwritten
185+
// secret file. A short-or-empty secret would silently weaken JWT
186+
// signing — regenerate and warn rather than ship the bad value.
187+
if (loaded.length >= 32) return loaded;
188+
console.warn(chalk.yellow(
189+
`⚠ ${secretFile} is empty or too short (${loaded.length} chars); regenerating.`
190+
));
191+
}
192+
const secret = randomBytes(48).toString('base64');
193+
writeFileSync(secretFile, secret, { mode: 0o600 });
194+
// Explicit chmod covers the overwrite case (mode option in
195+
// writeFileSync is ignored when the file already exists).
196+
ensureMode0600(secretFile);
197+
return secret;
198+
}
199+
200+
function ensureMode0600(path) {
201+
try {
202+
const mode = statSync(path).mode & 0o777;
203+
if (mode !== 0o600) {
204+
// Surface the previous mode so operators can investigate how the
205+
// file became group/world-readable (or just learn that jspod is
206+
// tightening it for them).
207+
console.warn(chalk.yellow(
208+
`⚠ Tightening permissions on ${path} (was ${mode.toString(8).padStart(3, '0')}, now 600)`
209+
));
210+
chmodSync(path, 0o600);
211+
}
212+
} catch {
213+
// chmod is a no-op on Windows and may fail on exotic filesystems.
214+
// Don't crash startup over it; the secret is still in use.
215+
}
216+
}
217+
const tokenSecret = resolveTokenSecret(options.root);
218+
102219
// Display startup banner
103220
console.log(chalk.cyan(`
104221
╔═══════════════════════════════════════════════════════════════════╗
@@ -125,6 +242,55 @@ console.log(chalk.cyan(' ├─ ') + chalk.white('Host: ') + chalk.yellow
125242
console.log(chalk.cyan(' ├─ ') + chalk.white('Pod Root: ') + chalk.yellow(options.root));
126243
console.log(chalk.cyan(' └─ ') + chalk.white('Mode: ') + (options.multiuser ? chalk.yellow('Multi-user') : chalk.yellow('Single-user')));
127244

245+
if (options.auth && !options.multiuser) {
246+
const rungLabel = RUNG_1_PASSWORD_FROM_ENV
247+
? 'Sign In (password from JSS_SINGLE_USER_PASSWORD):'
248+
: 'Sign In (rung 1 of the auth ladder):';
249+
console.log('\n' + chalk.bold.white(`🔑 ${rungLabel}\n`));
250+
console.log(chalk.cyan(' ├─ ') + chalk.white('Username: ') + chalk.bold.green(RUNG_1_USERNAME));
251+
// Only print the literal password when it's the rung-1 default. If
252+
// the user set a real password via env, echoing it to stdout would
253+
// leak into terminal scrollback, shell history capture, CI logs, and
254+
// shared sessions. They already know the value they set; the banner
255+
// just confirms it was picked up.
256+
if (RUNG_1_PASSWORD_FROM_ENV) {
257+
console.log(chalk.cyan(' ├─ ') + chalk.white('Password: ') + chalk.dim('(hidden — set via JSS_SINGLE_USER_PASSWORD)'));
258+
} else {
259+
console.log(chalk.cyan(' ├─ ') + chalk.white('Password: ') + chalk.bold.green(RUNG_1_PASSWORD));
260+
}
261+
console.log(chalk.cyan(' └─ ') + chalk.dim('Climb: change the password or add a passkey from account settings'));
262+
263+
// Loud warning if the rung-1 known credentials are reachable beyond
264+
// the local machine. See issue #6 ("auth ladder"): rung 1 is only
265+
// safe when the host is loopback-only. Any other bind exposes the
266+
// well-known me/me credentials to the LAN (or worse).
267+
// Loopback covers the full 127.0.0.0/8 IPv4 range plus IPv6 ::1.
268+
// (Bracketed `[::1]` input is stripped to `::1` at CLI parse time
269+
// — see options.host parsing — so it matches here without a
270+
// bracketed branch.)
271+
const isLoopback =
272+
options.host === 'localhost' ||
273+
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(options.host) ||
274+
options.host === '::1';
275+
if (!isLoopback) {
276+
if (RUNG_1_PASSWORD_FROM_ENV) {
277+
// Custom password from env. Still worth warning the user that
278+
// their sign-in is now reachable from anywhere this host
279+
// answers, but no longer accurate to call the credentials
280+
// "well-known."
281+
console.log('\n' + chalk.bold.red('⚠ Warning: ') + chalk.yellow(
282+
`--host ${options.host} exposes single-user sign-in beyond localhost.`
283+
));
284+
console.log(chalk.dim(' Make sure your JSS_SINGLE_USER_PASSWORD is strong, and use HTTPS in production.'));
285+
} else {
286+
console.log('\n' + chalk.bold.red('⚠ Warning: ') + chalk.yellow(
287+
`--host ${options.host} exposes the well-known me/me credentials beyond localhost.`
288+
));
289+
console.log(chalk.dim(' Set JSS_SINGLE_USER_PASSWORD=... before running, or bind to 127.0.0.1.'));
290+
}
291+
}
292+
}
293+
128294
console.log('\n' + chalk.bold.white('✨ Features:\n'));
129295
console.log(chalk.cyan(' ├─ ') + chalk.green('Solid Protocol ') + chalk.bold.green('✓'));
130296
console.log(chalk.cyan(' ├─ ') + chalk.green('WebID Auth ') + (options.auth ? chalk.bold.green('✓') : chalk.dim('✗')));
@@ -150,8 +316,34 @@ const jssArgs = [
150316
'--conneg'
151317
];
152318

153-
if (!options.multiuser) {
154-
jssArgs.push('--no-multiuser');
319+
if (options.multiuser) {
320+
// Multi-user mode is an explicit opt-out from jspod's single-user
321+
// positioning (#3). The IDP stays available so users can register.
322+
if (options.auth) jssArgs.push('--idp');
323+
} else {
324+
// Default: single-user personal pod with rung-1 credentials seeded.
325+
// The pod, IDP, and known credentials are created on first start;
326+
// every subsequent start is a no-op (JSS is idempotent on the seed).
327+
jssArgs.push('--no-multiuser', '--single-user');
328+
if (options.auth) {
329+
jssArgs.push('--idp');
330+
// Pass the rung-1 placeholder on argv (it has no secrecy property
331+
// — anyone reading the docs already knows the literal 'me'). For
332+
// an env-supplied password, *don't* re-expose it on argv where
333+
// `ps`, service-manager logs, and other local users can read it.
334+
// JSS reads JSS_SINGLE_USER_PASSWORD from env directly when no
335+
// CLI flag is given, and we forward process.env to the child.
336+
if (!RUNG_1_PASSWORD_FROM_ENV) {
337+
jssArgs.push('--single-user-password', RUNG_1_PASSWORD);
338+
}
339+
}
340+
}
341+
342+
if (!options.auth) {
343+
// JSS's `--public` is the real no-auth switch: skip WAC, open
344+
// read/write. Without it, `--no-auth` would only mean "no IDP"
345+
// — the pod would still be ACL-gated and unreachable.
346+
jssArgs.push('--public');
155347
}
156348

157349
// Start JSS with enhanced PATH to find the binary
@@ -160,7 +352,7 @@ const jss = spawn('jss', jssArgs, {
160352
env: {
161353
...process.env,
162354
PATH: `${join(__dirname, 'node_modules', '.bin')}${delimiter}${process.env.PATH}`,
163-
TOKEN_SECRET: process.env.TOKEN_SECRET || 'jspod-default-secret-change-in-production',
355+
TOKEN_SECRET: tokenSecret,
164356
NODE_ENV: process.env.NODE_ENV || 'development'
165357
}
166358
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jspod",
3-
"version": "0.0.9",
3+
"version": "0.0.10",
44
"description": "JavaScript Solid Pod - Just works, batteries included",
55
"type": "module",
66
"main": "index.js",

0 commit comments

Comments
 (0)