halkyon/packages/halkyon_base_server/lib/src/register/account_register.dart
Tizian 「ティツィアーン」 e5b9578916
feat(base): mending a broken SimpleLogin link with the handle and the recovery phrase (ADR 0020, D7)
Someone whose SimpleLogin identity is new to the register but who has an
account ("I already have one") names their handle and proves the
account's recovery phrase; the new identity takes the old one's place.

- `simpleLogin.beginMend(attempt, handle)`: the keys wrapped under the
  phrase's key, the public keys and a nonce, for the account with that
  handle (whatever its case). Only an account with keys and a SimpleLogin
  identity is mended, a link it had, as nobody adds SimpleLogin signed
  out (D10); a handle nobody has, or any other account's, gets a decoy
  from `signInDecoyKey`, the same each time. At most 10 an hour per
  handle and 30 per network.
- `simpleLogin.mend(attempt, proof)`: the phrase's proof for `mendLink`,
  binding the attempt (`mendLinkDigest`, on both sides), so a proof
  relayed from another sign-in links nobody's identity but the device's
  own; checked for the waiting identity first, which leaves the proof
  unspent. Every session of the account ends, in every service, and the
  sign-in stands ten minutes for a change, such as a lost password.
- SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's three keys
  (`signInDecoyKeyOf`); `addInstanceRoutes` stops a server with a
  SimpleLogin app and without it (`checkSimpleLoginMending`).
- Members ask the register for both; `RemoteRegister` and the instance
  API carry them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 09:17:49 +02:00

499 lines
18 KiB
Dart

