Skip to content

Gate Verified behind email confirmation or acknowledged 2FA codes - #4

Closed
mooserini wants to merge 28 commits into
Awassee:mainfrom
mooserini:ara/verified-either-or
Closed

mooserini wants to merge 28 commits into
Awassee:mainfrom
mooserini:ara/verified-either-or

Conversation

@mooserini

Copy link
Copy Markdown

Verified now means one of two honest things: a confirmed recovery email, or 2FA active with recovery codes explicitly acknowledged.

What changes:

  • New acknowledge_codes settings action + I've-saved-my-codes button until clicked; clicking verifies the account.
  • Confirming 2FA alone no longer verifies. Fresh code sets (re-setup, regen, admin reset) start unacknowledged.
  • Disabling 2FA or regenerating codes recomputes Verified, but only revokes verification the codes flow granted — admin hand-verify and bootstrap grants are never touched.

Tests: 5 new (all watched fail first), full web + auth suites green. Local Codex review findings (2x P2) addressed in-branch.

Follow-ups intentionally out of scope: SSH-side parity, sysop lockout-hatch email on repeated failures, CSV export sort-glyph leak.

mooserini and others added 28 commits September 29, 2026 15:52
Keeps this fork's bootstrap pinned to the installer in the same
checkout. Original bootstrap by Sean Heiney (Awassee/wolfbbs).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Back up .env before it is overwritten or amended (only when it changes)
- Require typing WIPE to overwrite .env, delete data volumes, or remove the
  install directory; --yes no longer auto-confirms these (use
  --i-understand-data-loss for automation)
- Default the bare-run menu to Status when a WolfBBS stack is running

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t-in

- Publish all ports on WOLFBBS_BIND_ADDR (default 127.0.0.1) so a local
  reverse proxy/tunnel cannot be bypassed from the LAN
- restart: unless-stopped on every service so the board survives reboots
- Move the bbs Docker socket and host mounts to docker-compose.app-upgrade.yml,
  layered in by the installer only when WOLFBBS_ENABLE_APP_UPGRADE=true
- Document network exposure, restarts, and in-BBS upgrades in INSTALL.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Callers set a recovery email in /settings, confirmed by a 24h single-use
  link; password resets go to the confirmed address
- The board host, its parent domain, and WOLFBBS_RECOVERY_EMAIL_BLOCKED_DOMAINS
  are refused as recovery addresses and never used as reset recipients
- deploy/cloudflare-email-worker: Email Routing Worker that posts inbound mail
  to wolfbbs-mailin through a tunnel, bouncing unknown handles
- Docs for free inbound (Email Routing) and outbound (free SMTP relay) setup

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Read-only mode blocked every admin save, including the toggle that turns it
off, so the only recovery was editing the database and restarting web.

- Add an exit_read_only action (admin role + CSRF) that is allowed while
  read-only is on, with a banner button on /admin/config and an audit entry
- Point the 403 message at /admin/config
- Document the button and the database fallback in admin.md and
  TROUBLESHOOTING.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…olors

The old stylesheet layered several theme and accent modes, saved per browser,
over hard-coded light-theme colors. Browsers on different saved modes looked
completely different, and some text (the guide-strip heading, the Search
button label) rendered invisibly on dark panels.

- Wrap the legacy stylesheet in @layer wolfbbs-legacy and add an unlayered
  brand layer, so brand colors and type win regardless of specificity
- Brand layer uses only BRAND_SPEC v1.0 colors: ink/cream foundation, chrome
  buttons, copper rules, and green/yellow/red/magenta accents for meaning
- PC6300 for headings, chrome and labels only; system sans for body; Menlo
  for code. The web font is embedded and served from /assets/fonts/
  (CC BY-SA 4.0, attribution included)
- Drop !important from legacy paint declarations so the layer order holds

Fixes #2

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
External email requires a verified account, but nothing ever verified the
bootstrap sysop, so the operator configuring SMTP was blocked by their own
prerequisite. "Verified" also had no path besides a sysop checkbox.

- Mark the bootstrap sysop verified on creation and on every start
- Confirming a recovery email marks the account verified, since it proves
  control of an outside mailbox

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Two sources of truth for the name (installer .env vs. /admin/config) left the
board with a split identity, and nothing prompted a fork to stop calling
itself WolfBBS.

- identity.env holds the public name and repo; the installer uses it for
  banners, FIRST_STEPS, and the default WOLFBBS_BBS_NAME
- A bare bootstrap on a fork that still says WolfBBS asks for a name and
  saves it to identity.env
- Web, SSH, IRC, and login banners fall back to WOLFBBS_BBS_NAME, now passed
  to the bbs, web, and irc containers; neutral wording replaces hard-coded
  WolfBBS in the web UI
