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
|
|
|
/* AUTOMATICALLY GENERATED CODE DO NOT MODIFY */
|
|
|
|
|
/* To generate run: "serverpod generate" */
|
|
|
|
|
|
|
|
|
|
// ignore_for_file: implementation_imports
|
|
|
|
|
// ignore_for_file: library_private_types_in_public_api
|
|
|
|
|
// ignore_for_file: non_constant_identifier_names
|
|
|
|
|
// ignore_for_file: public_member_api_docs
|
|
|
|
|
// ignore_for_file: type_literal_in_constant_pattern
|
|
|
|
|
// ignore_for_file: use_super_parameters
|
|
|
|
|
// ignore_for_file: invalid_use_of_internal_member
|
|
|
|
|
// ignore_for_file: dead_code, unnecessary_type_check
|
|
|
|
|
|
|
|
|
|
// ignore_for_file: no_leading_underscores_for_library_prefixes
|
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
|
|
|
import 'package:halkyon_base_client/src/protocol/password/authenticator.dart'
|
|
|
|
|
as _ivfmflch;
|
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:serverpod_auth_core_client/serverpod_auth_core_client.dart'
|
|
|
|
|
as _iacc;
|
|
|
|
|
import 'package:serverpod_auth_idp_client/serverpod_auth_idp_client.dart'
|
|
|
|
|
as _iaic;
|
|
|
|
|
import 'package:serverpod_client/serverpod_client.dart' as _isc;
|
|
|
|
|
import 'auth/email_code_accepted.dart' as _iys46ui1;
|
|
|
|
|
import 'auth/email_sign_in_refusal.dart' as _ieyvcag1;
|
|
|
|
|
import 'auth/email_sign_in_refused_exception.dart' as _iztfgag5;
|
|
|
|
|
import 'auth/github_sign_in_refusal.dart' as _ir1gd9p5;
|
|
|
|
|
import 'auth/github_sign_in_refused_exception.dart' as _i0xfcu5n;
|
|
|
|
|
import 'auth/sign_in_ways.dart' as _ie6tbh85;
|
|
|
|
|
import 'instance/instance_basics.dart' as _i8jixqx6;
|
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
|
|
|
import 'password/account_reset_kind.dart' as _i91ka1it;
|
|
|
|
|
import 'password/authenticator.dart' as _icpzuvvi;
|
|
|
|
|
import 'password/authenticator_enrolment.dart' as _iv7tra7v;
|
|
|
|
|
import 'password/key_proof_challenge.dart' as _iehtx95q;
|
|
|
|
|
import 'password/new_password.dart' as _itvvd6m6;
|
|
|
|
|
import 'password/password_challenge.dart' as _iuxisac7;
|
|
|
|
|
import 'password/password_costs.dart' as _izp492e5;
|
|
|
|
|
import 'password/password_refusal.dart' as _ibfrwdhm;
|
|
|
|
|
import 'password/password_refused_exception.dart' as _i4froq5y;
|
|
|
|
|
import 'password/password_setup.dart' as _igpggy1y;
|
|
|
|
|
import 'password/password_signed_in.dart' as _ipk7gvzy;
|
|
|
|
|
import 'password/reset_pending.dart' as _i3y6qzi3;
|
|
|
|
|
import 'password/sign_in_challenge_purpose.dart' as _izijxzng;
|
|
|
|
|
import 'password/wrapped_keys.dart' as _i6v10ugm;
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
import 'profile/profile.dart' as _ia5j8fch;
|
|
|
|
|
import 'profile/profile_not_found_exception.dart' as _iim3hfcf;
|
|
|
|
|
import 'profile/profile_refusal.dart' as _i6j6efd1;
|
|
|
|
|
import 'profile/profile_refused_exception.dart' as _idvc5e7p;
|
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
|
|
|
export 'auth/email_code_accepted.dart';
|
|
|
|
|
export 'auth/email_sign_in_refusal.dart';
|
|
|
|
|
export 'auth/email_sign_in_refused_exception.dart';
|
|
|
|
|
export 'auth/github_sign_in_refusal.dart';
|
|
|
|
|
export 'auth/github_sign_in_refused_exception.dart';
|
|
|
|
|
export 'auth/sign_in_ways.dart';
|
|
|
|
|
export 'instance/instance_basics.dart';
|
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
|
|
|
export 'password/account_reset_kind.dart';
|
|
|
|
|
export 'password/authenticator.dart';
|
|
|
|
|
export 'password/authenticator_enrolment.dart';
|
|
|
|
|
export 'password/key_proof_challenge.dart';
|
|
|
|
|
export 'password/new_password.dart';
|
|
|
|
|
export 'password/password_challenge.dart';
|
|
|
|
|
export 'password/password_costs.dart';
|
|
|
|
|
export 'password/password_refusal.dart';
|
|
|
|
|
export 'password/password_refused_exception.dart';
|
|
|
|
|
export 'password/password_setup.dart';
|
|
|
|
|
export 'password/password_signed_in.dart';
|
|
|
|
|
export 'password/reset_pending.dart';
|
|
|
|
|
export 'password/sign_in_challenge_purpose.dart';
|
|
|
|
|
export 'password/wrapped_keys.dart';
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
export 'profile/profile.dart';
|
|
|
|
|
export 'profile/profile_not_found_exception.dart';
|
|
|
|
|
export 'profile/profile_refusal.dart';
|
|
|
|
|
export 'profile/profile_refused_exception.dart';
|
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
|
|
|
export 'client.dart';
|
|
|
|
|
|
|
|
|
|
class Protocol extends _isc.SerializationManager {
|
|
|
|
|
Protocol._();
|
|
|
|
|
|
|
|
|
|
factory Protocol() => _instance;
|
|
|
|
|
|
|
|
|
|
static final Protocol _instance = Protocol._();
|
|
|
|
|
|
|
|
|
|
final Set<_isc.SerializationManager> _hostProtocols = {};
|
|
|
|
|
|
|
|
|
|
void registerHostProtocol(
|
|
|
|
|
String projectName,
|
|
|
|
|
_isc.SerializationManager protocol,
|
|
|
|
|
) {
|
|
|
|
|
_hostProtocols.add(protocol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static String? getClassNameFromObjectJson(dynamic data) {
|
|
|
|
|
if (data is! Map) return null;
|
|
|
|
|
final className = data['__className__'] as String?;
|
|
|
|
|
if (className == null) return null;
|
|
|
|
|
if (!className.startsWith('halkyon_base.')) return className;
|
|
|
|
|
return className.substring(13);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
T deserialize<T>(
|
|
|
|
|
dynamic data, [
|
|
|
|
|
Type? t,
|
|
|
|
|
]) {
|
|
|
|
|
t ??= T;
|
|
|
|
|
|
|
|
|
|
final dataClassName = getClassNameFromObjectJson(data);
|
|
|
|
|
if (dataClassName != null && dataClassName != getClassNameForType(t)) {
|
|
|
|
|
try {
|
|
|
|
|
return deserializeByClassName({
|
|
|
|
|
'className': dataClassName,
|
|
|
|
|
'data': data,
|
|
|
|
|
});
|
|
|
|
|
} on _isc.DeserializationClassNameNotFoundException catch (_) {
|
|
|
|
|
// If the className is not recognized (e.g., older client receiving
|
|
|
|
|
// data with a new subtype), fall back to deserializing without the
|
|
|
|
|
// className, using the expected type T.
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (t == _iys46ui1.EmailCodeAccepted) {
|
|
|
|
|
return _iys46ui1.EmailCodeAccepted.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _ieyvcag1.EmailSignInRefusal) {
|
|
|
|
|
return _ieyvcag1.EmailSignInRefusal.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _iztfgag5.EmailSignInRefusedException) {
|
|
|
|
|
return _iztfgag5.EmailSignInRefusedException.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _ir1gd9p5.GitHubSignInRefusal) {
|
|
|
|
|
return _ir1gd9p5.GitHubSignInRefusal.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _i0xfcu5n.GitHubSignInRefusedException) {
|
|
|
|
|
return _i0xfcu5n.GitHubSignInRefusedException.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _ie6tbh85.SignInWays) {
|
|
|
|
|
return _ie6tbh85.SignInWays.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _i8jixqx6.InstanceBasics) {
|
|
|
|
|
return _i8jixqx6.InstanceBasics.fromJson(data) as T;
|
|
|
|
|
}
|
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
|
|
|
if (t == _i91ka1it.AccountResetKind) {
|
|
|
|
|
return _i91ka1it.AccountResetKind.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _icpzuvvi.Authenticator) {
|
|
|
|
|
return _icpzuvvi.Authenticator.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _iv7tra7v.AuthenticatorEnrolment) {
|
|
|
|
|
return _iv7tra7v.AuthenticatorEnrolment.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _iehtx95q.KeyProofChallenge) {
|
|
|
|
|
return _iehtx95q.KeyProofChallenge.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _itvvd6m6.NewPassword) {
|
|
|
|
|
return _itvvd6m6.NewPassword.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _iuxisac7.PasswordChallenge) {
|
|
|
|
|
return _iuxisac7.PasswordChallenge.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _izp492e5.PasswordCosts) {
|
|
|
|
|
return _izp492e5.PasswordCosts.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _ibfrwdhm.PasswordRefusal) {
|
|
|
|
|
return _ibfrwdhm.PasswordRefusal.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _i4froq5y.PasswordRefusedException) {
|
|
|
|
|
return _i4froq5y.PasswordRefusedException.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _igpggy1y.PasswordSetup) {
|
|
|
|
|
return _igpggy1y.PasswordSetup.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _ipk7gvzy.PasswordSignedIn) {
|
|
|
|
|
return _ipk7gvzy.PasswordSignedIn.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _i3y6qzi3.ResetPending) {
|
|
|
|
|
return _i3y6qzi3.ResetPending.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _izijxzng.SignInChallengePurpose) {
|
|
|
|
|
return _izijxzng.SignInChallengePurpose.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _i6v10ugm.WrappedKeys) {
|
|
|
|
|
return _i6v10ugm.WrappedKeys.fromJson(data) as T;
|
|
|
|
|
}
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
if (t == _ia5j8fch.Profile) {
|
|
|
|
|
return _ia5j8fch.Profile.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _iim3hfcf.ProfileNotFoundException) {
|
|
|
|
|
return _iim3hfcf.ProfileNotFoundException.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _i6j6efd1.ProfileRefusal) {
|
|
|
|
|
return _i6j6efd1.ProfileRefusal.fromJson(data) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _idvc5e7p.ProfileRefusedException) {
|
|
|
|
|
return _idvc5e7p.ProfileRefusedException.fromJson(data) as T;
|
|
|
|
|
}
|
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
|
|
|
if (t == _isc.getType<_iys46ui1.EmailCodeAccepted?>()) {
|
|
|
|
|
return (data != null ? _iys46ui1.EmailCodeAccepted.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_ieyvcag1.EmailSignInRefusal?>()) {
|
|
|
|
|
return (data != null ? _ieyvcag1.EmailSignInRefusal.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_iztfgag5.EmailSignInRefusedException?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _iztfgag5.EmailSignInRefusedException.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_ir1gd9p5.GitHubSignInRefusal?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _ir1gd9p5.GitHubSignInRefusal.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_i0xfcu5n.GitHubSignInRefusedException?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _i0xfcu5n.GitHubSignInRefusedException.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_ie6tbh85.SignInWays?>()) {
|
|
|
|
|
return (data != null ? _ie6tbh85.SignInWays.fromJson(data) : null) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_i8jixqx6.InstanceBasics?>()) {
|
|
|
|
|
return (data != null ? _i8jixqx6.InstanceBasics.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
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
|
|
|
if (t == _isc.getType<_i91ka1it.AccountResetKind?>()) {
|
|
|
|
|
return (data != null ? _i91ka1it.AccountResetKind.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_icpzuvvi.Authenticator?>()) {
|
|
|
|
|
return (data != null ? _icpzuvvi.Authenticator.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_iv7tra7v.AuthenticatorEnrolment?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _iv7tra7v.AuthenticatorEnrolment.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_iehtx95q.KeyProofChallenge?>()) {
|
|
|
|
|
return (data != null ? _iehtx95q.KeyProofChallenge.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_itvvd6m6.NewPassword?>()) {
|
|
|
|
|
return (data != null ? _itvvd6m6.NewPassword.fromJson(data) : null) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_iuxisac7.PasswordChallenge?>()) {
|
|
|
|
|
return (data != null ? _iuxisac7.PasswordChallenge.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_izp492e5.PasswordCosts?>()) {
|
|
|
|
|
return (data != null ? _izp492e5.PasswordCosts.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_ibfrwdhm.PasswordRefusal?>()) {
|
|
|
|
|
return (data != null ? _ibfrwdhm.PasswordRefusal.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_i4froq5y.PasswordRefusedException?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _i4froq5y.PasswordRefusedException.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_igpggy1y.PasswordSetup?>()) {
|
|
|
|
|
return (data != null ? _igpggy1y.PasswordSetup.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_ipk7gvzy.PasswordSignedIn?>()) {
|
|
|
|
|
return (data != null ? _ipk7gvzy.PasswordSignedIn.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_i3y6qzi3.ResetPending?>()) {
|
|
|
|
|
return (data != null ? _i3y6qzi3.ResetPending.fromJson(data) : null) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_izijxzng.SignInChallengePurpose?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _izijxzng.SignInChallengePurpose.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_i6v10ugm.WrappedKeys?>()) {
|
|
|
|
|
return (data != null ? _i6v10ugm.WrappedKeys.fromJson(data) : null) as T;
|
|
|
|
|
}
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
if (t == _isc.getType<_ia5j8fch.Profile?>()) {
|
|
|
|
|
return (data != null ? _ia5j8fch.Profile.fromJson(data) : null) as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_iim3hfcf.ProfileNotFoundException?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _iim3hfcf.ProfileNotFoundException.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_i6j6efd1.ProfileRefusal?>()) {
|
|
|
|
|
return (data != null ? _i6j6efd1.ProfileRefusal.fromJson(data) : null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<_idvc5e7p.ProfileRefusedException?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? _idvc5e7p.ProfileRefusedException.fromJson(data)
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
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
|
|
|
if (t == Set<String>) {
|
|
|
|
|
return (data as List).map((e) => deserialize<String>(e)).toSet() as T;
|
|
|
|
|
}
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
if (t == List<String>) {
|
|
|
|
|
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == _isc.getType<List<String>?>()) {
|
|
|
|
|
return (data != null
|
|
|
|
|
? (data as List).map((e) => deserialize<String>(e)).toList()
|
|
|
|
|
: null)
|
|
|
|
|
as T;
|
|
|
|
|
}
|
|
|
|
|
if (t == List<String>) {
|
|
|
|
|
return (data as List).map((e) => deserialize<String>(e)).toList() as T;
|
|
|
|
|
}
|
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
|
|
|
if (t == List<_ivfmflch.Authenticator>) {
|
|
|
|
|
return (data as List)
|
|
|
|
|
.map((e) => deserialize<_ivfmflch.Authenticator>(e))
|
|
|
|
|
.toList()
|
|
|
|
|
as T;
|
|
|
|
|
}
|
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
|
|
|
try {
|
|
|
|
|
return _iaic.Protocol().deserialize<T>(data, t);
|
|
|
|
|
} on _isc.DeserializationTypeNotFoundException catch (_) {}
|
|
|
|
|
try {
|
|
|
|
|
return _iacc.Protocol().deserialize<T>(data, t);
|
|
|
|
|
} on _isc.DeserializationTypeNotFoundException catch (_) {}
|
|
|
|
|
return super.deserialize<T>(data, t);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static String? getClassNameForType(Type type) {
|
|
|
|
|
return switch (type) {
|
|
|
|
|
_iys46ui1.EmailCodeAccepted => 'EmailCodeAccepted',
|
|
|
|
|
_ieyvcag1.EmailSignInRefusal => 'EmailSignInRefusal',
|
|
|
|
|
_iztfgag5.EmailSignInRefusedException => 'EmailSignInRefusedException',
|
|
|
|
|
_ir1gd9p5.GitHubSignInRefusal => 'GitHubSignInRefusal',
|
|
|
|
|
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
|
|
|
|
|
_ie6tbh85.SignInWays => 'SignInWays',
|
|
|
|
|
_i8jixqx6.InstanceBasics => 'InstanceBasics',
|
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
|
|
|
_i91ka1it.AccountResetKind => 'AccountResetKind',
|
|
|
|
|
_icpzuvvi.Authenticator => 'Authenticator',
|
|
|
|
|
_iv7tra7v.AuthenticatorEnrolment => 'AuthenticatorEnrolment',
|
|
|
|
|
_iehtx95q.KeyProofChallenge => 'KeyProofChallenge',
|
|
|
|
|
_itvvd6m6.NewPassword => 'NewPassword',
|
|
|
|
|
_iuxisac7.PasswordChallenge => 'PasswordChallenge',
|
|
|
|
|
_izp492e5.PasswordCosts => 'PasswordCosts',
|
|
|
|
|
_ibfrwdhm.PasswordRefusal => 'PasswordRefusal',
|
|
|
|
|
_i4froq5y.PasswordRefusedException => 'PasswordRefusedException',
|
|
|
|
|
_igpggy1y.PasswordSetup => 'PasswordSetup',
|
|
|
|
|
_ipk7gvzy.PasswordSignedIn => 'PasswordSignedIn',
|
|
|
|
|
_i3y6qzi3.ResetPending => 'ResetPending',
|
|
|
|
|
_izijxzng.SignInChallengePurpose => 'SignInChallengePurpose',
|
|
|
|
|
_i6v10ugm.WrappedKeys => 'WrappedKeys',
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
_ia5j8fch.Profile => 'Profile',
|
|
|
|
|
_iim3hfcf.ProfileNotFoundException => 'ProfileNotFoundException',
|
|
|
|
|
_i6j6efd1.ProfileRefusal => 'ProfileRefusal',
|
|
|
|
|
_idvc5e7p.ProfileRefusedException => 'ProfileRefusedException',
|
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
|
|
|
_ => null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String? getClassNameForObject(Object? data) {
|
|
|
|
|
String? className = super.getClassNameForObject(data);
|
|
|
|
|
if (className != null) return className;
|
|
|
|
|
|
|
|
|
|
if (data is Map<String, dynamic> && data['__className__'] is String) {
|
|
|
|
|
return (data['__className__'] as String).replaceFirst(
|
|
|
|
|
'halkyon_base.',
|
|
|
|
|
'',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (data) {
|
|
|
|
|
case _iys46ui1.EmailCodeAccepted():
|
|
|
|
|
return 'EmailCodeAccepted';
|
|
|
|
|
case _ieyvcag1.EmailSignInRefusal():
|
|
|
|
|
return 'EmailSignInRefusal';
|
|
|
|
|
case _iztfgag5.EmailSignInRefusedException():
|
|
|
|
|
return 'EmailSignInRefusedException';
|
|
|
|
|
case _ir1gd9p5.GitHubSignInRefusal():
|
|
|
|
|
return 'GitHubSignInRefusal';
|
|
|
|
|
case _i0xfcu5n.GitHubSignInRefusedException():
|
|
|
|
|
return 'GitHubSignInRefusedException';
|
|
|
|
|
case _ie6tbh85.SignInWays():
|
|
|
|
|
return 'SignInWays';
|
|
|
|
|
case _i8jixqx6.InstanceBasics():
|
|
|
|
|
return 'InstanceBasics';
|
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
|
|
|
case _i91ka1it.AccountResetKind():
|
|
|
|
|
return 'AccountResetKind';
|
|
|
|
|
case _icpzuvvi.Authenticator():
|
|
|
|
|
return 'Authenticator';
|
|
|
|
|
case _iv7tra7v.AuthenticatorEnrolment():
|
|
|
|
|
return 'AuthenticatorEnrolment';
|
|
|
|
|
case _iehtx95q.KeyProofChallenge():
|
|
|
|
|
return 'KeyProofChallenge';
|
|
|
|
|
case _itvvd6m6.NewPassword():
|
|
|
|
|
return 'NewPassword';
|
|
|
|
|
case _iuxisac7.PasswordChallenge():
|
|
|
|
|
return 'PasswordChallenge';
|
|
|
|
|
case _izp492e5.PasswordCosts():
|
|
|
|
|
return 'PasswordCosts';
|
|
|
|
|
case _ibfrwdhm.PasswordRefusal():
|
|
|
|
|
return 'PasswordRefusal';
|
|
|
|
|
case _i4froq5y.PasswordRefusedException():
|
|
|
|
|
return 'PasswordRefusedException';
|
|
|
|
|
case _igpggy1y.PasswordSetup():
|
|
|
|
|
return 'PasswordSetup';
|
|
|
|
|
case _ipk7gvzy.PasswordSignedIn():
|
|
|
|
|
return 'PasswordSignedIn';
|
|
|
|
|
case _i3y6qzi3.ResetPending():
|
|
|
|
|
return 'ResetPending';
|
|
|
|
|
case _izijxzng.SignInChallengePurpose():
|
|
|
|
|
return 'SignInChallengePurpose';
|
|
|
|
|
case _i6v10ugm.WrappedKeys():
|
|
|
|
|
return 'WrappedKeys';
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
case _ia5j8fch.Profile():
|
|
|
|
|
return 'Profile';
|
|
|
|
|
case _iim3hfcf.ProfileNotFoundException():
|
|
|
|
|
return 'ProfileNotFoundException';
|
|
|
|
|
case _i6j6efd1.ProfileRefusal():
|
|
|
|
|
return 'ProfileRefusal';
|
|
|
|
|
case _idvc5e7p.ProfileRefusedException():
|
|
|
|
|
return 'ProfileRefusedException';
|
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
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
dynamic deserializeByClassName(Map<String, dynamic> data) {
|
|
|
|
|
var dataClassName = data['className'];
|
|
|
|
|
if (dataClassName is! String) {
|
|
|
|
|
return super.deserializeByClassName(data);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'EmailCodeAccepted') {
|
|
|
|
|
return deserialize<_iys46ui1.EmailCodeAccepted>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'EmailSignInRefusal') {
|
|
|
|
|
return deserialize<_ieyvcag1.EmailSignInRefusal>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'EmailSignInRefusedException') {
|
|
|
|
|
return deserialize<_iztfgag5.EmailSignInRefusedException>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'GitHubSignInRefusal') {
|
|
|
|
|
return deserialize<_ir1gd9p5.GitHubSignInRefusal>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'GitHubSignInRefusedException') {
|
|
|
|
|
return deserialize<_i0xfcu5n.GitHubSignInRefusedException>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'SignInWays') {
|
|
|
|
|
return deserialize<_ie6tbh85.SignInWays>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'InstanceBasics') {
|
|
|
|
|
return deserialize<_i8jixqx6.InstanceBasics>(data['data']);
|
|
|
|
|
}
|
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
|
|
|
if (dataClassName == 'AccountResetKind') {
|
|
|
|
|
return deserialize<_i91ka1it.AccountResetKind>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'Authenticator') {
|
|
|
|
|
return deserialize<_icpzuvvi.Authenticator>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'AuthenticatorEnrolment') {
|
|
|
|
|
return deserialize<_iv7tra7v.AuthenticatorEnrolment>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'KeyProofChallenge') {
|
|
|
|
|
return deserialize<_iehtx95q.KeyProofChallenge>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'NewPassword') {
|
|
|
|
|
return deserialize<_itvvd6m6.NewPassword>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'PasswordChallenge') {
|
|
|
|
|
return deserialize<_iuxisac7.PasswordChallenge>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'PasswordCosts') {
|
|
|
|
|
return deserialize<_izp492e5.PasswordCosts>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'PasswordRefusal') {
|
|
|
|
|
return deserialize<_ibfrwdhm.PasswordRefusal>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'PasswordRefusedException') {
|
|
|
|
|
return deserialize<_i4froq5y.PasswordRefusedException>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'PasswordSetup') {
|
|
|
|
|
return deserialize<_igpggy1y.PasswordSetup>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'PasswordSignedIn') {
|
|
|
|
|
return deserialize<_ipk7gvzy.PasswordSignedIn>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'ResetPending') {
|
|
|
|
|
return deserialize<_i3y6qzi3.ResetPending>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'SignInChallengePurpose') {
|
|
|
|
|
return deserialize<_izijxzng.SignInChallengePurpose>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'WrappedKeys') {
|
|
|
|
|
return deserialize<_i6v10ugm.WrappedKeys>(data['data']);
|
|
|
|
|
}
|
refactor(profile): handles and profiles move into halkyon_base
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 22:20:27 +02:00
|
|
|
if (dataClassName == 'Profile') {
|
|
|
|
|
return deserialize<_ia5j8fch.Profile>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'ProfileNotFoundException') {
|
|
|
|
|
return deserialize<_iim3hfcf.ProfileNotFoundException>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'ProfileRefusal') {
|
|
|
|
|
return deserialize<_i6j6efd1.ProfileRefusal>(data['data']);
|
|
|
|
|
}
|
|
|
|
|
if (dataClassName == 'ProfileRefusedException') {
|
|
|
|
|
return deserialize<_idvc5e7p.ProfileRefusedException>(data['data']);
|
|
|
|
|
}
|
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
|
|
|
return super.deserializeByClassName(data);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Object? dynamicFieldToJson(
|
|
|
|
|
Object? object, {
|
|
|
|
|
bool forProtocol = false,
|
|
|
|
|
}) {
|
|
|
|
|
if ((object is List || object is Set || object is Map) ||
|
|
|
|
|
getClassNameForObject(object) != null) {
|
|
|
|
|
return super.dynamicFieldToJson(object, forProtocol: forProtocol);
|
|
|
|
|
}
|
|
|
|
|
for (final protocol in _hostProtocols) {
|
|
|
|
|
final className = protocol.getClassNameForObject(object);
|
|
|
|
|
if (className == null) continue;
|
|
|
|
|
final host = protocol.getModuleName();
|
|
|
|
|
final wrapped = {
|
|
|
|
|
'className': className.contains('.') ? className : '$host.$className',
|
|
|
|
|
'data': object,
|
|
|
|
|
};
|
|
|
|
|
return forProtocol
|
|
|
|
|
? _isc.SerializationManager.toEncodableForProtocol(wrapped)
|
|
|
|
|
: _isc.SerializationManager.toEncodable(wrapped);
|
|
|
|
|
}
|
|
|
|
|
return super.dynamicFieldToJson(object, forProtocol: forProtocol);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
dynamic deserializeDynamicFieldValue(Object? value) {
|
|
|
|
|
if (value == null) return null;
|
|
|
|
|
if (value is! Map<String, dynamic> || value['className'] is! String) {
|
|
|
|
|
throw FormatException(
|
|
|
|
|
'Dynamic fields are encoded as a Map with className and data, but got '
|
|
|
|
|
'${value.runtimeType} instead.',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
final className = value['className'] as String;
|
|
|
|
|
for (final protocol in _hostProtocols) {
|
|
|
|
|
final host = protocol.getModuleName();
|
|
|
|
|
final hostPrefix = '$host.';
|
|
|
|
|
if (className.startsWith(hostPrefix)) {
|
|
|
|
|
final strippedClassName = className.substring(hostPrefix.length);
|
|
|
|
|
if (strippedClassName.contains('.')) {
|
|
|
|
|
throw FormatException(
|
|
|
|
|
'Dynamic field className must not use multiple prefixes: $className',
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
final hostData = Map<String, dynamic>.from(value);
|
|
|
|
|
hostData['className'] = strippedClassName;
|
|
|
|
|
return protocol.deserializeByClassName(hostData);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (className.contains('.')) {
|
|
|
|
|
for (final protocol in _hostProtocols) {
|
|
|
|
|
try {
|
|
|
|
|
return protocol.deserializeByClassName(value);
|
|
|
|
|
} on _isc.DeserializationClassNameNotFoundException catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return deserializeByClassName(value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
String getModuleName() => 'halkyon_base';
|
|
|
|
|
|
|
|
|
|
/// Maps any `Record`s known to this [Protocol] to their JSON representation
|
|
|
|
|
///
|
|
|
|
|
/// Throws in case the record type is not known.
|
|
|
|
|
///
|
|
|
|
|
/// This method will return `null` (only) for `null` inputs.
|
|
|
|
|
Map<String, dynamic>? mapRecordToJson(Record? record) {
|
|
|
|
|
if (record == null) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
return _iaic.Protocol().mapRecordToJson(record);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
try {
|
|
|
|
|
return _iacc.Protocol().mapRecordToJson(record);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
throw Exception('Unsupported record type ${record.runtimeType}');
|
|
|
|
|
}
|
|
|
|
|
}
|