halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.
The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 14:28:09 +02:00
|
|
|
import 'package:halkyon_base_server/base.dart';
|
|
|
|
|
import 'package:serverpod/serverpod.dart';
|
|
|
|
|
import 'package:test/test.dart';
|
|
|
|
|
|
|
|
|
|
import '../support/fixtures.dart';
|
|
|
|
|
import '../test_tools/serverpod_test_tools.dart';
|
|
|
|
|
|
|
|
|
|
/// Keeps nothing; there to say the instance can mail codes.
|
|
|
|
|
final class _NoMail implements CodeMailer {
|
|
|
|
|
@override
|
|
|
|
|
Future<void> send(
|
|
|
|
|
Session session, {
|
|
|
|
|
required String to,
|
|
|
|
|
required String code,
|
|
|
|
|
required String language,
|
|
|
|
|
}) async {}
|
feat(auth): a password with TOTP on the register, and resets
The register's side of signing in with an address, a password that never
leaves the device (SRP-6a) and a code from an authenticator (ADR 0017),
with members forwarding every step through AccountRegister:
- password.set: salt, Argon2id's costs, SRP's verifier, the public keys
and the private keys wrapped twice, all made on the device; with the
address a mailed code proves, an anonymous person becomes identified
under their ID. It signs in once an authenticator is confirmed.
- passwordSignIn.begin and complete: SRP's two steps with a TOTP or
recovery code. An address nobody has is answered from a decoy key as
an account would be; every wrong credential is refused alike.
- authenticator: secrets sealed under totpKey (XChaCha20-Poly1305, bound
to their row), each code accepted once, ten recovery codes with the
first, the last one kept.
- accountRecovery: a lost authenticator reset 72 hours after the
password and a mailed code or the recovery phrase ask, a lost password
set anew with the phrase's signature, or the account reset with new
keys 72 hours after a mailed code asks; a mail at each start, a
sign-in calling it off.
- An account with a password no longer signs in by a mailed code or
GitHub; no new GitHub account is linked where passwords are offered,
and from GITHUB_SIGN_IN_ENDS GitHub signs nobody in and its IDs go.
Five tables, added by the module's migration; deletePerson deletes
their rows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 13:32:12 +02:00
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Future<void> sendResetNotice(
|
|
|
|
|
Session session, {
|
|
|
|
|
required String to,
|
|
|
|
|
required AccountResetKind kind,
|
|
|
|
|
required DateTime takesEffectAt,
|
|
|
|
|
required String language,
|
|
|
|
|
}) async {}
|
halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.
The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 14:28:09 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void main() {
|
|
|
|
|
withServerpod('What every instance tells its apps', (
|
|
|
|
|
sessionBuilder,
|
|
|
|
|
endpoints,
|
|
|
|
|
) {
|
|
|
|
|
late InstanceBasics basics;
|
|
|
|
|
|
|
|
|
|
setUp(() async {
|
|
|
|
|
useBase(sessionBuilder);
|
|
|
|
|
basics = await endpoints.instance.basics(
|
|
|
|
|
sessionBuilder.copyWith(
|
|
|
|
|
authentication: AuthenticationOverride.unauthenticated(),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
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>
2026-10-19 15:18:49 +02:00
|
|
|
test('names the app it serves, so an app recognises its own instances', () {
|
|
|
|
|
expect(basics.app, testApp);
|
|
|
|
|
});
|
|
|
|
|
|
halkyon_base: a Serverpod module for sign-in, accounts and instance basics
What every Halkyon server has, taken from Halkyon Learn's: anonymous,
GitHub and email-code sign-in with their endpoints, an account's ways
in and its deletion, the pruning of abandoned anonymous people, of
sign-up attempts and of expired codes, and the instance's basics (the
sign-in ways offered, the source link, the minimum app build, where
people's data and its backups are). Endpoints sit under the nickname
`base`. A host configures it in one place before its pod starts: its
name for the mails, its source link, its minimum build, and hooks that
delete its own data with a person.
The tables keep Learn's names, columns and indexes, so a host moving
onto the module migrates nothing but version rows. Server package under
the SSPL, client under the AGPL, as Learn's are.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 14:28:09 +02:00
|
|
|
test("names where the host's project publishes its source when the "
|
|
|
|
|
'operator set nothing', () {
|
|
|
|
|
// The tests run without SOURCE_URL.
|
|
|
|
|
expect(basics.sourceCodeUrl, testUpstreamSourceCodeUrl);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('names the oldest app build the host works with, to anyone, so an '
|
|
|
|
|
'older app can say it needs an update', () {
|
|
|
|
|
expect(basics.minimumAppBuild, testMinimumAppBuild);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('says nothing of where its data and backups are where the '
|
|
|
|
|
'operator said nothing', () {
|
|
|
|
|
// The tests run without DATA_LOCATION and BACKUP_LOCATION.
|
|
|
|
|
expect(basics.dataLocation, isNull);
|
|
|
|
|
expect(basics.backupLocation, isNull);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('offers no GitHub sign-in without an OAuth app of its own', () {
|
|
|
|
|
// The tests' passwords hold no githubClientId.
|
|
|
|
|
expect(basics.gitHubClientId, isNull);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('offers email sign-in only where it can mail codes', () async {
|
|
|
|
|
final codes = EmailCodes.of(sessionBuilder.build().serverpod);
|
|
|
|
|
final unauthenticated = sessionBuilder.copyWith(
|
|
|
|
|
authentication: AuthenticationOverride.unauthenticated(),
|
|
|
|
|
);
|
|
|
|
|
addTearDown(() => codes.mailer = null);
|
|
|
|
|
|
|
|
|
|
codes.mailer = null;
|
|
|
|
|
expect(
|
|
|
|
|
(await endpoints.instance.basics(unauthenticated)).emailSignIn,
|
|
|
|
|
isFalse,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
codes.mailer = _NoMail();
|
|
|
|
|
expect(
|
|
|
|
|
(await endpoints.instance.basics(unauthenticated)).emailSignIn,
|
|
|
|
|
isTrue,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|