feat(instance): the basics say when GitHub sign-in ends

ADR 0017, D2: GitHub sign-in is deprecated. Where the operator set
`GITHUB_SIGN_IN_ENDS`, `instance.basics()` names the day until it has
come, so apps can tell people who sign in with GitHub to add an
address, a password and an authenticator before then.

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-20 14:55:13 +02:00
commit 1daa1b649a
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
6 changed files with 85 additions and 6 deletions

View file

@ -229,9 +229,8 @@ class EndpointInstance extends _isc.EndpointRef {
/// operator's OAuth app and until it ends (ADR 0017), email sign-in only /// operator's OAuth app and until it ends (ADR 0017), email sign-in only
/// where the register can mail codes (this server, or on a member the /// where the register can mail codes (this server, or on a member the
/// register, ADR 0016), password sign-in where the register holds its /// register, ADR 0016), password sign-in where the register holds its
/// keys, where its /// keys, when GitHub sign-in ends, where its source is, the oldest app
/// source is, the oldest app build it works with, and where it keeps /// build it works with, and where it keeps people's data and its backups.
/// people's data and its backups.
_ida.Future<_isnyur2i.InstanceBasics> basics() => _ida.Future<_isnyur2i.InstanceBasics> basics() =>
caller.callServerEndpoint<_isnyur2i.InstanceBasics>( caller.callServerEndpoint<_isnyur2i.InstanceBasics>(
'halkyon_base.instance', 'halkyon_base.instance',

View file

@ -27,6 +27,7 @@ abstract class InstanceBasics
this.dataLocation, this.dataLocation,
this.backupLocation, this.backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
this.gitHubSignInEnds,
}) : passwordSignIn = passwordSignIn ?? false; }) : passwordSignIn = passwordSignIn ?? false;
factory InstanceBasics({ factory InstanceBasics({
@ -38,6 +39,7 @@ abstract class InstanceBasics
String? dataLocation, String? dataLocation,
String? backupLocation, String? backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
DateTime? gitHubSignInEnds,
}) = _InstanceBasicsImpl; }) = _InstanceBasicsImpl;
factory InstanceBasics.fromJson(Map<String, dynamic> jsonSerialization) { factory InstanceBasics.fromJson(Map<String, dynamic> jsonSerialization) {
@ -56,6 +58,11 @@ abstract class InstanceBasics
: _isc.BoolJsonExtension.fromJson( : _isc.BoolJsonExtension.fromJson(
jsonSerialization['passwordSignIn'], jsonSerialization['passwordSignIn'],
), ),
gitHubSignInEnds: jsonSerialization['gitHubSignInEnds'] == null
? null
: _isc.DateTimeJsonExtension.fromJson(
jsonSerialization['gitHubSignInEnds'],
),
); );
} }
@ -93,6 +100,12 @@ abstract class InstanceBasics
/// operator set the keys it needs. /// operator set the keys it needs.
bool passwordSignIn; bool passwordSignIn;
/// When GitHub sign-in ends here (ADR 0017: it is deprecated), as its
/// operator set it (GITHUB_SIGN_IN_ENDS), in UTC; null while no end is set,
/// or once it has ended. People who sign in with GitHub add an address, a
/// password and an authenticator before then.
DateTime? gitHubSignInEnds;
/// Returns a shallow copy of this [InstanceBasics] /// Returns a shallow copy of this [InstanceBasics]
/// with some or all fields replaced by the given arguments. /// with some or all fields replaced by the given arguments.
@_isc.useResult @_isc.useResult
@ -105,6 +118,7 @@ abstract class InstanceBasics
String? dataLocation, String? dataLocation,
String? backupLocation, String? backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
DateTime? gitHubSignInEnds,
}); });
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -118,6 +132,8 @@ abstract class InstanceBasics
if (dataLocation != null) 'dataLocation': dataLocation, if (dataLocation != null) 'dataLocation': dataLocation,
if (backupLocation != null) 'backupLocation': backupLocation, if (backupLocation != null) 'backupLocation': backupLocation,
'passwordSignIn': passwordSignIn, 'passwordSignIn': passwordSignIn,
if (gitHubSignInEnds != null)
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
}; };
} }
@ -133,6 +149,8 @@ abstract class InstanceBasics
if (dataLocation != null) 'dataLocation': dataLocation, if (dataLocation != null) 'dataLocation': dataLocation,
if (backupLocation != null) 'backupLocation': backupLocation, if (backupLocation != null) 'backupLocation': backupLocation,
'passwordSignIn': passwordSignIn, 'passwordSignIn': passwordSignIn,
if (gitHubSignInEnds != null)
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
}; };
} }
@ -154,6 +172,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
String? dataLocation, String? dataLocation,
String? backupLocation, String? backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
DateTime? gitHubSignInEnds,
}) : super._( }) : super._(
app: app, app: app,
gitHubClientId: gitHubClientId, gitHubClientId: gitHubClientId,
@ -163,6 +182,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
dataLocation: dataLocation, dataLocation: dataLocation,
backupLocation: backupLocation, backupLocation: backupLocation,
passwordSignIn: passwordSignIn, passwordSignIn: passwordSignIn,
gitHubSignInEnds: gitHubSignInEnds,
); );
/// Returns a shallow copy of this [InstanceBasics] /// Returns a shallow copy of this [InstanceBasics]
@ -178,6 +198,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
Object? dataLocation = _Undefined, Object? dataLocation = _Undefined,
Object? backupLocation = _Undefined, Object? backupLocation = _Undefined,
bool? passwordSignIn, bool? passwordSignIn,
Object? gitHubSignInEnds = _Undefined,
}) { }) {
return InstanceBasics( return InstanceBasics(
app: app ?? this.app, app: app ?? this.app,
@ -192,6 +213,9 @@ class _InstanceBasicsImpl extends InstanceBasics {
? backupLocation ? backupLocation
: this.backupLocation, : this.backupLocation,
passwordSignIn: passwordSignIn ?? this.passwordSignIn, passwordSignIn: passwordSignIn ?? this.passwordSignIn,
gitHubSignInEnds: gitHubSignInEnds is DateTime?
? gitHubSignInEnds
: this.gitHubSignInEnds,
); );
} }
} }

