fix(auth): the reading of the fixes' nits

- The background delivery of session notices logs (its failure warning
  was never written: its session had logging off).
- A sign-in's and a phrase proof's begin check the address's ceiling
  after the network's limits, which hold a flood back before it costs
  the ceiling's queries (one an UPDATE, pushing resets).
- The module's README lists every pruning step a host schedules
  (forgetLapsedResets, deliverSessionNotices) and which mode each is for.

(The check of a member's clock ahead, which counted the register's
processing time as skew, went with C1's fix.)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-21 16:08:01 +02:00
commit 8c910f8cfc
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 21 additions and 12 deletions

View file

@ -60,12 +60,13 @@ Endpoints, under the nickname `base` in a host's client (`client.modules.base.em
`tokenManagerBuilders` and `identityProviderBuilders`, the `identified` scope and
`grantIdentified`, `deletePerson`, the pruning (`pruneAbandonedAnonymousPeople`,
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`, `forgetUnconfirmedAuthenticators`,
`forgetGitHubAccounts`), short codes, `sourceCodeUrl`,
`forgetGitHubAccounts`, `forgetLapsedResets`), short codes, `sourceCodeUrl`,
`dataLocation`, `backupLocation`, people's profiles as the host's code reads them
(`profileOf`, `profilesOf`, `personWithHandle`, `fixHandle`; `saveProfile` for its
tests), `accountDomain`, `profilePageUrl`, `addProfilePageRoute`, the register
(`AccountRegister`, `AccountsMode`, `addInstanceRoutes`, `deliverDeletionNotices`,
`forgetPeopleTheRegisterForgot`, `issueSession`), and the models. A host never reads
`deliverSessionNotices`, `forgetPeopleTheRegisterForgot`, `issueSession`), and the
models. A host never reads
the profiles' table itself.
## The instance's register (ADR 0016)
@ -329,8 +330,11 @@ Built on the three parts above, on the register (members forward every step thro
longer signs in with GitHub (`usePassword`); where passwords are offered, no new GitHub
account is linked (`deprecated`); from `GITHUB_SIGN_IN_ENDS` on GitHub signs nobody in
(`ended`), the basics stop naming the day (`gitHubSignInEnds`, which apps show until
then) and `forgetGitHubAccounts` deletes every GitHub ID, first deleting the accounts
that sign in with GitHub alone (no address, never a password). `password.set` adds an
then) and, 30 days later (`gitHubAccountsKeptAfterEnd`, so a date set wrong can be put
right first), `forgetGitHubAccounts` deletes every GitHub ID on the register, first
deleting the accounts that sign in with GitHub alone (no address, never a password),
each on its own (one failing is tried again the next day), the members told once
after all of them. `password.set` adds an
address without GitHub only for someone anonymous where the session lives
(`anonymousHere`, which a member tells the register).
@ -386,10 +390,12 @@ Built on the three parts above, on the register (members forward every step thro
literal routes of its own.
Then schedule the pruning in a future call of the host's own: daily
`pruneAbandonedAnonymousPeople`, `forgetGitHubAccounts` and
`forgetPeopleTheRegisterForgot` (a member's), hourly `forgetOldSignUpAttempts`,
`forgetExpiredEmailCodes`, `forgetUnconfirmedAuthenticators` and
`deliverDeletionNotices` (the register's). Each does nothing in the other mode.
`pruneAbandonedAnonymousPeople`, `forgetGitHubAccounts` (the register's),
`forgetLapsedResets` and `forgetPeopleTheRegisterForgot` (a member's), hourly
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`,
`forgetUnconfirmedAuthenticators`, `deliverDeletionNotices` and
`deliverSessionNotices` (the register's). Each marked as one mode's does nothing in
the other.
4. A host's deletion (`PersonDataDeletion`) runs inside `deletePerson`'s transaction,
after the person's sessions ended and before their identities and the person go, in

View file

@ -692,7 +692,6 @@ Future<PasswordChallenge> beginPasswordSignIn(
final pod = session.serverpod;
final keys = requirePasswordKeys(pod);
final hash = _hashOfTyped(pod, address);
await _refuseAddressProvingTooOften(session, hash);
// The address first: a sign-in it can't begin takes nothing of the
// network's.
if (!await _beginsPerAddressAndNetwork.tryRecordAttempt(
@ -702,6 +701,9 @@ Future<PasswordChallenge> beginPasswordSignIn(
!await _beginsPerNetwork.tryRecordAttempt(session, key: network)) {
throw refusedFor(PasswordRefusal.tooManyAttempts);
}
// The address's ceiling after the network's limits, which hold a flood
// back before it costs the ceiling's queries (the reading of the fixes).
await _refuseAddressProvingTooOften(session, hash);
// Both, always: an account's sign-in takes as long as a decoy's.
final decoy = _decoyAccount(keys.decoyKey, hash);
final password = await _passwordWithAddress(session, hash);
@ -1333,7 +1335,6 @@ Future<KeyProofChallenge> beginKeyProof(
final pod = session.serverpod;
final keys = requirePasswordKeys(pod);
final hash = _hashOfTyped(pod, address);
await _refuseAddressProvingTooOften(session, hash);
if (!await _keyProofsPerAddressAndNetwork.tryRecordAttempt(
session,
key: '$hash|$network',
@ -1341,6 +1342,7 @@ Future<KeyProofChallenge> beginKeyProof(
!await _beginsPerNetwork.tryRecordAttempt(session, key: network)) {
throw refusedFor(PasswordRefusal.tooManyAttempts);
}
await _refuseAddressProvingTooOften(session, hash);
final decoy = await _decoyKeys(keys.decoyKey, hash);
final password = await _passwordWithAddress(session, hash);
final nonce = _randomBytes(32);

View file

@ -102,9 +102,10 @@ Future<void> announceSessionsEnded(Session session, UuidValue account) async {
final _deliveries = <Future<void>>{};
/// Tells the members of [account]'s ended sessions, on a session of its
/// own: the request's may close first.
/// own: the request's may close first. It logs, so a failure is written
/// (persisting session logs stays off everywhere, `config/`).
Future<void> _deliverInBackground(Serverpod pod, UuidValue account) async {
final session = await pod.createSession(enableLogging: false);
final session = await pod.createSession();
try {
await deliverSessionNotices(session, account: account);
} on Object catch (error) {