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
2 changed files with 837 additions and 1353 deletions

View file

@ -166,13 +166,11 @@ class _FutureCalls {
}
class _PruningFutureCall {
Future<void> pruneAnonymousPeople(
_ist.TestSessionBuilder sessionBuilder,
) async {
Future<void> pruneAccounts(_ist.TestSessionBuilder sessionBuilder) async {
var _localUniqueSession =
(sessionBuilder as _ist.InternalTestSessionBuilder).internalBuild();
try {
await _i772x17y.PruningPruneAnonymousPeopleFutureCall().invoke(
await _i772x17y.PruningPruneAccountsFutureCall().invoke(
_localUniqueSession,
null,
);

View file

@ -54,15 +54,16 @@ void main() {
);
});
test('a device starts without an account through the module, as a '
'person of their own', () async {
final success = (await call('anonymousIdp', 'login'))! as AuthSuccess;
final person = await AuthServices.instance.authUsers.get(
sessionBuilder.build(),
authUserId: success.authUserId,
test('makes no account without a way in: the module signs nobody in '
'anonymously (ADR 0020, D25)', () {
expect(
pod.endpoints.getConnectorByName('halkyon_base.anonymousIdp'),
isNull,
);
expect(
pod.endpoints.getConnectorByName('halkyon_base.instance'),
isNotNull,
);
expect(person.id, success.authUserId);
});
test("is a member of the instance's register the operator names, and "
@ -86,8 +87,19 @@ void main() {
});
test(
'the pruning is scheduled once, however often the server starts',
'the pruning is scheduled once, however often the server starts, '
'and the one under its earlier name goes',
() async {
// INSERT INTO "serverpod_future_call" (…) VALUES (…)
await FutureCallEntry.db.insertRow(
sessionBuilder.build(),
FutureCallEntry(
name: 'PruningFutureCall.pruneAnonymousPeople',
time: DateTime.utc(2026, 10, 25, 3, 17),
serverId: 'default',
identifier: 'prune-abandoned-anonymous-people',
),
);
await schedulePruning(pod);
await schedulePruning(pod);
@ -99,14 +111,22 @@ void main() {
scheduled.map((entry) => entry.identifier),
unorderedEquals(pruningIdentifiers),
);
expect(
await FutureCallEntry.db.count(
sessionBuilder.build(),
where: (t) =>
t.identifier.equals('prune-abandoned-anonymous-people'),
),
0,
);
},
);
},
);
}
/// Deletes every rate-limit record, every person (their anonymous accounts
/// and sessions go with them, `ON DELETE CASCADE`) and every future call.
/// Deletes every rate-limit record, every person (their sessions go with
/// them, `ON DELETE CASCADE`) and every future call.
Future<void> _deleteAll(Session session) async {
await RateLimitedRequestAttempt.db.deleteWhere(
session,