feat(base)!: no anonymous accounts on the server (ADR 0020, D25)
All checks were successful
CI / check (push) Successful in 30m13s
CI / deploy (push) Has been skipped

The server's half of D25: every account has a way in, and the anonymous
accounts left from before go with the daily pruning.

- The module signs nobody in anonymously: `anonymousIdp` and
  `gitHubUpgrade` are gone, and so is linking GitHub at the register.
- A password is set only by someone whose address is theirs (a
  `firstPassword` code) or who signs in with GitHub alone (a fresh
  sign-in and a `signUp` code); an anonymous account is refused
  `wrongCredentials` before any code is used.
- SimpleLogin refuses to link onto a caller without `identified`
  (`wrongCaller`), the member's endpoint too, so nothing turns an
  anonymous account into one with a way in.
- `pruneAnonymousPeople` deletes every anonymous account left, however
  new, never one identified. Learn and Todo schedule it as
  `prune-accounts` and cancel the old `prune-abandoned-anonymous-people`
  entry once.
- Learn: someone without `identified` keeps no files;
  `FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds
  up to 22 start with an anonymous account.
- Learn's sync tests make their person and session by SQL in the
  development database, as Serverpod stores sessions.
- Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended.

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-24 16:07:27 +02:00
commit 87671208df
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
3 changed files with 837 additions and 1353 deletions

View file

@ -58,8 +58,7 @@ class FutureCalls extends _is.FutureCallDispatch<_FutureCallRef> {
String serverId,
) {
var registeredFutureCalls = <String, _is.InvokableFutureCall>{
'PruningPruneAnonymousPeopleFutureCall':
PruningPruneAnonymousPeopleFutureCall(),
'PruningPruneAccountsFutureCall': PruningPruneAccountsFutureCall(),
'PruningForgetSignUpAttemptsFutureCall':
PruningForgetSignUpAttemptsFutureCall(),
'PruningTellMembersOfDeletionsFutureCall':
@ -194,9 +193,9 @@ class _PruningFutureCallDispatcher {
final _InvokeFutureCall _invokeFutureCall;
Future<void> pruneAnonymousPeople() {
Future<void> pruneAccounts() {
return _invokeFutureCall(
'PruningPruneAnonymousPeopleFutureCall',
'PruningPruneAccountsFutureCall',
null,
);
}
@ -223,18 +222,18 @@ class _PruningFutureCallDispatcher {
}
}
/// Prunes one batch and logs how many people went; 30 days after GitHub
/// sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the accounts that had
/// no other way in and forgets every GitHub ID too, on the register; and
/// forgets the resets that lapsed (ADR 0017).
class PruningPruneAnonymousPeopleFutureCall extends _is.FutureCall
/// Prunes one batch of anonymous accounts and logs how many people went;
/// 30 days after GitHub sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the
/// accounts that had no other way in and forgets every GitHub ID too, on
/// the register; and forgets the resets that lapsed (ADR 0017).
class PruningPruneAccountsFutureCall extends _is.FutureCall
implements _is.InvokableFutureCall {
@override
_ida.Future<void> invoke(
_is.Session session,
_is.SerializableModel? object,
) async {
await _ibrplims.PruningFutureCall().pruneAnonymousPeople(session);
await _ibrplims.PruningFutureCall().pruneAccounts(session);
}
}

View file

@ -1,8 +1,11 @@
import 'package:halkyon_todo_server/src/generated/future_calls.dart';
import 'package:serverpod/serverpod.dart';
/// The identifier of the daily pruning of abandoned anonymous people.
const _pruning = 'prune-abandoned-anonymous-people';
/// The identifier of the daily pruning of accounts.
const _pruning = 'prune-accounts';
/// The identifier the daily pruning had until ADR 0020, D25.
const _anonymousPruning = 'prune-abandoned-anonymous-people';
/// The identifier of the hourly forgetting of sign-up attempts.
const _attemptPruning = 'forget-sign-up-attempts';
@ -21,20 +24,22 @@ const pruningIdentifiers = [
_deletedAccounts,
];
/// Schedules the pruning (`PruningFutureCall`): abandoned anonymous people
/// daily, sign-up attempts and expired email codes hourly, and the instance's
/// Schedules the pruning (`PruningFutureCall`): the anonymous accounts left
/// from before daily, sign-up attempts and expired email codes hourly, and the instance's
/// accounts (ADR 0016): the register's deletions its members haven't
/// confirmed hourly, a member's people the register forgot daily (each does
/// nothing in the other mode). Scheduling always adds an entry, so the
/// previous one goes first, and starting the server again leaves one of
/// each.
/// each; the one scheduled under the pruning's earlier name, which would
/// call nothing, goes too.
Future<void> schedulePruning(Serverpod pod) async {
await pod.futureCalls.cancel(_anonymousPruning);
await pod.futureCalls.cancel(_pruning);
await pod.futureCalls
.callRecurring(identifier: _pruning)
.cron('17 3 * * *')
.pruning
.pruneAnonymousPeople();
.pruneAccounts();
await pod.futureCalls.cancel(_attemptPruning);
await pod.futureCalls

View file

@ -2,25 +2,25 @@ import 'package:clock/clock.dart';
import 'package:halkyon_base_server/base.dart';
import 'package:serverpod/serverpod.dart';
/// Deletes the anonymous people nobody can return to, daily, and the IP
/// addresses of sign-up attempts the rate limit is done with and the email
/// sign-in codes past their time, hourly: halkyon_base's pruning (ADR 0014),
/// Deletes the anonymous accounts left from before (ADR 0020, D25), daily,
/// and the IP addresses of attempts the rate limits are done with and the
/// email sign-in codes past their time, hourly: halkyon_base's pruning (ADR 0014),
/// which the server schedules when it starts (`schedulePruning`). Then the
/// instance's accounts (ADR 0016): as its register, telling the members
/// again of the accounts gone, hourly; as a member, deleting the people
/// whose account the register no longer knows, daily.
class PruningFutureCall extends FutureCall {
/// Prunes one batch and logs how many people went; 30 days after GitHub
/// sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the accounts that had
/// no other way in and forgets every GitHub ID too, on the register; and
/// forgets the resets that lapsed (ADR 0017).
Future<void> pruneAnonymousPeople(Session session) async {
/// Prunes one batch of anonymous accounts and logs how many people went;
/// 30 days after GitHub sign-in ended (`GITHUB_SIGN_IN_ENDS`), deletes the
/// accounts that had no other way in and forgets every GitHub ID too, on
/// the register; and forgets the resets that lapsed (ADR 0017).
Future<void> pruneAccounts(Session session) async {
final now = clock.now();
final pruned = await pruneAbandonedAnonymousPeople(session, now: now);
final pruned = await pruneAnonymousPeople(session);
final gitHub = await forgetGitHubAccounts(session, now: now);
final resets = await forgetLapsedResets(session, now: now);
session.log(
'Pruned $pruned abandoned anonymous people, $gitHub GitHub IDs and '
'Pruned $pruned anonymous people, $gitHub GitHub IDs and '
'$resets lapsed resets.',
);
}