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:
parent
af1de799f4
commit
e23cc77f7c
7 changed files with 10761 additions and 10 deletions
|
|
@ -17,7 +17,7 @@ Endpoints, under the nickname `base` in a host's client (`client.base.emailCode`
|
|||
| `emailCode` | `sendCode`, `verifyCode`: a code by mail; a new address becomes the caller's, another person's gives a switch token | signed in |
|
||||
| `emailSignIn` | `signIn`: a session for the person a switch token names | without a session |
|
||||
| `account` | `delete`, `signInWays` | signed in |
|
||||
| `instance` | `basics`: GitHub's client ID if any, whether email sign-in is on, the source link, the oldest app build, where data and backups are | anyone |
|
||||
| `instance` | `basics`: the app it serves, GitHub's client ID if any, whether email sign-in is on, the source link, the oldest app build, where data and backups are | anyone |
|
||||
|
||||
- GitHub keeps only the numeric GitHub ID, in Serverpod's own table; email keeps only
|
||||
HMAC-SHA256 of the address, keyed with `emailSignInPepper` (`email_identity`), and its
|
||||
|
|
@ -60,6 +60,7 @@ Endpoints, under the nickname `base` in a host's client (`client.base.emailCode`
|
|||
HalkyonBase.configure(
|
||||
pod,
|
||||
HalkyonBase(
|
||||
app: 'todo', // its apps accept only instances naming it; never change it
|
||||
appName: 'Halkyon Todo', // in sign-in mails
|
||||
upstreamSourceCodeUrl: 'https://code.halkyon.app/project-halkyon/halkyon_todo',
|
||||
minimumAppBuild: 1, // raise it in the commit that breaks older apps
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ void main() {
|
|||
);
|
||||
});
|
||||
|
||||
test('names the app it serves, so an app recognises its own instances', () {
|
||||
expect(basics.app, testApp);
|
||||
});
|
||||
|
||||
test("names where the host's project publishes its source when the "
|
||||
'operator set nothing', () {
|
||||
// The tests run without SOURCE_URL.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import '../test_tools/serverpod_test_tools.dart';
|
|||
/// The app the tests' host is.
|
||||
const testAppName = 'Halkyon Test';
|
||||
|
||||
/// The id of the app the tests' host serves.
|
||||
const testApp = 'test';
|
||||
|
||||
/// Where the tests' host says its project publishes its source.
|
||||
const testUpstreamSourceCodeUrl = 'https://code.example.org/halkyon_test';
|
||||
|
||||
|
|
@ -26,6 +29,7 @@ void useBase(
|
|||
HalkyonBase.configure(
|
||||
pod,
|
||||
HalkyonBase(
|
||||
app: testApp,
|
||||
appName: testAppName,
|
||||
upstreamSourceCodeUrl: testUpstreamSourceCodeUrl,
|
||||
minimumAppBuild: testMinimumAppBuild,
|
||||
|
|
|
|||
Loading…
Reference in a new issue