import 'dart:typed_data';
import 'package:halkyon_base_server/src/generated/protocol.dart';
import 'package:halkyon_base_server/src/halkyon_base.dart';
import 'package:halkyon_base_server/src/register/accounts_mode.dart';
import 'package:halkyon_base_server/src/register/local_register.dart';
import 'package:halkyon_base_server/src/register/remote_register.dart';
import 'package:halkyon_base_server/src/register/sessions_ended.dart';
import 'package:halkyon_oidc_server/oidc.dart';
import 'package:serverpod/serverpod.dart';
/// The instance's register of accounts (ADR 0016): what one person is across
/// every service of the instance, kept by the service its operator names.
/// This server asks it here for everything the register keeps, so the same
/// call works whether this server holds the register itself
/// ([LocalRegister]) or is one of its members.
///
/// A person is named by their account ID, which is their `AuthUser`'s ID in
/// every service of the instance.
abstract interface class AccountRegister {
/// The register [pod] asks: its own database while it holds the register
/// (also unconfigured, as tests of a host's own code run), else the
/// register's server ([RemoteRegister]), which needs the instance's shared
/// secret.
static AccountRegister of(Serverpod pod) =>
_ofPod[pod] ??
switch (HalkyonBase.maybeOf(pod)?.accounts) {
MemberOfRegister(:final register) => RemoteRegister(
register,
secret:
instanceSecretOf(pod) ??
(throw StateError(
'A member of an instance needs its shared secret: set '
'instanceSecret in the passwords (INSTANCE_SECRET).',
)),
),
HoldsRegister() || null => const LocalRegister(),
};
/// Puts [register] in place of the one [pod] asks, or with null the one its
/// configuration names back: tests do.
static void use(Serverpod pod, AccountRegister? register) =>
_ofPod[pod] = register;
static final _ofPod = Expando<AccountRegister>('Account register');
/// [person]'s public profile, as apps see it; null for none.
Future<Profile?> profileOf(Session session, UuidValue person);
/// The public profiles of those of [people] who have one, by person.
Future<Map<UuidValue, Profile>> profilesOf(
Session session,
Set<UuidValue> people,
);
/// The public profile whose handle is [handle], whatever its case; null for
/// none.
Future<Profile?> profileByHandle(Session session, String handle);
/// The person whose handle is [handle], whatever its case; null for none.
/// For a host's own code (who is blocked, who wrote), never for apps:
/// profiles never say whose they are.
Future<UuidValue?> personWithHandle(Session session, String handle);
/// Saves [person]'s public profile, making it if they have none. Throws
/// `ProfileRefusedException` when it can't be saved (see
/// `ProfileRefusal`): a handle someone else has, whatever its case, or a
/// new handle once theirs is fixed.
Future<Profile> saveProfile(
Session session,
UuidValue person, {
required String handle,
required String displayName,
required String bio,
required List<String> links,
});
/// Why [handle] couldn't be [person]'s, as saving it would refuse it; null
/// when it can.
Future<ProfileRefusal?> checkHandle(
Session session,
UuidValue person,
String handle,
);
/// Checks which of [person]'s links' pages link back to their profile's
/// page with `rel="me"`, and keeps the answer; at most 10 times an hour.
/// Throws `ProfileNotFoundException` while they have no profile.
Future<Profile> checkLinks(Session session, UuidValue person);
/// Shows [png] as [person]'s avatar: a PNG of at most 1 MB. Throws
/// `ProfileRefusedException` for anything else, and
/// `ProfileNotFoundException` while they have no profile.
Future<Profile> setAvatar(Session session, UuidValue person, ByteData png);
/// Shows no avatar for [person] any more. Throws
/// `ProfileNotFoundException` while they have no profile.
Future<Profile> removeAvatar(Session session, UuidValue person);
/// Removes [person]'s public profile, freeing its handle; nothing when they
/// have none. Throws `ProfileRefusedException` once their handle is fixed.
Future<void> removeProfile(Session session, UuidValue person);
/// Fixes [person]'s handle, as others now refer to it (Halkyon Learn: their
/// first publish); nothing when it is fixed already or they have no
/// profile.
Future<void> fixHandle(Session session, UuidValue person);
/// Whether people can sign in with an email address: the register mails
/// the codes.
Future<bool> emailSignInOffered(Session session);
/// Mails a code for [purpose] to [address] in [language], the codes asked
/// for from [network] counted; the request it answers. Refusals as
/// `sendSignInCode`'s.
Future<UuidValue> sendSignInCode(
Session session, {
required String address,
required String language,
required EmailCodePurpose purpose,
required String network,
});
/// Links the GitHub account [gitHubUserId] to [person] (an account of the
/// register, under their ID). Refusals as `linkGitHubAccount`'s.
Future<void> linkGitHub(
Session session, {
required UuidValue person,
required String gitHubUserId,
});
/// The account that signs in with the GitHub account [gitHubUserId]; null
/// for none.
Future<UuidValue?> accountWithGitHub(Session session, String gitHubUserId);
/// Whether people sign in with SimpleLogin (ADR 0020): the register has
/// the instance's SimpleLogin app, and its operator offers it.
Future<bool> simpleLoginOffered(Session session);
/// Begins a SimpleLogin sign-in for the app and platform [marker] names,
/// by [caller] if signed in, the sign-ins begun from [network] counted.
/// Refusals as `beginSimpleLogin`'s.
Future<OidcStart> beginSimpleLogin(
Session session, {
required String marker,
UuidValue? caller,
required String network,
});
/// Ends the SimpleLogin sign-in [attempt] with SimpleLogin's [code], as
/// [caller] (null signed out) began it: how it ended, the account signed
/// in to, which the member issues its own session for. Refusals as
/// `completeSimpleLogin`'s.
Future<SimpleLoginProof> completeSimpleLogin(
Session session, {
required UuidValue attempt,
required String code,
UuidValue? caller,
});
/// Whether someone new whose SimpleLogin identity waits under [attempt]
/// could take [handle], the checks from [network] counted; null where they
/// could. Refusals as `checkSimpleLoginHandle`'s.
Future<ProfileRefusal?> checkSimpleLoginHandle(
Session session, {
required UuidValue attempt,
required String handle,
required String network,
});
/// Signs up someone new whose SimpleLogin identity waits under [attempt],
/// with [handle] and the keys [setup] describes: the new account's ID,
/// which the member issues its own session for. Refusals as
/// `signUpWithSimpleLogin`'s.
Future<UuidValue> signUpWithSimpleLogin(
Session session, {
required UuidValue attempt,
required String handle,
required KeysSetup setup,
});
/// The first step of mending the SimpleLogin link of the account with
/// [handle] for someone whose identity waits under [attempt], the begins
/// from [network] counted. Refusals as `beginSimpleLoginMend`'s.
Future<KeyProofChallenge> beginSimpleLoginMend(
Session session, {
required UuidValue attempt,
required String handle,
required String network,
});
/// Mends the SimpleLogin link of the account whose recovery phrase
/// [proof] proved, with the identity waiting under [attempt]: the account
/// signed in to, which the member issues its own session for, after
/// [SimpleLoginProof.endedBefore]. Refusals as `mendSimpleLogin`'s.
Future<SimpleLoginProof> mendSimpleLogin(
Session session, {
required UuidValue attempt,
required KeysProof proof,
});
/// The first step of [person] proving their password with its proof key
/// (an account that signs in with SimpleLogin). Refusals as
/// `beginPasswordKeyProof`'s.
Future<KeysChallenge> beginPasswordKeyProof(
Session session,
UuidValue person,
);
/// [person]'s keys for a device, once [proof] proved their password for
/// it. Refusals as `unlockKeys`'s.
Future<DeviceKeys> unlockKeys(
Session session,
UuidValue person,
KeysProof proof,
);
/// Gives [person], an account without keys that SimpleLogin was added to,
/// the keys [setup] describes and [handle] where it has none, with their
/// SimpleLogin sign-in under [attempt]. Refusals as `setFirstKeys`'.
Future<void> setFirstKeys(
Session session, {
required UuidValue person,
required UuidValue attempt,
required KeysSetup setup,
String? handle,
});
/// The first step of [person] proving their recovery phrase, signed in,
/// for a lost password (an account that signs in with SimpleLogin).
/// Refusals as `beginPhraseKeyProof`'s.
Future<KeyProofChallenge> beginPhraseKeyProof(
Session session,
UuidValue person,
);
/// Changes [person]'s password for their keys to [password], with
/// [proof]: every other session ends but [keepSession], in every service.
/// Refusals as `changeKeysPassword`'s.
Future<void> changeKeysPassword(
Session session, {
required UuidValue person,
required KeysOwnerProof proof,
required KeysPassword password,
UuidValue? keepSession,
});
/// Sets [person]'s new [password] for their keys with the recovery
/// phrase's [proof], as [changeKeysPassword] does. Refusals as
/// `resetKeysPassword`'s.
Future<void> resetKeysPassword(
Session session, {
required UuidValue person,
required KeysOwnerProof proof,
required KeysPassword password,
UuidValue? keepSession,
});
/// Replaces [person]'s recovery phrase's wrapping and proof key, with
/// [proof]. Refusals as `setKeysRecovery`'.
Future<void> setKeysRecovery(
Session session, {
required UuidValue person,
required KeysOwnerProof proof,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
});
/// How [person] signs in: with GitHub, an email address, a password and
/// whether an authenticator is confirmed; and their warning that a session
/// was ended for its wrong proofs, if any.
Future<SignInWays> signInWaysOf(Session session, UuidValue person);
/// Warns [person] that a session of theirs was ended for ten wrong proofs
/// in a row (ADR 0017, the fourth reading), as of now: a member tells it
/// of its own sessions.
Future<void> noteSessionEndedForProofs(Session session, UuidValue person);
/// Dismisses [person]'s warning that a session was ended for its proofs.
Future<void> dismissSessionEndedWarning(
Session session,
UuidValue person, {
required DateTime? sessionMadeAt,
});
/// Whether people sign in with a password and TOTP (ADR 0017): the
/// register holds the keys it needs.
Future<bool> passwordSignInOffered(Session session);
/// Gives [person] the password [setup] describes, with the address [code]
/// proves for [request] (their own where they have one), and for one who
/// signs in with GitHub alone a GitHub sign-in of theirs made recently
/// ([freshGitHubSignIn], of the session asking); someone with neither
/// only while anonymous where the session lives ([anonymousHere]).
/// Replacing a setup never finished ends every other session of the
/// account but [keepSession]. Refusals as `setPassword`'s.
Future<void> setPassword(
Session session, {
required UuidValue person,
required PasswordSetup setup,
UuidValue? request,
String? code,
bool freshGitHubSignIn = false,
bool anonymousHere = false,
UuidValue? keepSession,
});
/// A password sign-in's first step for [address], the sign-ins begun from
/// [network] counted. Refusals as `beginPasswordSignIn`'s.
Future<PasswordChallenge> beginPasswordSignIn(
Session session, {
required String address,
required String network,
});
/// A password sign-in's second step: the account it proved, for the
/// caller's session; a [code] mailed for [request] in place of the second
/// factor of a setup never finished. Refusals as
/// `completePasswordSignIn`'s.
Future<PasswordProof> completePasswordSignIn(
Session session, {
required UuidValue challenge,
required ByteData clientValue,
required ByteData clientProof,
String? totpCode,
String? recoveryCode,
UuidValue? request,
String? code,
required String instanceUrl,
required String address,
required String language,
});
/// Signs up with the address [code] proves for [request] and the password
/// [setup] describes: the account, new or one with the address and no
/// password, for the caller's session. Refusals as `signUpWithCode`'s.
Future<UuidValue> signUpWithCode(
Session session, {
required UuidValue request,
required String code,
required PasswordSetup setup,
});
/// A new authenticator of [person]. Refusals as `enrolAuthenticator`'s.
Future<AuthenticatorEnrolment> enrolAuthenticator(
Session session, {
required UuidValue person,
required String name,
required String label,
});
/// Confirms [person]'s [authenticator], with the owner's [proof] where
/// the account has one confirmed already; the first one's recovery codes.
/// Refusals as `confirmAuthenticator`'s.
Future<List<String>> confirmAuthenticator(
Session session, {
required UuidValue person,
required UuidValue authenticator,
required String code,
OwnerProof? proof,
});
/// [person]'s authenticators, the oldest first.
Future<List<Authenticator>> authenticatorsOf(
Session session,
UuidValue person,
);
/// Removes [person]'s [authenticator]. Refusals as
/// `removeAuthenticator`'s.
Future<void> removeAuthenticator(
Session session, {
required UuidValue person,
required UuidValue authenticator,
OwnerProof? proof,
});
/// New recovery codes for [person], with the owner's [proof]. Refusals as
/// `newRecoveryCodes`'.
Future<List<String>> newRecoveryCodes(
Session session, {
required UuidValue person,
required OwnerProof proof,
});
/// Asks for the reset of a lost authenticator. Refusals as
/// `resetAuthenticator`'s.
Future<ResetPending> resetAuthenticator(
Session session, {
required UuidValue challenge,
required ByteData clientValue,
required ByteData clientProof,
required String address,
required String language,
UuidValue? request,
String? code,
UuidValue? keyChallenge,
ByteData? signature,
required String instanceUrl,
});
/// The first step of proving the recovery phrase for [address], the
/// proofs begun from [network] counted. Refusals as `beginKeyProof`'s.
Future<KeyProofChallenge> beginKeyProof(
Session session, {
required String address,
required String network,
});
/// Sets a new password with the recovery phrase's proof; the account, for
/// the caller's session made after its sessions ended. Refusals as
/// `resetPasswordWithPhrase`'s.
Future<AccountSessionsEnded> resetPasswordWithPhrase(
Session session, {
required UuidValue challenge,
required ByteData signature,
required NewPassword password,
String? totpCode,
String? recoveryCode,
required String instanceUrl,
required String address,
required String language,
});
/// Asks for the reset of a whole account. Refusals as
/// `startAccountReset`'s.
Future<ResetPending> startAccountReset(
Session session, {
required String address,
required String language,
required UuidValue request,
required String code,
});
/// Carries out the reset of a whole account; the account, for the
/// caller's session made after its sessions ended. Refusals as
/// `completeAccountReset`'s.
Future<AccountSessionsEnded> completeAccountReset(
Session session, {
required String address,
required UuidValue request,
required String code,
required PasswordSetup setup,
});
/// The first step of [person] proving their password to change it.
/// Refusals as `beginPasswordChange`'s.
Future<PasswordChallenge> beginPasswordChange(
Session session, {
required UuidValue person,
});
/// Changes [person]'s password, ending their sessions everywhere but
/// [keepSession]. Refusals as `changePassword`'s.
Future<void> changePassword(
Session session, {
required UuidValue person,
required OwnerProof proof,
required NewPassword password,
UuidValue? keepSession,
});
/// Calls off every reset of [person]'s account waiting, with the owner's
/// [proof] (ADR 0017, the full reading's H1). Refusals as
/// `callOffResets`'.
Future<void> callOffResets(
Session session, {
required UuidValue person,
required OwnerProof proof,
});
/// Replaces the wrapping of [person]'s keys under their recovery phrase
/// and the phrase's proof key, with the owner's [proof]. Refusals as
/// `setRecoveryKeys`'.
Future<void> setRecoveryKeys(
Session session, {
required UuidValue person,
required OwnerProof proof,
required WrappedKeys recoveryKeys,
required ByteData recoveryProofKey,
});
/// Deletes the account [person] with everything the register's server has
/// of them, and has every member of the instance delete theirs: one with a
/// password only with [proof], one that signs in with SimpleLogin only
/// with [keysProof] (`requireOwnerToDelete`).
Future<void> deleteAccount(
Session session,
UuidValue person, {
DeletionProof? proof,
KeysOwnerProof? keysProof,
});
/// Those of [people] that are accounts of the register.
Future<Set<UuidValue>> knownAccounts(
Session session,
Set<UuidValue> people,
);
}