- Internals (WOLFBBS_* settings, wolfbbs Docker project and volume, binaries,
  Go module) keep the WolfBBS name as provenance
- README leads with ResoNET BBS and credits WolfBBS; LICENSE keeps the
  original notice and adds a line for fork changes
- Fix prompt_default printing its prompt into the captured answer, which
  corrupted install dir, ports, and repo edits in the guided install menu
- Update installer regressions for the WIPE guard and assert --yes alone no
  longer deletes volumes

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The web UI had no favicon. Crop the Resonant Mirror emblem (the rings and
light line, without the wordmark, which is unreadable at tab size) and serve:

- /favicon.ico with 16, 32 and 48 px images
- /assets/icons/icon-32, -192, -512 PNGs and a 180 px apple-touch-icon
- icon links injected into every page head unless a page sets its own

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Replace the no-op Theme menu button (the brand layer overrides the old
  themes) with a Phosphor switcher using the portfolio's five palettes
  (P1 green, P3 amber, CGA cyan, Hermes bronze, Resonant violet); it colors
  CRT surfaces only, so browsers can't drift apart again
- Add a CRT banner to /start and /connect: the board name on a phosphor
  screen with scanlines, plus PALETTE and EMBLEM/BANNER toggles
- Add the portfolio's thin blue-violet-pink-amber strips along the page top,
  page header, banner and nav row
- Stop the recent-pages rail filling with copies of one page: key entries by
  path without one-shot ?notice=/?error= params
- Expose the board name to scripts via <meta name="application-name">

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The table tools (filter, CSV/JSON export, columns), sticky/compact toggles,
row inspector, "On this page" counters and the diagnostic/bug/feedback
buttons are useful self-debugging aids but showed on every page for everyone.

- Hide them by default; moderators and sysops get a "Sysop tools" checkbox at
  the top left that shows them (remembered per browser)
- Pass the viewer's role and the live /admin/config site name to page scripts
  via <meta> tags (the CRT banner now uses the live name too)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
"Enable TOTP" switched 2FA on in one click without ever showing the secret,
so nobody could produce a code and the next login failed. Codes are app-based
and never emailed, which made the lockout look like missing mail.

- Setup is now two steps: show a QR code (rsc.io/qr) and the key, and only
  turn 2FA on after a working code is entered; pending setups expire after
  15 minutes and can be cancelled
- Sysops get a "Reset 2FA" button per account in /admin/users (audited)
- bootstrap.sh --reset-2fa <handle> clears 2FA through the local Docker
  socket, for when the sysop is the one locked out
- Document all three in TROUBLESHOOTING.md and INSTALL.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The runtime reads doors/ and menus/ relative to /app, but neither Dockerfile
copied them, so every Docker install started with zero doors (the launch
check "Doors available" failed) and no menu file.

- COPY doors and menus into /app in Dockerfile and Dockerfile.bundle
- Include both in release bundles built by package-dist.sh

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing backed up the database (users, boards, messages, and every setting
saved in /admin/config and /admin/gateways) or .env.

- --backup writes <prefix>/backups/<timestamp>/ (mode 700): pg_dump custom
  format, a .env copy, and a MANIFEST with commit and checksum; keeps the
  newest WOLFBBS_BACKUP_KEEP (default 14); removes incomplete dumps
- --restore <dir> requires typed WIPE, takes a safety backup first (aborts if
  that fails), stops the app containers, pg_restore --clean, starts them
- Document both in OPERATIONS.md and INSTALL.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Line input saved/restored the cursor with the SCO sequences CSI s / CSI u,
  which macOS Terminal.app ignores, so every keystroke re-printed the whole
  line ("Handle: MMoMooMoos..."). Use DEC ESC 7 / ESC 8, which every
  VT100-compatible terminal supports.
- The Door Cockpit web page now says plainly that doors are played in the
  terminal over SSH (press D), since the page is a catalog and scoreboard.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Show localhost URLs when ports are bound to 127.0.0.1 (the default), plus
  the public hostname saved in /admin/config; the LAN IP URLs didn't work
- Read the live site name/hostname from the database, not just .env
- Add a "Where things live" block: app source, compose file, .env, admin
  settings, database volume, backups, install folder; drop the "Managed app
  dir" line unless that folder exists (it's only used by curl | bash installs)
- Replace the menu *.bak check, which could never pass on Docker installs
  (the editor writes inside the container), with "backup in the last 7 days"
- FIRST_STEPS.txt: resolve the host before building URLs and honor the
  loopback bind, so all its links agree

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After a line ended on a bare CR (what macOS Terminal.app sends for Enter),
readLine peeked for a trailing LF, which blocks until the next keystroke. So
the Password prompt didn't appear after Enter; pressing Enter again then gave
an empty password, "Missing input", and the next key went back to Handle.

