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

View file

@ -67,9 +67,6 @@ final class _Register implements AccountRegister {
/// When the session dismissing a warning was made, by person.
final dismissedMadeAt = <UuidValue, DateTime?>{};
/// Those [setPassword] was told are anonymous on the member.
final anonymousHere_ = <UuidValue>{};
/// What a password sign-in proves; null refuses it.
PasswordProof? proof;
@ -209,16 +206,6 @@ final class _Register implements AccountRegister {
return request;
});
@override
Future<void> linkGitHub(
Session session, {
required UuidValue person,
required String gitHubUserId,
}) => _answer('linkGitHub', () {
accounts.add(person);
gitHub[gitHubUserId] = person;
});
@override
Future<UuidValue?> accountWithGitHub(Session session, String gitHubUserId) =>
_answer('accountWithGitHub', () => gitHub[gitHubUserId]);
@ -477,7 +464,6 @@ final class _Register implements AccountRegister {
UuidValue? request,
String? code,
bool freshGitHubSignIn = false,
bool anonymousHere = false,
UuidValue? keepSession,
}) => _answer('setPassword', () {
add(person);
@ -485,9 +471,6 @@ final class _Register implements AccountRegister {
if (freshGitHubSignIn) {
freshGitHubSignIns.add(person);
}
if (anonymousHere) {
anonymousHere_.add(person);
}
});
@override
@ -750,7 +733,9 @@ void main() {
late Serverpod pod;
late _Register register;
/// Anonymous here, signed in on her phone.
/// Not identified here, signed in on her phone: an anonymous account
/// left from before (ADR 0020, D25), or one the register linked whose
/// becoming identified here failed (the full reading's L7).
late UuidValue anna;
late UuidValue annasPhone;
@ -1095,17 +1080,7 @@ void main() {
expect(register.passwords, {anna});
expect(register.freshGitHubSignIns, isEmpty);
// Anna was anonymous here, which the register can't know (the
// full reading's M1).
expect(register.anonymousHere_, {anna});
expect(await scopesOf(anna), contains(identified.name));
expect(
await AnonymousAccount.db.count(
session,
where: (t) => t.authUserId.equals(anna),
),
0,
);
});
test(
@ -1201,16 +1176,44 @@ void main() {
test('begins signed in for the caller', () async {
await endpoints.simpleLogin.begin(
asAnna(),
asBen(),
back: SimpleLoginReturn.loopback,
);
expect(register.simpleLoginsBegun.single, (
marker: '$testApp-loopback',
caller: anna,
caller: ben,
));
});
test('refuses a caller not identified here, asking the register '
'nothing (ADR 0020, D25)', () async {
Matcher wrongCaller() => throwsA(
isA<OidcRefusedException>().having(
(error) => error.reason,
'reason',
OidcRefusal.wrongCaller,
),
);
await expectLater(
endpoints.simpleLogin.begin(
asAnna(),
back: SimpleLoginReturn.loopback,
),
wrongCaller(),
);
await expectLater(
endpoints.simpleLogin.complete(
asAnna(),
attempt: const Uuid().v7obj(),
code: 'code',
),
wrongCaller(),
);
expect(register.asked, isEmpty);
});
test('signs the account the register found in here, made a person '
'here the first time', () async {
final carla = const Uuid().v7obj();
@ -1241,31 +1244,24 @@ void main() {
);
});
test('makes the caller identified here once the register added '
'SimpleLogin to their account', () async {
test("adds SimpleLogin at the register to the caller's account, "
'who stays identified here', () async {
register.simpleLoginProof = SimpleLoginProof(
outcome: SimpleLoginOutcome.linked,
);
final completed = await endpoints.simpleLogin.complete(
asAnna(),
asBen(),
attempt: const Uuid().v7obj(),
code: 'code',
);
expect(completed.outcome, SimpleLoginOutcome.linked);
expect(completed.session, isNull);
expect(register.simpleLoginLinked, {anna});
expect(await scopesOf(anna), contains(identified.name));
expect(register.simpleLoginLinked, {ben});
expect(await scopesOf(ben), contains(identified.name));
expect(
await AnonymousAccount.db.find(
session,
where: (t) => t.authUserId.equals(anna),
),
isEmpty,
);
expect(
(await endpoints.account.signInWays(asAnna())).simpleLogin,
(await endpoints.account.signInWays(asBen())).simpleLogin,
isTrue,
);
});
@ -1297,7 +1293,7 @@ void main() {
await expectLater(
endpoints.simpleLogin.complete(
asAnna(),
asBen(),
attempt: const Uuid().v7obj(),
code: 'code',
),
@ -1309,7 +1305,7 @@ void main() {
),
),
);
expect(await scopesOf(anna), isNot(contains(identified.name)));
expect(register.simpleLoginLinked, isEmpty);
});
test('checks a handle for someone new at the register, the device '
@ -1759,8 +1755,8 @@ void main() {
expect(register.deletedWithKeys.single?.attempt, attempt);
});
test('of an anonymous person deletes it here alone, once the '
'register says it has no such account', () async {
test('of an anonymous account left from before deletes it here '
'alone, once the register says it has no such account', () async {
await endpoints.account.delete(asAnna());
expect(register.asked, ['knownAccounts']);