Commit Graph
6 Commits
Author SHA1 Message Date
thepraandClaude Opus 5.5 cff75e3a47 R2 accounts are safe: email by password and code, recovery only to it, sessions that end
Build / Build (push) Successful in 26s
Completion program R2 (docs/ROADMAP.md, 2026-10-17).

- An email address is set only with the password and a 6-digit code sent to it: /clientapi/user/email, /email/verify,
  /email/remove and GET /clientapi/user/account (EmailVerification: an hour, five tries, the code's hash only;
  EmailVerificationJob through the new IMailer). The request answers one sentence whether or not the address is free,
  and nothing goes to an address another login verified. The old POST /clientapi/user/update, which changed the
  address with neither and said when one was taken, answers 422.
- Recovery writes only to a verified address; migration _019 keeps the addresses given before verification existed.
- A changed password ends every session and every persona token, and answers a new JWT for the device that changed it.
- Sessions: every JWT carries its session's id ("si", kept by sniff/again). GET /clientapi/user/logout, which did
  nothing, ends that session alone (RootUser.EndedSessions, until its tokens would expire); POST
  /clientapi/user/logout/everywhere ends them all.
- AppConfiguration:HashingOptions was never bound, so passwords were hashed at the default 9789 iterations: it is
  bound now, at 210,000, and a hash made at other iterations is made again at the next sign-in (matched on the old
  hash, so two sign-ins never undo each other).
- Recovery and verification codes expire through TTL indexes; EmailVerification is never in a backup.
- AppConfigurationService reads appsettings as they are now, not the copy older builds saved in Mongo on first boot.
- ViewUserSettings and RootUserSettings carry SavedAt, so a client signing in keeps its own theme until one saved.
- Pasture: peers/mailpit.sh (Mailpit 1.31.4 as mailpit.test) receives PrivaPub's mail; recovery links open
  https://decepub.test. scenarios/accounts.sh, in sweep batch A, checks all of the above (28 checks).

Verified: dotnet build PrivaPub.sln -c Release; the pasture with gts and mailpit: accounts 28/28 and gts, 91 passed,
0 failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof
2026-10-17 03:13:16 +02:00
thepraandClaude Opus 5.5 8156c1927c First-time setup: the first login made while no administrator exists becomes it
Build / Build (push) Successful in 10m49s
Deploy / privapub.thepra.dev (push) Successful in 14m0s
Sign-up stays closed (invitations only), so a new server's first login could only be made over SSH with admin
create-root, and an invitation needs a group, which needs a login. While no administrator exists, GET
/clientapi/user/setup says so and POST takes a SetupForm (login, password, first persona): the login is made the
administrator with its persona, and setup closes for good (owner decision 2026-10-16: first come, first served, no setup
code).

ServerSetup is one document with a fixed id, so the first request to insert it wins and two setups at once make one
administrator. A setup that fails before its login is made gives the claim back; one that named an administrator never
does, whoever is demoted or deleted later, and the CLI stays the way back. admin promote and create-root --admin seal it
too, migration _018 seals a server that already had an administrator, and no backup holds it nor does a restore drop it,
since a restore tombstones an administrator made after the backup. Public sign-up, NodeInfo and the instance API are
unchanged. RootUser.SetAdmin is the one place an administrator is made (with isModerator, which IsAdminPolicy asks for),
and the invitation endpoints share InsertPersona with setup.

Tests: ServerSetupTests on the invitations-only host (setup, the race, failed setups, sign-up still closed, the CLI's
seal) and the migration; the whole suite passes (962). In the pasture, decePub's setup and invitation pages pass end to
end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 23:21:41 +02:00
thepraandClaude Opus 5.5 3d7d406834 A persona's archive: exported in Mastodon's layout, imported without telling anyone
A root exports one of its personas (a job) as Mastodon's account archive, so other servers' importers read it: the
actor with its public key only, its own posts and boosts with their media, likes, bookmarks and Mastodon's CSV files,
plus PrivaPub's filters, followed hashtags, notification policy, pins, scheduled and located posts; nothing of its root,
its siblings, its keys or anyone's token. A ticket link downloads it, for a week.

