halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps

What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.

An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.

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 15:18:49 +02:00
commit e23cc77f7c
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
4 changed files with 10761 additions and 10 deletions

View file

@ -13,12 +13,13 @@
import 'package:serverpod/serverpod.dart' as _is;
/// What every Halkyon instance tells its apps, to anyone, before anyone signs
/// in: how people sign in here, where the source of what it runs is, the
/// in: which app it serves, how people sign in here, where the source of what it runs is, the
/// oldest app build it works with, and where it keeps people's data. Every
/// instance is run by someone else, with its own configuration.
abstract class InstanceBasics
implements _is.SerializableModel, _is.ProtocolSerialization {
InstanceBasics._({
required this.app,
this.gitHubClientId,
required this.emailSignIn,
required this.sourceCodeUrl,
@ -28,6 +29,7 @@ abstract class InstanceBasics
});
factory InstanceBasics({
required String app,
String? gitHubClientId,
required bool emailSignIn,
required String sourceCodeUrl,
@ -38,6 +40,7 @@ abstract class InstanceBasics
factory InstanceBasics.fromJson(Map<String, dynamic> jsonSerialization) {
return InstanceBasics(
app: jsonSerialization['app'] as String,
gitHubClientId: jsonSerialization['gitHubClientId'] as String?,
emailSignIn: _is.BoolJsonExtension.fromJson(
jsonSerialization['emailSignIn'],
@ -49,6 +52,10 @@ abstract class InstanceBasics
);
}
/// Which app this instance serves (`learn` for Halkyon Learn), as its host
/// configures it: an app accepts only an instance of its own.
String app;
/// The client ID of this instance's GitHub OAuth app, or null when GitHub
/// sign-in is off here.
String? gitHubClientId;
@ -79,6 +86,7 @@ abstract class InstanceBasics
/// with some or all fields replaced by the given arguments.
@_is.useResult
InstanceBasics copyWith({
String? app,
String? gitHubClientId,
bool? emailSignIn,
String? sourceCodeUrl,
@ -90,6 +98,7 @@ abstract class InstanceBasics
Map<String, dynamic> toJson() {
return {
'__className__': 'halkyon_base.InstanceBasics',
'app': app,
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
'emailSignIn': emailSignIn,
'sourceCodeUrl': sourceCodeUrl,
@ -103,6 +112,7 @@ abstract class InstanceBasics
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'halkyon_base.InstanceBasics',
'app': app,
if (gitHubClientId != null) 'gitHubClientId': gitHubClientId,
'emailSignIn': emailSignIn,
'sourceCodeUrl': sourceCodeUrl,
@ -122,6 +132,7 @@ class _Undefined {}
class _InstanceBasicsImpl extends InstanceBasics {
_InstanceBasicsImpl({
required String app,
String? gitHubClientId,
required bool emailSignIn,
required String sourceCodeUrl,
@ -129,6 +140,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
String? dataLocation,
String? backupLocation,
}) : super._(
app: app,
gitHubClientId: gitHubClientId,
emailSignIn: emailSignIn,
sourceCodeUrl: sourceCodeUrl,
@ -142,6 +154,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
@_is.useResult
@override
InstanceBasics copyWith({
String? app,
Object? gitHubClientId = _Undefined,
bool? emailSignIn,
String? sourceCodeUrl,
@ -150,6 +163,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
Object? backupLocation = _Undefined,
}) {
return InstanceBasics(
app: app ?? this.app,
gitHubClientId: gitHubClientId is String?
? gitHubClientId
: this.gitHubClientId,

View file

@ -20,12 +20,18 @@ typedef AfterDeletion = Future<void> Function();
/// pod, before the pod starts ([configure]).
final class HalkyonBase {
HalkyonBase({
required this.app,
required this.appName,
required this.upstreamSourceCodeUrl,
required this.minimumAppBuild,
this.deletePersonData = const [],
});
/// Which app this server serves, as its apps know it (`learn` for Halkyon
/// Learn): an instance names it in its basics, so an app recognises its own
/// instances and refuses another app's. Never change it once apps use it.
final String app;
/// The app's name as people know it, such as "Halkyon Learn": sign-in
/// mails name it in their subject and text, and as their sender.
final String appName;

View file

@ -1,9 +1,12 @@
### What every Halkyon instance tells its apps, to anyone, before anyone signs
### in: how people sign in here, where the source of what it runs is, the
### in: which app it serves, how people sign in here, where the source of what it runs is, the
### oldest app build it works with, and where it keeps people's data. Every
### instance is run by someone else, with its own configuration.
class: InstanceBasics
fields:
### Which app this instance serves (`learn` for Halkyon Learn), as its host
### configures it: an app accepts only an instance of its own.
app: String
### The client ID of this instance's GitHub OAuth app, or null when GitHub
### sign-in is off here.
gitHubClientId: String?

View file

@ -9,19 +9,21 @@ import 'package:serverpod/serverpod.dart';
/// What every Halkyon instance tells its apps. Public: an app asks before
/// anyone is signed in.
class InstanceEndpoint extends Endpoint {
/// This instance's basics: GitHub sign-in only with the operator's OAuth
/// 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.
Future<InstanceBasics> basics(Session session) async {
final pod = session.serverpod;
final base = HalkyonBase.of(pod);
return InstanceBasics(
app: base.app,
gitHubClientId: gitHubSignInAvailable(pod)
? pod.getPassword('githubClientId')
: null,
emailSignIn: emailSignInAvailable(pod),
sourceCodeUrl: sourceCodeUrl(pod),
minimumAppBuild: HalkyonBase.of(pod).minimumAppBuild,
minimumAppBuild: base.minimumAppBuild,
dataLocation: dataLocation(),
backupLocation: backupLocation(),
);