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:
parent
4107cc3087
commit
65c5cd740d
4 changed files with 41906 additions and 167 deletions
|
|
@ -62,6 +62,10 @@ class FutureCalls extends _is.FutureCallDispatch<_FutureCallRef> {
|
|||
PruningPruneAnonymousPeopleFutureCall(),
|
||||
'PruningForgetSignUpAttemptsFutureCall':
|
||||
PruningForgetSignUpAttemptsFutureCall(),
|
||||
'PruningTellMembersOfDeletionsFutureCall':
|
||||
PruningTellMembersOfDeletionsFutureCall(),
|
||||
'PruningForgetDeletedAccountsFutureCall':
|
||||
PruningForgetDeletedAccountsFutureCall(),
|
||||
};
|
||||
_futureCallManager = futureCallManager;
|
||||
_serverId = serverId;
|
||||
|
|
@ -203,6 +207,20 @@ class _PruningFutureCallDispatcher {
|
|||
null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> tellMembersOfDeletions() {
|
||||
return _invokeFutureCall(
|
||||
'PruningTellMembersOfDeletionsFutureCall',
|
||||
null,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> forgetDeletedAccounts() {
|
||||
return _invokeFutureCall(
|
||||
'PruningForgetDeletedAccountsFutureCall',
|
||||
null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Prunes one batch and logs how many people went.
|
||||
|
|
@ -230,3 +248,29 @@ class PruningForgetSignUpAttemptsFutureCall extends _is.FutureCall
|
|||
await _ibrplims.PruningFutureCall().forgetSignUpAttempts(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// On the register, tells the instance's members again of the accounts
|
||||
/// gone that they haven't confirmed yet, and logs how many confirmed.
|
||||
class PruningTellMembersOfDeletionsFutureCall extends _is.FutureCall
|
||||
implements _is.InvokableFutureCall {
|
||||
@override
|
||||
_ida.Future<void> invoke(
|
||||
_is.Session session,
|
||||
_is.SerializableModel? object,
|
||||
) async {
|
||||
await _ibrplims.PruningFutureCall().tellMembersOfDeletions(session);
|
||||
}
|
||||
}
|
||||
|
||||
/// On a member, deletes the people whose account the register no longer
|
||||
/// knows, and logs how many.
|
||||
class PruningForgetDeletedAccountsFutureCall extends _is.FutureCall
|
||||
implements _is.InvokableFutureCall {
|
||||
@override
|
||||
_ida.Future<void> invoke(
|
||||
_is.Session session,
|
||||
_is.SerializableModel? object,
|
||||
) async {
|
||||
await _ibrplims.PruningFutureCall().forgetDeletedAccounts(session);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,27 @@ const _pruning = 'prune-abandoned-anonymous-people';
|
|||
/// The identifier of the hourly forgetting of sign-up attempts.
|
||||
const _attemptPruning = 'forget-sign-up-attempts';
|
||||
|
||||
/// The identifier of the hourly telling of the members of deleted accounts.
|
||||
const _deletionNotices = 'tell-members-of-deletions';
|
||||
|
||||
/// The identifier of the daily deleting of accounts the register forgot.
|
||||
const _deletedAccounts = 'forget-deleted-accounts';
|
||||
|
||||
/// The future calls [schedulePruning] keeps scheduled, by their identifiers.
|
||||
const pruningIdentifiers = [_pruning, _attemptPruning];
|
||||
const pruningIdentifiers = [
|
||||
_pruning,
|
||||
_attemptPruning,
|
||||
_deletionNotices,
|
||||
_deletedAccounts,
|
||||
];
|
||||
|
||||
/// Schedules the pruning (`PruningFutureCall`): abandoned anonymous people
|
||||
/// daily, sign-up attempts and expired email codes hourly. Scheduling always
|
||||
/// adds an entry, so the previous one goes first, and starting the server
|
||||
/// again leaves one of each.
|
||||
/// daily, sign-up attempts and expired email codes hourly, and the instance's
|
||||
/// accounts (ADR 0016): the register's deletions its members haven't
|
||||
/// confirmed hourly, a member's people the register forgot daily (each does
|
||||
/// nothing in the other mode). Scheduling always adds an entry, so the
|
||||
/// previous one goes first, and starting the server again leaves one of
|
||||
/// each.
|
||||
Future<void> schedulePruning(Serverpod pod) async {
|
||||
await pod.futureCalls.cancel(_pruning);
|
||||
await pod.futureCalls
|
||||
|
|
@ -28,4 +42,18 @@ Future<void> schedulePruning(Serverpod pod) async {
|
|||
.cron('27 * * * *')
|
||||
.pruning
|
||||
.forgetSignUpAttempts();
|
||||
|
||||
await pod.futureCalls.cancel(_deletionNotices);
|
||||
await pod.futureCalls
|
||||
.callRecurring(identifier: _deletionNotices)
|
||||
.cron('52 * * * *')
|
||||
.pruning
|
||||
.tellMembersOfDeletions();
|
||||
|
||||
await pod.futureCalls.cancel(_deletedAccounts);
|
||||
await pod.futureCalls
|
||||
.callRecurring(identifier: _deletedAccounts)
|
||||
.cron('37 3 * * *')
|
||||
.pruning
|
||||
.forgetDeletedAccounts();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,10 @@ import 'package:serverpod/serverpod.dart';
|
|||
/// Deletes the anonymous people nobody can return to, daily, and the IP
|
||||
/// addresses of sign-up attempts the rate limit is done with and the email
|
||||
/// sign-in codes past their time, hourly: halkyon_base's pruning (ADR 0014),
|
||||
/// which the server schedules when it starts (`schedulePruning`).
|
||||
/// which the server schedules when it starts (`schedulePruning`). Then the
|
||||
/// instance's accounts (ADR 0016): as its register, telling the members
|
||||
/// again of the accounts gone, hourly; as a member, deleting the people
|
||||
/// whose account the register no longer knows, daily.
|
||||
class PruningFutureCall extends FutureCall {
|
||||
/// Prunes one batch and logs how many people went.
|
||||
Future<void> pruneAnonymousPeople(Session session) async {
|
||||
|
|
@ -25,4 +28,18 @@ class PruningFutureCall extends FutureCall {
|
|||
final codes = await forgetExpiredEmailCodes(session, now: now);
|
||||
session.log('Forgot $forgotten sign-up attempts and $codes email codes.');
|
||||
}
|
||||
|
||||
/// On the register, tells the instance's members again of the accounts
|
||||
/// gone that they haven't confirmed yet, and logs how many confirmed.
|
||||
Future<void> tellMembersOfDeletions(Session session) async {
|
||||
final confirmed = await deliverDeletionNotices(session);
|
||||
session.log('Members confirmed $confirmed deleted accounts.');
|
||||
}
|
||||
|
||||
/// On a member, deletes the people whose account the register no longer
|
||||
/// knows, and logs how many.
|
||||
Future<void> forgetDeletedAccounts(Session session) async {
|
||||
final forgotten = await forgetPeopleTheRegisterForgot(session);
|
||||
session.log('Deleted $forgotten people the register no longer knows.');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,11 +17,16 @@ const minimumAppBuild = 1;
|
|||
/// the instance's basics: the instance says it serves `todo` (the app accepts
|
||||
/// only such an instance; never change it), sign-in mails name the app, and
|
||||
/// the instance names this project's source. Todo keeps nothing of a person's
|
||||
/// yet, so it deletes nothing of its own with them. The server sets it up
|
||||
/// before it starts (`server.dart`), and so do its tests.
|
||||
HalkyonBase todoBase() => HalkyonBase(
|
||||
app: 'todo',
|
||||
appName: 'Halkyon Todo',
|
||||
upstreamSourceCodeUrl: upstreamSourceCodeUrl,
|
||||
minimumAppBuild: minimumAppBuild,
|
||||
);
|
||||
/// yet, so it deletes nothing of its own with them. It holds the instance's
|
||||
/// register, or is a member of the one [accounts] names (ADR 0016; the
|
||||
/// server reads it from the operator's environment, `ACCOUNT_REGISTER_URL`).
|
||||
/// The server sets it up before it starts (`server.dart`), and so do its
|
||||
/// tests.
|
||||
HalkyonBase todoBase({AccountsMode accounts = const HoldsRegister()}) =>
|
||||
HalkyonBase(
|
||||
app: 'todo',
|
||||
appName: 'Halkyon Todo',
|
||||
upstreamSourceCodeUrl: upstreamSourceCodeUrl,
|
||||
minimumAppBuild: minimumAppBuild,
|
||||
accounts: accounts,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue