cff75e3a47a3df00af2f3505c96bec6273796f14
6
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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
|
||
|
|
ff5e635dda |
R1 cleanup: collAnon and dead code gone, docs current, every peer's scenario in one sweep
Build / Build (push) Successful in 1m7s
Starts the completion program (docs/ROADMAP.md, "Owner decisions on finishing everything", 2026-10-17). - Dead code: the commented-out collAnon blocks (discussions, seeding, admin user lists, HTTP/3, the old signature middleware), the empty PrivaPubWorkersConfiguration, PublicAvatarUsersService and UserPolicyType, unused helpers, the second IPasswordHasher registration, the PasswordGenerator package, SignUpAsync/LoginAsync's unused invitation parameters. "Starting PrivaPub"; a signed-in login is sent to "/", not to "/discussions". - Stored fields nothing read: RootUser.Contacts and ResetPasswordToken*, the local Avatar.AvatarServer, EmailRecovery.RecoveryCode/RequestIP/CreationDate, MongoSettings.LogsDatabase, and AppConfiguration's upload/discussion keys, RequiresFirstTimeSetup and Version (old documents still load: MongoDB.Entities ignores extra elements). DmPost's indexes are no longer made. 49 collAnon keys leave FieldsNameResource in every language. - /clientapi/data/current-version answers the build's ref. UsersIds is a list that can't throw, and the admin routes leave the administrator out without mutating the request. - deploy.yml checks SecureMode on a name no persona can have, and the browser redirect on the directory's first local persona: @thepra is the administrator's own now. - Town: decePub's e2e remnants (expect, client.jsonl, the decePub column) and town/tests go; the dead gaps/load commands too. Mobilizon no longer keeps an event for a deleted test. - tools/pasture/sweep.sh runs every peer's scenario, batch by batch, as CLAUDE.md asks before a change to what PrivaPub sends. - Docs: CLAUDE.md's repo map, route table (every frozen route) and stale lines; ROADMAP's status (setup, v1.25.0, v1.26.0, the program C), the 2026-10-17 decisions, and dated notes where earlier text no longer holds; FEDERATION.md's FEP list (7888, f228, 7628, ae0c and 8967 supported) and the deploy's persona. Verified: dotnet build PrivaPub.sln -c Release (0 errors, no new warnings); bash -n on the touched scripts; py_compile on the town; the pasture's gts, mastodon and lemmy scenarios on this build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WpVCoDeM1qUTRp4fXQdfof |
||
|
|
8156c1927c |
First-time setup: the first login made while no administrator exists becomes it
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> |
||
|
|
8f25bf056d |
Everything on, phase 4a: one answer everywhere for counts, search, collections and the instance API
Owner decision 2026-10-04: fix the mismatches and every other mismatch of the same kind.
- One counting rule (Domain/Privacy/Counted), Mastodon's. It is used for a persona's statuses_count, its outbox
totalItems, NodeInfo localPosts and the instance status_count, which used to count four different things. It
counts every post that is neither deleted nor a DM, boosts included, and circle and located posts too (owner
decision). A group's count includes its remote members' posts.
- Users. Personas of banned or deleted roots no longer count, and are not found in search. NodeInfo now gives
activeMonth and activeHalfyear, and the v2 instance gives active_month instead of a constant 0.
- replies_count counts only public and unlisted replies, so it no longer tells anyone that a private reply exists.
Migration _012 recounts it.
- A remote account that deletes itself takes everything out of every count (GoneActors): its likes, downvotes,
reactions and poll votes go and their counters come back, as do its boosts', replies' and quotes' counts, and its
notifications. Lookups, account lists, search and favourited_by no longer show it. Migration _012 applies this to
accounts already gone.
- Deleting a post also deletes its pins and the local boosts of it.
- /stalking gives the same total as following_count. Members are still never listed, and hide_collections is now
always true, since the setting never did anything.
- Joining a community by invitation is following it, so /flock and /groupies agree; leaving unfollows.
- Search. Anyone may search, as on Mastodon; resolve and offset need a sign-in, offset pages, and deleted accounts
are never found.
- notifications/unread_count counts what the list shows, and the owner's follower and following lists page with
Link.
- The instance API advertises what is enforced:
- max_characters, now enforced with a 422;
- max_pinned_statuses = MaxPins;
- the media types and limits MediaService and MediaOptions accept;
- PollService's limits;
- the configured languages;
- no streaming URL until streaming exists.
domain_count counts the servers we have exchanged with; which ones stays unpublished (peers is empty).
- Routes Mastodon answers now answer instead of 404:
- directory, tags/{name}, timelines/link and identity_proofs;
- instance/languages, translation_languages, domain_blocks and privacy_policy;
- the v1 and v2 notification policy, and notification requests.
Also, from phase 3: a recovered password ends /clientapi sessions through a per-root SessionStamp claim instead of
comparing the JWT's whole-second nbf with the change time. That comparison let a token issued in the same second
survive, which made a test flaky.
671 tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
|
||
|
|
8c2eba6cbb |
Everything on, phase 3: sign-in and recovery tell nothing, recovered passwords end sessions, deleted roots are gone everywhere
Owner decision 2026-10-04: fix the account privacy findings.
- Sign-in. Every failure answers "That username and password do not match." after the same work: an unknown login
is hashed against a decoy, and the comparison is constant-time. "Banned" is told only to someone who gave the right
password. This covers /clientapi/user/login, /invitation/login and /oauth/login.
- Recovery.
- Every request answers the same sentence and queues a SendRecovery job, whether or not the account exists or has an
email. The lookup, the code and SMTP move to RecoveryJob, so neither the answer nor its timing says anything.
- Codes are kept only as a SHA-256 hash, for one hour. Migration _011 drops the plaintext ones, which never expired.
- A recovered password ends every session of the root. RootSessions sets CredentialsChangedAt, which JwtEvents
checks against the JWT's issue time, now stamped as nbf, and revokes each persona's OAuth tokens and authorizations.
- Deleting a root (RootRemoval: the admin route, or the restored self-delete at /clientapi/user/delete, which asks for
the password).
- Its sessions end.
- Each persona and each group it owns sends Delete{Actor} to its followers, its members and the accounts it follows.
- The personas' posts are emptied.
- /peasants/{name} answers 410 with a Tombstone (formerType Person or Group), as do its inbox and WebFinger, through
LocalActorService.Gone. The names stay reserved.
- The root keeps only a unique `deleted-{id}` name; the second deletion on an instance used to collide on
"Deleted user".
Also, from phase 2's pasture: GoToSocial files a circle post like a DM and shows it only to accounts it mentions. Each
member's copy, and a member's refetch, now also mentions that member silently. The GoToSocial scenario checks circle
posts in conversations, like DMs, and they pass there now, as on Mastodon.
657 tests pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ELjqpznMFMNrJoJUj6K5p2
|
||
|
|
1e66851113 | Saving |