Skip to content

Phase 2: enable IDP, seed me/me, localhost-only by default - #7

Merged
melvincarvalho merged 6 commits into
gh-pagesfrom
issue-1-phase-2-enable-idp
May 16, 2026
Merged

melvincarvalho merged 6 commits into
gh-pagesfrom
issue-1-phase-2-enable-idp

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Summary

Phase 2 of #1, executed through the lens of #3 (single-user positioning) and #6 (auth ladder). The welcome page that previously had only a stale docs-link CTA now reveals a real Sign in button, and a fresh npx jspod lands the user inside the IDP flow within seconds.

Root cause of the previous "Welcome → docs link" page

JSS's server-root template at / is already HEAD-adaptive: it probes ./idp/register and reveals Sign up / Sign in buttons based on the response (200 → both, 403 → Sign in only, 404 / error → neither). With the IDP disabled in jspod's previous default spawn, neither button rendered and the only visible CTA was the external jss.live/docs/getting-started/first-run link.

What changes

  • --host 127.0.0.1 is the new default (was 0.0.0.0). Personal pods almost always want localhost-only, and this is what makes seeding rung-1 credentials safe.
  • Default spawn now enables single-user + IDP: --no-multiuser --single-user --idp --single-user-password me.
  • Username is me / password is me. JSS hardcodes the username for root pods (server.js:970: username: isRootPod ? 'me' : singleUserName); jspod seeds the password to match. Symmetric, memorable, obviously a placeholder.
  • Banner adds a "Sign In (rung 1 of the auth ladder)" section showing credentials and the climb hint.
  • Loud warning if the user passes --host 0.0.0.0 (or any non-loopback host) — the well-known credentials are then reachable beyond localhost and need to be changed.
  • --no-auth still produces a no-auth server with no sign-in block and no IDP.
  • --multiuser still works: the IDP is enabled (so users can register) but no single-user seeding happens.
  • README First Run Guide rewritten around the auth-ladder framing (rungs 0-4 table, climb instructions, JSS_SINGLE_USER_PASSWORD escape hatch).
  • Bump jspod to 0.0.10.

