feat(accounts): one account across an instance's services (ADR 0016)

halkyon_base runs as the instance's register or as one of its members.
The register keeps the ways in, handles and profiles; members ask it
over HTTP with the instance's shared secret (instanceSecret), and open
sessions of their own for its accounts, under the same account ID.

- AccountsMode from the environment: INSTANCE_MEMBERS on the register,
  ACCOUNT_REGISTER_URL on a member. Learn always holds the register
  (Explore joins the profiles); Todo is a member where it is set.
- AccountRegister: LocalRegister, RemoteRegister, an in-memory one in
  tests. Calls between services on the web server, refused without the
  secret (constant-time compare); a server needing it doesn't start
  without it.
- Deleting an account deletes it everywhere: notices per member kept in
  the deletion's transaction (account_deletion_notice), told at once and
  hourly until confirmed; members prune daily whom the register no longer
  knows, never while it knows none of them or can't be reached.
- Deploy: INSTANCE_SECRET and INSTANCE_MEMBERS in compose and .env.

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-19 23:10:08 +02:00
commit 65c5cd740d
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -39,7 +39,9 @@ class EndpointAccount extends _isc.EndpointRef {
/// Deletes the caller with everything they have (see [deletePerson]). The
/// app also calls it when an anonymous account is given up (decided
/// 2026-09-30). Their other devices learn at once that their sessions
/// ended.
/// ended. An account deletes everywhere (ADR 0016): on a member, the
/// register deletes it and tells every member, this one deleting its own
/// data at once; an anonymous person is this service's alone.
_ida.Future<void> delete() => caller.callServerEndpoint<void>(
'halkyon_base.account',
'delete',
@ -81,7 +83,9 @@ class EndpointAnonymousIdp extends _iaic.EndpointAnonymousIdpBase {
/// Signing in with an email address, by a code mailed to it:
/// the way to keep an anonymous account, as linking GitHub is, and a second
/// way in for someone who signs in with GitHub. Signed in only, as every
/// device is from its first start.
/// device is from its first start. The instance's register mails the codes
/// and keeps the addresses' hashes (ADR 0016), this server's own database
/// when it holds the register.
/// {@category Endpoint}
class EndpointEmailCode extends _isc.EndpointRef {
EndpointEmailCode(_isc.EndpointCaller caller) : super(caller);
@ -130,7 +134,7 @@ class EndpointEmailSignIn extends _isc.EndpointRef {
String get name => 'halkyon_base.emailSignIn';
/// A session for the person [switchToken] names; refusals as in
/// [signInWithEmail].
/// [completeEmailSwitch].
_ida.Future<_iacc.AuthSuccess> signIn({required String switchToken}) =>
caller.callServerEndpoint<_iacc.AuthSuccess>(
'halkyon_base.emailSignIn',
@ -149,8 +153,9 @@ class EndpointGitHubSignIn extends _isc.EndpointRef {
@override
String get name => 'halkyon_base.gitHubSignIn';
/// A session for the person with the GitHub account that [code] proves;
/// refusals as in [signInWithGitHubAccount].
/// A session for the person with the GitHub account that [code] proves,
/// whom the instance's register knows by it; an account it doesn't know
/// is refused ([GitHubSignInRefusal.unknownAccount]).
_ida.Future<_iacc.AuthSuccess> signIn({
required String code,
required String codeVerifier,
@ -168,7 +173,8 @@ class EndpointGitHubSignIn extends _isc.EndpointRef {
/// Links a GitHub account to the signed-in person: the lossless upgrade from
/// an anonymous account. The person and everything they have stay
/// where they are; only the way they sign in changes.
/// where they are; only the way they sign in changes. The instance's
/// register keeps the GitHub ID (ADR 0016).
/// {@category Endpoint}
class EndpointGitHubUpgrade extends _isc.EndpointRef {
EndpointGitHubUpgrade(_isc.EndpointCaller caller) : super(caller);
@ -202,10 +208,11 @@ class EndpointInstance extends _isc.EndpointRef {
@override
String get name => 'halkyon_base.instance';
/// This instance's basics: the app it serves, GitHub sign-in only with the operator's OAuth
/// app, email sign-in only where it can mail codes, where its source is,
/// the oldest app build it works with, and where it keeps people's data
/// and its backups.
/// This instance's basics: the app it serves, GitHub sign-in only with the
/// operator's OAuth app, email sign-in only where the register can mail
/// codes (this server, or on a member the register, ADR 0016), where its
/// source is, the oldest app build it works with, and where it keeps
/// people's data and its backups.
_ida.Future<_isnyur2i.InstanceBasics> basics() =>
caller.callServerEndpoint<_isnyur2i.InstanceBasics>(
'halkyon_base.instance',