View file

@ -27,6 +27,7 @@ abstract class InstanceBasics
this.dataLocation, this.dataLocation,
this.backupLocation, this.backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
this.gitHubSignInEnds,
}) : passwordSignIn = passwordSignIn ?? false; }) : passwordSignIn = passwordSignIn ?? false;
factory InstanceBasics({ factory InstanceBasics({
@ -38,6 +39,7 @@ abstract class InstanceBasics
String? dataLocation, String? dataLocation,
String? backupLocation, String? backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
DateTime? gitHubSignInEnds,
}) = _InstanceBasicsImpl; }) = _InstanceBasicsImpl;
factory InstanceBasics.fromJson(Map<String, dynamic> jsonSerialization) { factory InstanceBasics.fromJson(Map<String, dynamic> jsonSerialization) {
@ -54,6 +56,11 @@ abstract class InstanceBasics
passwordSignIn: jsonSerialization['passwordSignIn'] == null passwordSignIn: jsonSerialization['passwordSignIn'] == null
? null ? null
: _is.BoolJsonExtension.fromJson(jsonSerialization['passwordSignIn']), : _is.BoolJsonExtension.fromJson(jsonSerialization['passwordSignIn']),
gitHubSignInEnds: jsonSerialization['gitHubSignInEnds'] == null
? null
: _is.DateTimeJsonExtension.fromJson(
jsonSerialization['gitHubSignInEnds'],
),
); );
} }
@ -91,6 +98,12 @@ abstract class InstanceBasics
/// operator set the keys it needs. /// operator set the keys it needs.
bool passwordSignIn; bool passwordSignIn;
/// When GitHub sign-in ends here (ADR 0017: it is deprecated), as its
/// operator set it (GITHUB_SIGN_IN_ENDS), in UTC; null while no end is set,
/// or once it has ended. People who sign in with GitHub add an address, a
/// password and an authenticator before then.
DateTime? gitHubSignInEnds;
/// Returns a shallow copy of this [InstanceBasics] /// Returns a shallow copy of this [InstanceBasics]
/// with some or all fields replaced by the given arguments. /// with some or all fields replaced by the given arguments.
@_is.useResult @_is.useResult
@ -103,6 +116,7 @@ abstract class InstanceBasics
String? dataLocation, String? dataLocation,
String? backupLocation, String? backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
DateTime? gitHubSignInEnds,
}); });
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -116,6 +130,8 @@ abstract class InstanceBasics
if (dataLocation != null) 'dataLocation': dataLocation, if (dataLocation != null) 'dataLocation': dataLocation,
if (backupLocation != null) 'backupLocation': backupLocation, if (backupLocation != null) 'backupLocation': backupLocation,
'passwordSignIn': passwordSignIn, 'passwordSignIn': passwordSignIn,
if (gitHubSignInEnds != null)
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
}; };
} }
@ -131,6 +147,8 @@ abstract class InstanceBasics
if (dataLocation != null) 'dataLocation': dataLocation, if (dataLocation != null) 'dataLocation': dataLocation,
if (backupLocation != null) 'backupLocation': backupLocation, if (backupLocation != null) 'backupLocation': backupLocation,
'passwordSignIn': passwordSignIn, 'passwordSignIn': passwordSignIn,
if (gitHubSignInEnds != null)
'gitHubSignInEnds': gitHubSignInEnds?.toJson(),
}; };
} }
@ -152,6 +170,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
String? dataLocation, String? dataLocation,
String? backupLocation, String? backupLocation,
bool? passwordSignIn, bool? passwordSignIn,
DateTime? gitHubSignInEnds,
}) : super._( }) : super._(
app: app, app: app,
gitHubClientId: gitHubClientId, gitHubClientId: gitHubClientId,
@ -161,6 +180,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
dataLocation: dataLocation, dataLocation: dataLocation,
backupLocation: backupLocation, backupLocation: backupLocation,
passwordSignIn: passwordSignIn, passwordSignIn: passwordSignIn,
gitHubSignInEnds: gitHubSignInEnds,
); );
/// Returns a shallow copy of this [InstanceBasics] /// Returns a shallow copy of this [InstanceBasics]
@ -176,6 +196,7 @@ class _InstanceBasicsImpl extends InstanceBasics {
Object? dataLocation = _Undefined, Object? dataLocation = _Undefined,
Object? backupLocation = _Undefined, Object? backupLocation = _Undefined,
bool? passwordSignIn, bool? passwordSignIn,
Object? gitHubSignInEnds = _Undefined,
}) { }) {
return InstanceBasics( return InstanceBasics(
app: app ?? this.app, app: app ?? this.app,
@ -190,6 +211,9 @@ class _InstanceBasicsImpl extends InstanceBasics {
? backupLocation ? backupLocation
: this.backupLocation, : this.backupLocation,
passwordSignIn: passwordSignIn ?? this.passwordSignIn, passwordSignIn: passwordSignIn ?? this.passwordSignIn,
gitHubSignInEnds: gitHubSignInEnds is DateTime?
? gitHubSignInEnds
: this.gitHubSignInEnds,
); );
} }
} }