Acceptance criteria (from #1 phase 2, adapted via #6)

  • Fresh npx jspod + browser visit to localhost:5444 shows a working welcome page with Sign in revealed (HEAD probe returns 403 → "Sign in" branch)
  • npx jspod --no-auth produces a no-auth server (current bare behaviour)
  • --host 0.0.0.0 still works, but prints a clear LAN-exposure warning
  • Single-user is the default per Positioning: jspod is the single-user personal-pod layer (not a multi-user server) #3 positioning; multi-user still available via --multiuser

Test plan

  • Default spawn: banner shows Sign In rung 1, HEAD /idp/register returns 403, GET /idp returns 200 with Solid-app-friendly copy
  • --host 0.0.0.0: loud red warning about exposed credentials
  • --no-auth: no Sign In block, WebID Auth ✗ in features, no warning
  • Auto-open from Phase 3 still works (didn't touch that code)
  • Manual TTY check by reviewer: run npx jspod from a real terminal, click Sign in on the welcome page, point a Solid app (e.g. Pilot at https://solid-apps.github.io/pilot/) at the server, sign in with me / me, confirm pod access.

What this doesn't do (deferred to follow-up PRs)

  • Climb-the-ladder UI nudge (rung-1 pill or post-sign-in "add a passkey" CTA in mashlib) — requires either a JSS PR or a jspod-injected script. Substantial change, separate PR.
  • Phase 4 seed content (/profile, /public/welcome, /inbox) — still a gap; targeted next.
  • Phase 5 visual assets (README GIF + screenshot) — easier to record now that the flow actually works end-to-end.

Cross-references

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates jspod’s first-run experience to land users in an IDP-backed single-user flow by default, binding to localhost for safety and documenting the “auth ladder” onboarding story.

Changes:

  • Default host binding changed to 127.0.0.1 (localhost-only by default).
  • Default single-user startup now enables IDP and seeds rung-1 credentials (me / me), with a banner and LAN-exposure warning for non-loopback binds.
  • README first-run guide rewritten around the auth-ladder concept; package version bumped to 0.0.10.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
README.md Updates defaults + adds auth-ladder / first-run sign-in guidance.
index.js Changes default host, adds rung-1 credential banner + warning, and adjusts JSS spawn args for single-user vs multi-user.
package.json Bumps version to 0.0.10.
Comments suppressed due to low confidence (1)

README.md:252

  • This section still instructs users to “Register with a passkey” immediately after the new default flow tells them to sign in with the seeded me/me account. In single-user mode HEAD /idp/register is expected to return 403 (Sign in only), so these registration instructions may not be actionable for the default path. Consider scoping this to multi-user mode (or explaining when registration is available).
**Step 3**: Register with a passkey
- Click "Register" or "Sign Up"
- Use your device's biometric auth (fingerprint, Face ID, etc.)
- Your WebID will be created automatically


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread index.js
Comment on lines +182 to +186
if (options.multiuser) {
// Multi-user mode is an explicit opt-out from jspod's single-user
// positioning (#3). The IDP stays available so users can register.
if (options.auth) jssArgs.push('--idp');
} else {
Comment thread index.js
Comment on lines +190 to +193
jssArgs.push('--no-multiuser', '--single-user');
if (options.auth) {
jssArgs.push('--idp', '--single-user-password', RUNG_1_PASSWORD);
}
Comment thread index.js
Comment on lines +145 to +149
const isLoopback =
options.host === '127.0.0.1' ||
options.host === 'localhost' ||
options.host === '::1';
if (!isLoopback) {
Comment thread README.md
Comment on lines +219 to +233
**Step 3**: Sign in (rung 1 of the auth ladder)

The first time you start jspod, an IDP account is seeded with deliberately weak default credentials:

| Field | Value |
| -------- | ----- |
| Username | `me` |
| Password | `me` |

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`.

> **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.

**Step 4**: Climb the ladder

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
@melvincarvalho

Copy link
Copy Markdown
Contributor Author

Thanks @copilot-pull-request-reviewer — all four were legitimate. Pushed 89d9fe7:

  1. --no-auth no-op against JSS → now forwards JSS's --public flag so the pod actually skips WAC. Verified: JSS prints "PUBLIC MODE ENABLED" at startup.

  2. JSS_SINGLE_USER_PASSWORD override ineffective → jspod now reads the env, passes it to JSS via --single-user-password, and surfaces the source in the banner header ("Sign In (password from JSS_SINGLE_USER_PASSWORD):") so the displayed credentials match reality. Verified with JSS_SINGLE_USER_PASSWORD=hunter2 npx jspod → banner shows hunter2.

  3. Loopback detection too strict → expanded to full 127.0.0.0/8 (catches Ubuntu's default 127.0.1.1 in /etc/hosts and any other 127.x.x.x) plus the bracketed [::1] form. Verified: --host 127.0.1.1 → no warning, --host 192.168.1.1 → warning fires.

  4. README step duplication → renumbered. The leftover "Register with a passkey" step (which was wrong anyway since registration is disabled in single-user mode) is removed; the climb-the-ladder step now flows naturally into the "Start using your pod" step.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comments suppressed due to low confidence (1)

README.md:205

  • The README claims TOKEN_SECRET is “(auto)” and “auto-generated”, but index.js currently sets a fixed default (jspod-default-secret-change-in-production) when TOKEN_SECRET is not provided. This is both inaccurate documentation and potentially insecure if the server is ever bound beyond localhost; either generate a random secret per data directory (persist it) or update the README to reflect the actual behavior.
{
  port: 5444,              // Memorable, low collision with common dev servers
  host: '127.0.0.1',       // Localhost-only by default (rung-1 credentials)
  root: './pod-data',      // Local data directory
  multiuser: false,        // Single pod per server
  TOKEN_SECRET: (auto)     // JWT secret (auto-generated, change for production)
}

Comment thread index.js Outdated
console.log('\n' + chalk.bold.red('⚠ Warning: ') + chalk.yellow(
`--host ${options.host} exposes the well-known me/me credentials beyond localhost.`
));
console.log(chalk.dim(' Set --single-user-password via env (JSS_SINGLE_USER_PASSWORD) or run on 127.0.0.1.'));
Comment thread index.js
Comment on lines +140 to +147
if (options.auth && !options.multiuser) {
const rungLabel = RUNG_1_PASSWORD_FROM_ENV
? 'Sign In (password from JSS_SINGLE_USER_PASSWORD):'
: 'Sign In (rung 1 of the auth ladder):';
console.log('\n' + chalk.bold.white(`🔑 ${rungLabel}\n`));
console.log(chalk.cyan(' ├─ ') + chalk.white('Username: ') + chalk.bold.green(RUNG_1_USERNAME));
console.log(chalk.cyan(' ├─ ') + chalk.white('Password: ') + chalk.bold.green(RUNG_1_PASSWORD));
console.log(chalk.cyan(' └─ ') + chalk.dim('Climb: change the password or add a passkey from account settings'));
Comment thread index.js Outdated
Comment on lines +153 to +159
// Loopback covers the full 127.0.0.0/8 IPv4 range plus IPv6 ::1
// (and its bracketed form, which a user might paste in by accident).
const isLoopback =
options.host === 'localhost' ||
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(options.host) ||
options.host === '::1' ||
options.host === '[::1]';
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
@melvincarvalho

Copy link
Copy Markdown
Contributor Author

Thanks @copilot-pull-request-reviewer — all four addressed in f6458f2:

  1. Banner leaking env password: when JSS_SINGLE_USER_PASSWORD is set, the banner now shows (hidden — set via JSS_SINGLE_USER_PASSWORD) instead of the literal value. Default me still prints (no secrecy property; user doesn't know it yet). Verified: JSS_SINGLE_USER_PASSWORD=topsecret123 npx jspod → 0 occurrences of topsecret123 in stdout.

  2. [::1] double-bracketing: brackets are now stripped at CLI parse time, so options.host stays canonical (::1) and formatUrl re-adds brackets exactly once. Verified: --host [::1] → URL renders as http://[::1]:PORT, Host field shows ::1, no warning (loopback recognized).

  3. Warning referenced a flag jspod doesn't expose: reworded to point users to JSS_SINGLE_USER_PASSWORD=... or binding to 127.0.0.1. No more phantom --single-user-password mention.

  4. README TOKEN_SECRET: (auto) was incorrect (low-confidence comment, but legitimate): code uses a static fallback jspod-default-secret-change-in-production, not auto-generation. README now matches reality. A real per-data-dir secret generator is a separate follow-up.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread index.js Outdated
Comment on lines +59 to +63
// Strip optional brackets from IPv6 literals so a user-friendly
// `--host [::1]` paste-in stays canonical. formatUrl re-adds the
// brackets where they belong in URLs; the raw host going to JSS
// and to comparisons remains the unbracketed literal.
options.host = args[++i].replace(/^\[|\]$/g, '');
Comment thread index.js
Comment on lines +59 to 66
// Strip optional brackets from IPv6 literals so a user-friendly
// `--host [::1]` paste-in stays canonical. formatUrl re-adds the
// brackets where they belong in URLs; the raw host going to JSS
// and to comparisons remains the unbracketed literal.
options.host = args[++i].replace(/^\[|\]$/g, '');
} else if (arg === '--root' || arg === '-r') {
options.root = args[++i];
} else if (arg === '--multiuser') {
Comment thread index.js
Comment on lines +170 to +179
const isLoopback =
options.host === 'localhost' ||
/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(options.host) ||
options.host === '::1';
if (!isLoopback) {
console.log('\n' + chalk.bold.red('⚠ Warning: ') + chalk.yellow(
`--host ${options.host} exposes the well-known me/me credentials beyond localhost.`
));
console.log(chalk.dim(' Set JSS_SINGLE_USER_PASSWORD=... before running, or bind to 127.0.0.1.'));
}
Comment thread README.md
Comment on lines 198 to 206
```javascript
{
port: 5444, // Memorable, low collision with common dev servers
host: '0.0.0.0', // Accept connections from anywhere
host: '127.0.0.1', // Localhost-only by default (rung-1 credentials)
root: './pod-data', // Local data directory
multiuser: false, // Single pod per server
TOKEN_SECRET: (auto) // JWT secret (auto-generated, change for production)
TOKEN_SECRET: 'jspod-default-secret-change-in-production'
// Static fallback. Override via env for any non-local use.
}
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
@melvincarvalho

Copy link
Copy Markdown
Contributor Author

Thanks @copilot-pull-request-reviewer — all four addressed in 8de8cbd:

  1. Missing-value crash on bare flags: added requireValue(flag, val) helper called from --port, --host, and --root parsing. jspod --host now exits cleanly with "Missing value for --host" instead of throwing TypeError on undefined.replace(). Verified for all three flags.

  2. IPv6 zone identifiers: tried the %25 encoding route first and confirmed Node's WHATWG URL parser still rejects the result (zone IDs aren't in the spec). Switched to explicit rejection at CLI parse time with a clear error pointing the user to a non-zoned alternative. Valid IPv6 (--host [::1], --host ::1) still works end-to-end.

  3. TOKEN_SECRET static fallback was remotely forgeable: replaced with resolveTokenSecret(rootDir) — generates a 48-byte random secret on first run, persists at <root>/.token-secret (mode 0600), reads it on subsequent starts so sessions/refresh-tokens survive process bounces. Per-data-dir, so different pods on the same machine get different secrets. Env TOKEN_SECRET still wins when set (operator-managed deployments). Verified: file written with 0600, second start reuses same secret, env override bypasses file creation.

  4. README publishing the literal fallback secret: replaced with accurate auto-generation docs (path, mode, persistence behaviour, when to override via env). The literal string no longer appears anywhere in the repo.

The static fallback string is gone from both the code and the docs — no more "publishing the password" footgun.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment thread index.js Outdated
Comment on lines +74 to +75
if (arg === '--port' || arg === '-p') {
options.port = parseInt(args[++i], 10);
options.port = parseInt(requireValue(arg, args[++i]), 10);
Comment thread index.js
Comment on lines +155 to +160
function resolveTokenSecret(rootDir) {
if (process.env.TOKEN_SECRET) return process.env.TOKEN_SECRET;
const secretFile = join(rootDir, '.token-secret');
if (existsSync(secretFile)) {
return readFileSync(secretFile, 'utf8').trim();
}
Comment thread index.js Outdated
Comment on lines +223 to +227
if (!isLoopback) {
console.log('\n' + chalk.bold.red('⚠ Warning: ') + chalk.yellow(
`--host ${options.host} exposes the well-known me/me credentials beyond localhost.`
));
console.log(chalk.dim(' Set JSS_SINGLE_USER_PASSWORD=... before running, or bind to 127.0.0.1.'));
Comment thread index.js Outdated
// every subsequent start is a no-op (JSS is idempotent on the seed).
jssArgs.push('--no-multiuser', '--single-user');
if (options.auth) {
jssArgs.push('--idp', '--single-user-password', RUNG_1_PASSWORD);
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
@melvincarvalho

Copy link
Copy Markdown
Contributor Author

Thanks @copilot-pull-request-reviewer — all four addressed in 27e301f:

  1. --port NaN / out-of-range silently propagated: now validated at parse time. --port abc, --port 99999, --port 0, and --port 1.5 all exit cleanly with "Invalid port: ... Port must be an integer in the range 1-65535." Valid ports (1-65535) start normally.

  2. .token-secret empty/short regeneration: resolveTokenSecret now requires the loaded file to be ≥32 chars. Anything shorter (or whitespace-only) prints a yellow warning and regenerates a fresh 48-byte secret in place. Verified by truncating the file to 0 bytes: warning fires, file is rewritten to 64 chars on next start.

  3. Misleading "well-known me/me" warning on env-supplied password: warning is now conditional on RUNG_1_PASSWORD_FROM_ENV. Default rung-1 keeps the well-known-credentials wording; env-supplied gets a neutral "exposes single-user sign-in" warning that suggests strong-password discipline + HTTPS for production. The previous text falsely claimed me/me was being exposed even when the operator had supplied a strong password.

  4. Env password leaked via argv to ps: this is the big one — thanks for catching it. jspod no longer passes --single-user-password on argv when the value came from JSS_SINGLE_USER_PASSWORD. JSS reads the env var directly when the CLI flag is absent (per jss start --help), and jspod already forwards process.env to the subprocess. The literal me placeholder still passes via argv (no secrecy property — it's documented). Verified via ps — JSS argv is now just --port ... --host ... --root ... --notifications --conneg --no-multiuser --single-user --idp with no password, regardless of whether the user supplied one.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread index.js
Comment on lines +62 to +69
function requireValue(flag, value) {
if (value === undefined) {
console.error(chalk.red(`✗ Missing value for ${flag}`));
console.error(chalk.dim('Use --help for usage information'));
process.exit(1);
}
return value;
}
Comment thread index.js
Comment on lines +165 to +180
function resolveTokenSecret(rootDir) {
if (process.env.TOKEN_SECRET) return process.env.TOKEN_SECRET;
const secretFile = join(rootDir, '.token-secret');
if (existsSync(secretFile)) {
const loaded = readFileSync(secretFile, 'utf8').trim();
// Guard against a truncated / empty / accidentally-overwritten
// secret file. A short-or-empty secret would silently weaken JWT
// signing — regenerate and warn rather than ship the bad value.
if (loaded.length >= 32) return loaded;
console.warn(chalk.yellow(
`⚠ ${secretFile} is empty or too short (${loaded.length} chars); regenerating.`
));
}
const secret = randomBytes(48).toString('base64');
writeFileSync(secretFile, secret, { mode: 0o600 });
return secret;
Comment thread README.md
**⚠️ Important**: Before deploying to production:

1. **Set TOKEN_SECRET**
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:
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
@melvincarvalho

Copy link
Copy Markdown
Contributor Author

Thanks @copilot-pull-request-reviewer — all three addressed in fbafde7:

  1. requireValue consumed flag-shaped next-tokens: now rejects any value that starts with -. jspod --host --no-auth exits with "Got: --no-auth (looks like another option, not a value)" instead of binding the server to a literal --no-auth hostname. Side effect: --port -1 is also caught here, slightly earlier than the range-check (still rejected, message is "looks like another option" rather than "invalid port" — acceptable IMO).

  2. 0600 mode only applied on file creation: extracted ensureMode0600(path) helper that stat's the file, warns if the mode isn't already 0600, and chmod's it. Called after every writeFileSync (covers the regeneration / overwrite case where the mode option in writeFileSync is silently ignored).

  3. Existing files with broader perms also tightened: ensureMode0600 runs on read too, so a .token-secret left over from before this PR — or touched manually with broader perms — gets tightened on first start, with a warning surfacing the previous mode (e.g. "was 644, now 600"). README claim of "mode 0600" now matches reality without doc changes.

Errors from chmod/stat are swallowed so platforms without unix mode semantics (Windows) or exotic filesystems don't crash startup. Verified: 644 → 600 with warning on both read and regenerate paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants