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>
499 lines
18 KiB
Dart
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,
|
|
);
|
|
}
|