View file

@ -30,3 +30,8 @@ fields:
### Whether people sign in with a password and TOTP here (ADR 0017): the ### Whether people sign in with a password and TOTP here (ADR 0017): the
### operator set the keys it needs. ### operator set the keys it needs.
passwordSignIn: bool, default=false passwordSignIn: bool, default=false
### When GitHub sign-in ends here (ADR 0017: it is deprecated), as its
### operator set it (GITHUB_SIGN_IN_ENDS), in UTC; null while no end is set,
### or once it has ended. People who sign in with GitHub add an address, a
### password and an authenticator before then.
gitHubSignInEnds: DateTime?

View file

@ -17,9 +17,8 @@ class InstanceEndpoint extends Endpoint {
/// operator's OAuth app and until it ends (ADR 0017), email sign-in only /// operator's OAuth app and until it ends (ADR 0017), email sign-in only
/// where the register can mail codes (this server, or on a member the /// where the register can mail codes (this server, or on a member the
/// register, ADR 0016), password sign-in where the register holds its /// register, ADR 0016), password sign-in where the register holds its
/// keys, where its /// keys, when GitHub sign-in ends, where its source is, the oldest app
/// source is, the oldest app build it works with, and where it keeps /// build it works with, and where it keeps people's data and its backups.
/// people's data and its backups.
Future<InstanceBasics> basics(Session session) async { Future<InstanceBasics> basics(Session session) async {
final pod = session.serverpod; final pod = session.serverpod;
final base = HalkyonBase.of(pod); final base = HalkyonBase.of(pod);
@ -42,6 +41,7 @@ class InstanceEndpoint extends Endpoint {
minimumAppBuild: base.minimumAppBuild, minimumAppBuild: base.minimumAppBuild,
dataLocation: dataLocation(), dataLocation: dataLocation(),
backupLocation: backupLocation(), backupLocation: backupLocation(),
gitHubSignInEnds: gitHubSignInEnded(pod) ? null : gitHubSignInEnds(pod),
); );
} }
} }

View file

@ -1,3 +1,4 @@
import 'package:clock/clock.dart';
import 'package:halkyon_base_server/base.dart'; import 'package:halkyon_base_server/base.dart';
import 'package:serverpod/serverpod.dart'; import 'package:serverpod/serverpod.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
@ -68,6 +69,32 @@ void main() {
expect(basics.gitHubClientId, isNull); expect(basics.gitHubClientId, isNull);
}); });
test('says when GitHub sign-in ends, until it has', () async {
final pod = sessionBuilder.build().serverpod;
final unauthenticated = sessionBuilder.copyWith(
authentication: AuthenticationOverride.unauthenticated(),
);
addTearDown(() => useGitHubSignInEnds(pod, null));
expect(basics.gitHubSignInEnds, isNull);
final ends = DateTime.utc(2027, 1, 22);
useGitHubSignInEnds(pod, ends);
expect(
(await withClock(
Clock.fixed(DateTime.utc(2026, 10, 22)),
() => endpoints.instance.basics(unauthenticated),
)).gitHubSignInEnds,
ends,
);
expect(
(await withClock(
Clock.fixed(ends),
() => endpoints.instance.basics(unauthenticated),
)).gitHubSignInEnds,
isNull,
);
});
test('offers email sign-in only where it can mail codes', () async { test('offers email sign-in only where it can mail codes', () async {
final codes = EmailCodes.of(sessionBuilder.build().serverpod); final codes = EmailCodes.of(sessionBuilder.build().serverpod);
final unauthenticated = sessionBuilder.copyWith( final unauthenticated = sessionBuilder.copyWith(