- Only look for a trailing LF/NUL if it's already buffered; otherwise
  remember the bare CR and swallow a late LF/NUL at the start of the next
  read (readLine and readKey)
- An empty password asks for the password again (up to 3 times) instead of
  returning to the Handle prompt
- Tests for bare CR, late LF, and two empty Enters

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
/terminal serves an xterm.js page, and /terminal/ws bridges its WebSocket
into the BBS SSH listener from inside the web container, so browser callers
get the full ANSI board, login screen and doors with no SSH client. The
backquote key drops the same terminal down over any site page.

The bridge adds no auth: callers still sign in at the BBS login screen, and
the socket only upgrades for same-host origins. With a shared
WOLFBBS_TERMINAL_BRIDGE_SECRET the BBS records the caller's real IP instead
of the web container's.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The SSH server never set a host key, so gliderlabs generated a new one on
every start and clients warned "REMOTE HOST IDENTIFICATION HAS CHANGED"
after each rebuild.

- Load the host key from WOLFBBS_SSH_HOST_KEY (default
  .wolfbbs/ssh/ssh_host_ed25519_key), creating an ed25519 key (mode 600) on
  first start; a corrupt key is reported, never overwritten
- Keep it on a new "sshkeys" Docker volume so rebuilds don't replace it
- --backup saves a copy of the host key
- Document it in OPERATIONS.md

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…cally

- /app upgrade: when the opt-in Docker socket and host mounts aren't present
  (the default since the socket became opt-in), say so and point to
  `bash bootstrap.sh --rapid-upgrade` instead of running a command that can
  only fail with "chdir /wolfbbs-host: no such file or directory"
- Doors: reaching a door's per-session time limit (max_run_seconds) now shows
  "Time's up for this door session" and is recorded as door_timeout /
  a "timeout" door event, not door_failure "context deadline exceeded"

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The welcome panel hardcoded "WolfBBS Welcome", "wolfbbs (c) 2026" and a
wolf. It now uses the configured site name (site.name / WOLFBBS_BBS_NAME)
and the current year, and draws the ring-and-beam emblem from the web
brand in ANSI instead of the wolf.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
--rapid-upgrade rebuilt whatever the local checkout held, so "merge on
GitHub, then rapid-upgrade" silently shipped old code. It now fast-forwards
the compose checkout from origin first when that is safe (the checkout's own
root, on a branch, no uncommitted tracked changes, no local-only commits),
and otherwise says why it is building as-is. WOLFBBS_RAPID_UPGRADE_NO_PULL=1
opts out.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Pressing (F)orest used to resolve a whole fight with one formula and
print the result. It now opens a forest menu where you look for a
monster, trade blows round by round with (A)ttack/(S)tats/(R)un, visit
the healer's hut, and head back to town. Heroes get 15 forest fights a
day; dying costs the gold you carry and a tenth of your XP and keeps
you out until tomorrow. The door's session limit goes from 3 to 30
minutes so a full day of fights isn't cut off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The AI door had no access check, so switching it on let every caller spend
the sysop's model. Sysops now always have access; other users need to be
listed in /admin/gateways (or SSH admin AIUSERS) and get a daily prompt cap
(default 20, 0 = no limit).

For local llama.cpp models: compose now passes the WOLFBBS_GATEWAY_AI_* env
vars (including ALLOW_PRIVATE) into the bbs and web containers, a Skip
thinking setting sends enable_thinking=false so reasoning models don't spend
the whole token budget before answering, and an out-of-tokens reply says so
instead of "ai response was empty".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The migration runner splits on every ';', so the semicolon in the 0005
comment left 'a sysop-chosen limit is left alone.' as its own statement and
Postgres rejected it, stopping web, bbs and irc from starting. Adds a test
that fails on any semicolon inside a migration comment.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Running the sshserver tests writes internal/sshserver/.wolfbbs/ssh/, and
that throwaway key was committed by mistake. It is not the live board's
key. Untrack it and ignore .wolfbbs/ssh/ everywhere.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Verified now means: confirmed recovery email, or 2FA active with
recovery codes explicitly acknowledged. Confirming 2FA alone no
longer implies anything; fresh code sets start unacknowledged;
disabling 2FA or regenerating codes recomputes Verified unless a
confirmed email holds it. Admin hand-verify and bootstrap grants
are never revoked by flows that did not grant them.

Co-Authored-By: Ara Voss <ara.voss.resonant.mirror@gmail.com>
@mooserini

Copy link
Copy Markdown
Author

Opened against the wrong repo by mistake — this belongs on the fork. Closing to keep your queue clean.

@mooserini mooserini closed this Oct 1, 2026
@mooserini
mooserini deleted the ara/verified-either-or branch October 1, 2026 11:45
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.

1 participant