An archive (PrivaPub's or Mastodon's) uploaded in pieces is imported into a persona in a job, the parts the root picks,
with progress and a stop. SafeArchive refuses links, escaping paths, duplicates, bombs and oversized items, and reads the
outbox one item at a time. Imported posts are delivered to no one, put in no home and notify nobody, yet show on the
profile, outbox, hashtags and search; back home a post keeps its id, from another actor it gets one of its date and
ImportedFromURI, so importing twice changes nothing. Relationships go through the existing services; located, scheduled
and likes only when asked; followers never.

tools/pasture/scenarios/persona-archive.sh imports mastouser's real Mastodon archive (156 posts, 24 pictures) into a
persona Mastodon follows: Mastodon receives none of it, and a second import changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 12:43:12 +02:00
thepraandClaude Opus 5.5 bdc8be4508 A restore on the live pasture: its own scenario, and two fixes it found
tools/pasture/scenarios/restore.sh backs the pasture up from the administrator's endpoint, then alice_restore deletes a
post and makes another, mastouser follows her, she blocks bob_restore and carol_restore is made; the restore, asked for
from the endpoint, keeps every protective act (19 checks). town.sh renew <peer> signs a peer's town accounts in again,
since a restore ends every session.

It found that a backup listed media files already missing when it was made, so verifying it failed and the restore was
refused: a manifest now lists only the files it holds (refusals are cut to five lines). And a local post made after the
backup now comes back as a deleted row, as a deletion leaves it, so it answers 410 and its id is never given again;
DeletedObject holds remote tombstones only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 12:24:23 +02:00
thepraandClaude Opus 5.5 fba57318fa A backup is restored at boot, and never undoes a protective act
PrivaPub admin restore <id> (and soon the administrator's page) checks the backup (same host, a format and newest
migration this build reads, every hash) and writes restore.json; the running service sees it within seconds and stops,
and the next start restores it in MaintenanceGate, before migrations, indexes and hosted services: a pre-restore backup
taken once, every collection dropped and imported raw with its indexes, the media the live directory lacks brought
back, then the protective merge from the pre-restore backup. Followers and follows are the live ones; blocks, mutes,
domain blocks, reserved names, tombstones, reports, filters and OAuth applications are the union; deletions win;
accounts made since become tombstones and local posts made since answer 410; every session ends.

Each attempt redoes everything; one refused before any change is abandoned and recorded, one failed midway exits 1 for
systemd to retry, and after three it exits 75, which the unit no longer restarts. Commands wait (exit 75) while a
restore is pending. RestoreRecord tells what happened (admin restore --status).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 11:55:17 +02:00
thepraandClaude Opus 5.5 12bb75809f The server backs itself up: every collection byte for byte, the media linked
A backup is a directory <stamp>-<kind> under Backups:Root (/var/lib/privapub/backups, 2770, files 0640), written as
.partial and renamed once whole: a manifest (host, build, newest migration, each collection's count, size, sha256 and
indexes, what was left out and why, the media list), each collection as gzipped canonical Extended JSON read raw, and
hard links to the files of untrashed media rows (copies where a link can't be made). On a replica set every collection
is read in one snapshot session. Never in a backup: the statistics salt, jobs, recovery codes, sessions, the
maintenance lock and the configuration's copy with its SMTP password.

One backup or restore at a time (MaintenanceLock, a heartbeat document), and the janitor purges nothing meanwhile.
BackupScheduler backs up nightly at 03:30 UTC (or at once after missing a night); rotation keeps 7 daily, 4 weekly,
3 pre-deploy and 3 pre-restore backups. CLI: admin backup [--kind] [--db-only], admin backups, admin backup verify;
these run before migrations, so the deploy's own pre-deploy backup, which replaces mongodump, is of the database as the
live build left it. EntityMaps.Warm runs once under a lock, since test hosts now boot side by side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-07 11:40:06 +02:00