refactor(auth)!: sign-in and the account come from halkyon_base

Sign-in (anonymous, GitHub, email codes), an account's ways in and its
deletion, the pruning and the instance's basics move into the
halkyon_base module (ADR 0014), which every Halkyon server includes.
Learn configures it in server.dart and deletes its own data with a
person through the module's hook, in the order it did. The identity
tables move unchanged: the migration records versions only. The app
calls the module's endpoints; instance.info() stays as it was.

BREAKING CHANGE: the sign-in endpoints moved into the module, so apps up
to build 20 can no longer sign in or start; minimumAppBuild is 21.
Deploy this only together with the release of build 21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 14:39:00 +02:00
commit a463f24444
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
101 changed files with 28050 additions and 6268 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,86 @@
# ADR 0014: What every Halkyon server has, as a Serverpod module of its own
- **Status:** accepted
- **Date:** 2026-10-19
## Context
Halkyon is to have other services beside Learn (a to-do app, a training app), each a
Flutter app with a Serverpod server of its own, run by whoever runs an instance. Their
look already comes from `halkyon_ui` in halkyon_shared. What each of their servers needs
besides its own features is what Learn's has built since September: signing in
anonymously, with GitHub and with a code by mail (ADR 0010), the account's ways in and its
deletion in ADR 0001 rule 6's order, the pruning of what nobody can return to, and what an
instance tells its apps before anyone signs in (its sign-in ways, its source, the oldest
app build it works with, where it keeps people's data).
The app's side of signing in (its gateway, sessions, the start screen) can only be shared
once these endpoints are shared: an app's generated client calls its own server's
endpoints, under their names. The developer chose to share the server side first (19 Oct),
rather than letting each app's server copy Learn's.
Serverpod's way of sharing server code with endpoints, models and tables is a module: a
server package and a generated client package, which a host project names in its
`generator.yaml` and reaches as `client.modules.<nickname>`. Learn already runs
Serverpod's own modules (`serverpod_auth_idp`, `serverpod_auth_core`) and the sync
engine's.
## Decision
1. **`halkyon_base`, a Serverpod module in halkyon_shared.** `halkyon_base_server` and
`halkyon_base_client`, nickname `base`, pinned by git to a signed commit of
halkyon_shared's main as Learn pins the sync fork. It holds the anonymous, GitHub and
email sign-ins with their endpoints, models and tables (`email_identity`,
`email_code_request`), the scopes and `grantIdentified`, the code mails, the account's
endpoint (`delete`, `signInWays`), `deletePerson`, the pruning functions, and the
instance's basics (`instance.basics()`, `InstanceBasics`).
2. **A host configures it in one place, before its pod starts.**
`HalkyonBase.configure(pod, HalkyonBase(...))`: the app's name, which sign-in mails
name; where the project publishes its source, which the instance names unless its
operator sets `SOURCE_URL`; the oldest app build the server works with; and what the
host deletes of a person who goes. Learn's is `learnBase()`
(`lib/src/auth/learn_base.dart`), set up in `server.dart`.
3. **A person's own data is the host's, deleted through a hook.** `deletePerson` ends the
person's sessions first, then runs the host's deletions inside its transaction, then
deletes their identities and the person; what must wait until the deletion holds
(Learn's release repositories) runs after the commit. Learn's hook,
`deleteLearnData`, deletes their space and its files, profile, discussions, published
studysets, blocks, what they read, assistant connections and file allowance, as
`deletePerson` did before. The pruning goes through the same deletion. Each host
schedules the pruning in its own future call.
4. **The tables move as they are.** Serverpod (checked with v4.0.4, in a throwaway
project and module, then on Learn) matches tables by name alone when it compares a
migration's definitions, and ignores which module owns them. So the moved models keep
their class, table, column and index names, and Learn's migration
(`20261019121406578-halkyon-base`, made with `create-migration --empty`) holds no SQL
but the rows recording each module's version; its definition gives the two tables to
`halkyon_base`. The rows stay where they are, and the password keys keep their names
(`emailSignInPepper` among them), so every address's hash stays valid. The device's
local migration creates the two tables, empty, as it holds every module's tables
(Serverpod's own auth tables among them); nothing on a device reads them.
5. **Released apps are told to update, rather than served by wrappers.** A module's
endpoints are called under its nickname, and its models go over the wire under its
name (`halkyon_base.EmailSignInRefusedException`), so Learn's released apps (up to
v0.17.0, build 20) call endpoints that are gone and can't read the refusals. Learn
keeps no wrappers: `minimumAppBuild` is 21, the first build on the module, and
Learn's own `instance.info()` stays with every field it had, as released apps read
`minimumAppBuild` there. Its other Learn fields (the assistant connector, Jellyfin)
stay Learn's; the fields it shares with `InstanceBasics` are filled from the module.
6. **Licences as Learn's.** The server package is under the SSPL, as Learn's server; the
client package under the AGPL, as Learn's client and app; each combines with its
counterpart in every app as Learn's own packages do.
## Consequences
- **The server with the module deploys only together with the release of build 21.**
Until people update, released apps can't sign in, switch accounts or start afresh, and
say the server needs a newer app. Not before 22 Oct 2026 either: the hackathon's judges
use v0.17.0 until then.
- Another Halkyon server gets all of this by naming the module and configuring it, and its
app can share the app side of signing in (`halkyon_app`, next) on the module's client.
- A change to sign-in, deletion or the instance's basics is made in halkyon_shared and
reaches Learn by moving the pin; Learn's tests of its own deletion and its
`instance.info()` set the module up as the server does (`useLearnBase`).
- Serverpod's version moves in the module too, together with every other package.
- A table holding a person's data in a host gets a line in that host's deletion hook;
the module can't know it.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,186 @@
{
"__className__": "serverpod.DatabaseMigration",
"actions": [
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "email_code_request",
"dartName": "EmailCodeRequest",
"module": "halkyon_base",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 7,
"isNullable": false,
"columnDefault": "random_v7",
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "addressHash",
"fieldName": "addressHash",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "challengeId",
"fieldName": "challengeId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "switchChallengeId",
"fieldName": "switchChallengeId",
"columnType": 7,
"isNullable": true,
"dartType": "UuidValue?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "createdAt",
"fieldName": "createdAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
}
],
"foreignKeys": [
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "email_code_request_fk_0",
"columns": [
"challengeId"
],
"referenceTable": "serverpod_auth_idp_secret_challenge",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
},
{
"__className__": "serverpod.ForeignKeyDefinition",
"constraintName": "email_code_request_fk_1",
"columns": [
"switchChallengeId"
],
"referenceTable": "serverpod_auth_idp_secret_challenge",
"referenceTableSchema": "public",
"referenceColumns": [
"id"
],
"onUpdate": 3,
"onDelete": 4
}
],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "email_code_request_address_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "addressHash"
}
],
"type": "btree",
"isUnique": false,
"isPrimary": false
}
],
"managed": true
}
},
{
"__className__": "serverpod.DatabaseMigrationAction",
"type": "createTable",
"createTable": {
"__className__": "serverpod.TableDefinition",
"name": "email_identity",
"dartName": "EmailIdentity",
"module": "halkyon_base",
"schema": "public",
"columns": [
{
"__className__": "serverpod.ColumnDefinition",
"name": "id",
"fieldName": "id",
"columnType": 6,
"isNullable": false,
"columnDefault": "serial",
"dartType": "int?"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "authUserId",
"fieldName": "authUserId",
"columnType": 7,
"isNullable": false,
"dartType": "UuidValue"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "addressHash",
"fieldName": "addressHash",
"columnType": 0,
"isNullable": false,
"dartType": "String"
},
{
"__className__": "serverpod.ColumnDefinition",
"name": "createdAt",
"fieldName": "createdAt",
"columnType": 4,
"isNullable": false,
"dartType": "DateTime"
}
],
"foreignKeys": [],
"indexes": [
{
"__className__": "serverpod.IndexDefinition",
"indexName": "email_identity_person_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "authUserId"
}
],
"type": "btree",
"isUnique": true,
"isPrimary": false
},
{
"__className__": "serverpod.IndexDefinition",
"indexName": "email_identity_address_idx",
"elements": [
{
"__className__": "serverpod.IndexElementDefinition",
"type": 0,
"definition": "addressHash"
}
],
"type": "btree",
"isUnique": true,
"isPrimary": false
}
],
"managed": true
}
}
],
"warnings": [],
"migrationApiVersion": 1
}

View file

@ -15,6 +15,7 @@ part '20261007205440868-card-buried-at/migration.dart';
part '20261008165051407-pdf-position/migration.dart';
part '20261009224606274-feedback/migration.dart';
part '20261012162411835-studyset-deleted-at/migration.dart';
part '20261019121406578-halkyon-base/migration.dart';
/// Migration registry for the client-side database.
class MigrationRegistry {
@ -32,6 +33,7 @@ class MigrationRegistry {
_Migration20261008165051407_pdf_position(),
_Migration20261009224606274_feedback(),
_Migration20261012162411835_studyset_deleted_at(),
_Migration20261019121406578_halkyon_base(),
];
/// List of all client-side database migration versions.

View file

@ -1,75 +0,0 @@
/* 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: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// A code accepted: the address is now the signed-in person's (or already
/// was), or it is another person's and [switchToken] signs in as them.
abstract class EmailCodeAccepted
implements _isc.SerializableModel, _isc.ProtocolSerialization {
EmailCodeAccepted._({this.switchToken});
factory EmailCodeAccepted({String? switchToken}) = _EmailCodeAcceptedImpl;
factory EmailCodeAccepted.fromJson(Map<String, dynamic> jsonSerialization) {
return EmailCodeAccepted(
switchToken: jsonSerialization['switchToken'] as String?,
);
}
/// Null when the address is the signed-in person's; else what signs in as
/// the person who has it, once, within the code's 15 minutes
/// (`EmailSignInEndpoint.signIn`).
String? switchToken;
/// Returns a shallow copy of this [EmailCodeAccepted]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
EmailCodeAccepted copyWith({String? switchToken});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'EmailCodeAccepted',
if (switchToken != null) 'switchToken': switchToken,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'EmailCodeAccepted',
if (switchToken != null) 'switchToken': switchToken,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _Undefined {}
class _EmailCodeAcceptedImpl extends EmailCodeAccepted {
_EmailCodeAcceptedImpl({String? switchToken})
: super._(switchToken: switchToken);
/// Returns a shallow copy of this [EmailCodeAccepted]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
EmailCodeAccepted copyWith({Object? switchToken = _Undefined}) {
return EmailCodeAccepted(
switchToken: switchToken is String? ? switchToken : this.switchToken,
);
}
}

View file

@ -1,75 +0,0 @@
/* 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: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// Why mailing a code, accepting it, or signing in with an email address
/// was refused.
enum EmailSignInRefusal implements _isc.SerializableModel {
/// This instance has no email sign-in (no emailSignInPepper in its
/// passwords, or no way to send mail).
unavailable,
/// What was typed isn't an email address.
notAnAddress,
/// Too many codes asked for, for this address or from this network, in
/// the last hour.
tooManyCodes,
/// The code is wrong.
wrongCode,
/// The code was typed wrong too often; a new one is needed.
tooManyTries,
/// The code is no longer valid: past its time, used, or replaced by a
/// newer one.
codeExpired,
/// The person already signs in with another address.
otherAddressLinked,
/// The mail couldn't be sent; nothing was kept of the request.
mailFailed;
static EmailSignInRefusal fromJson(String name) {
switch (name) {
case 'unavailable':
return EmailSignInRefusal.unavailable;
case 'notAnAddress':
return EmailSignInRefusal.notAnAddress;
case 'tooManyCodes':
return EmailSignInRefusal.tooManyCodes;
case 'wrongCode':
return EmailSignInRefusal.wrongCode;
case 'tooManyTries':
return EmailSignInRefusal.tooManyTries;
case 'codeExpired':
return EmailSignInRefusal.codeExpired;
case 'otherAddressLinked':
return EmailSignInRefusal.otherAddressLinked;
case 'mailFailed':
return EmailSignInRefusal.mailFailed;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "EmailSignInRefusal"',
);
}
}
@override
String toJson() => name;
@override
String toString() => name;
}

View file

@ -1,80 +0,0 @@
/* 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: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../auth/email_sign_in_refusal.dart' as _in9x16lz;
/// Mailing a code, accepting it, or signing in with an email address was
/// refused; nothing changed.
abstract class EmailSignInRefusedException
implements
_isc.SerializableException,
_isc.SerializableModel,
_isc.ProtocolSerialization {
EmailSignInRefusedException._({required this.reason});
factory EmailSignInRefusedException({
required _in9x16lz.EmailSignInRefusal reason,
}) = _EmailSignInRefusedExceptionImpl;
factory EmailSignInRefusedException.fromJson(
Map<String, dynamic> jsonSerialization,
) {
return EmailSignInRefusedException(
reason: _in9x16lz.EmailSignInRefusal.fromJson(
(jsonSerialization['reason'] as String),
),
);
}
/// Why it was refused.
_in9x16lz.EmailSignInRefusal reason;
/// Returns a shallow copy of this [EmailSignInRefusedException]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
EmailSignInRefusedException copyWith({_in9x16lz.EmailSignInRefusal? reason});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'EmailSignInRefusedException',
'reason': reason.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'EmailSignInRefusedException',
'reason': reason.toJson(),
};
}
@override
String toString() {
return 'EmailSignInRefusedException(reason: $reason)';
}
}
class _EmailSignInRefusedExceptionImpl extends EmailSignInRefusedException {
_EmailSignInRefusedExceptionImpl({
required _in9x16lz.EmailSignInRefusal reason,
}) : super._(reason: reason);
/// Returns a shallow copy of this [EmailSignInRefusedException]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
EmailSignInRefusedException copyWith({_in9x16lz.EmailSignInRefusal? reason}) {
return EmailSignInRefusedException(reason: reason ?? this.reason);
}
}

View file

@ -1,52 +0,0 @@
/* 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: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// Why signing in with GitHub, or linking a GitHub account, was refused.
enum GitHubSignInRefusal implements _isc.SerializableModel {
/// This instance has no GitHub OAuth app (no githubClientId and
/// githubClientSecret in its passwords).
unavailable,
/// The GitHub account already belongs to another person.
takenByAnotherPerson,
/// The person already has a different GitHub account.
otherGitHubAccountLinked,
/// No person has this GitHub account; only linking one creates it.
unknownAccount;
static GitHubSignInRefusal fromJson(String name) {
switch (name) {
case 'unavailable':
return GitHubSignInRefusal.unavailable;
case 'takenByAnotherPerson':
return GitHubSignInRefusal.takenByAnotherPerson;
case 'otherGitHubAccountLinked':
return GitHubSignInRefusal.otherGitHubAccountLinked;
case 'unknownAccount':
return GitHubSignInRefusal.unknownAccount;
default:
throw ArgumentError(
'Value "$name" cannot be converted to "GitHubSignInRefusal"',
);
}
}
@override
String toJson() => name;
@override
String toString() => name;
}

View file

@ -1,84 +0,0 @@
/* 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: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
import '../auth/github_sign_in_refusal.dart' as _irgbwy1y;
/// Signing in with GitHub, or linking a GitHub account, was refused; nothing
/// changed.
abstract class GitHubSignInRefusedException
implements
_isc.SerializableException,
_isc.SerializableModel,
_isc.ProtocolSerialization {
GitHubSignInRefusedException._({required this.reason});
factory GitHubSignInRefusedException({
required _irgbwy1y.GitHubSignInRefusal reason,
}) = _GitHubSignInRefusedExceptionImpl;
factory GitHubSignInRefusedException.fromJson(
Map<String, dynamic> jsonSerialization,
) {
return GitHubSignInRefusedException(
reason: _irgbwy1y.GitHubSignInRefusal.fromJson(
(jsonSerialization['reason'] as String),
),
);
}
/// Why it was refused.
_irgbwy1y.GitHubSignInRefusal reason;
/// Returns a shallow copy of this [GitHubSignInRefusedException]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
GitHubSignInRefusedException copyWith({
_irgbwy1y.GitHubSignInRefusal? reason,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'GitHubSignInRefusedException',
'reason': reason.toJson(),
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'GitHubSignInRefusedException',
'reason': reason.toJson(),
};
}
@override
String toString() {
return 'GitHubSignInRefusedException(reason: $reason)';
}
}
class _GitHubSignInRefusedExceptionImpl extends GitHubSignInRefusedException {
_GitHubSignInRefusedExceptionImpl({
required _irgbwy1y.GitHubSignInRefusal reason,
}) : super._(reason: reason);
/// Returns a shallow copy of this [GitHubSignInRefusedException]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
GitHubSignInRefusedException copyWith({
_irgbwy1y.GitHubSignInRefusal? reason,
}) {
return GitHubSignInRefusedException(reason: reason ?? this.reason);
}
}

View file

@ -1,92 +0,0 @@
/* 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: no_leading_underscores_for_library_prefixes
import 'package:serverpod_client/serverpod_client.dart' as _isc;
/// The ways a person signs in, besides an anonymous account's session.
abstract class SignInWays
implements _isc.SerializableModel, _isc.ProtocolSerialization {
SignInWays._({
required this.gitHub,
required this.email,
});
factory SignInWays({
required bool gitHub,
required bool email,
}) = _SignInWaysImpl;
factory SignInWays.fromJson(Map<String, dynamic> jsonSerialization) {
return SignInWays(
gitHub: _isc.BoolJsonExtension.fromJson(jsonSerialization['gitHub']),
email: _isc.BoolJsonExtension.fromJson(jsonSerialization['email']),
);
}
bool gitHub;
bool email;
/// Returns a shallow copy of this [SignInWays]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
SignInWays copyWith({
bool? gitHub,
bool? email,
});
@override
Map<String, dynamic> toJson() {
return {
'__className__': 'SignInWays',
'gitHub': gitHub,
'email': email,
};
}
@override
Map<String, dynamic> toJsonForProtocol() {
return {
'__className__': 'SignInWays',
'gitHub': gitHub,
'email': email,
};
}
@override
String toString() {
return _isc.SerializationManager.encode(this);
}
}
class _SignInWaysImpl extends SignInWays {
_SignInWaysImpl({
required bool gitHub,
required bool email,
}) : super._(
gitHub: gitHub,
email: email,
);
/// Returns a shallow copy of this [SignInWays]
/// with some or all fields replaced by the given arguments.
@_isc.useResult
@override
SignInWays copyWith({
bool? gitHub,
bool? email,
}) {
return SignInWays(
gitHub: gitHub ?? this.gitHub,
email: email ?? this.email,
);
}
}

View file

@ -12,6 +12,7 @@
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _ida;
import 'dart:typed_data' as _idt;
import 'package:halkyon_base_client/halkyon_base_client.dart' as _ihk4p5t7;
import 'package:halkyon_learn_client/src/protocol/asset/file_usage.dart'
as _iimptq0q;
import 'package:halkyon_learn_client/src/protocol/assistant/assistant_code.dart'
@ -20,10 +21,6 @@ import 'package:halkyon_learn_client/src/protocol/assistant/assistant_connection
as _iplrdti2;
import 'package:halkyon_learn_client/src/protocol/auth/device.dart'
as _ieqh4yr9;
import 'package:halkyon_learn_client/src/protocol/auth/email_code_accepted.dart'
as _ivlnmdt4;
import 'package:halkyon_learn_client/src/protocol/auth/sign_in_ways.dart'
as _ixy30i05;
import 'package:halkyon_learn_client/src/protocol/dictionary/device_dictionary_file.dart'
as _ijp9kwtx;
import 'package:halkyon_learn_client/src/protocol/dictionary/dictionary_hit.dart'
@ -170,56 +167,6 @@ class EndpointAssistant extends _isc.EndpointRef {
);
}
/// The signed-in person's account.
/// {@category Endpoint}
class EndpointAccount extends _isc.EndpointRef {
EndpointAccount(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'account';
/// Deletes the caller with everything they have (see [deletePerson]). The
/// app also calls it when an anonymous account is given up (decided
/// 2026-09-30). Their other devices learn at once that their sessions
/// ended.
_ida.Future<void> delete() => caller.callServerEndpoint<void>(
'account',
'delete',
{},
);
/// How the caller signs in besides their sessions: with GitHub, with an
/// email address, both, or neither (anonymous).
_ida.Future<_ixy30i05.SignInWays> signInWays() =>
caller.callServerEndpoint<_ixy30i05.SignInWays>(
'account',
'signInWays',
{},
);
}
/// Signs people in anonymously; the app calls it on first launch. Creating an
/// account is rate-limited per IP address (see `auth_config.dart`).
/// {@category Endpoint}
class EndpointAnonymousIdp extends _iaic.EndpointAnonymousIdpBase {
EndpointAnonymousIdp(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'anonymousIdp';
/// Creates a new anonymous account and returns its session.
///
/// Invokes the [AnonymousIdp.beforeAnonymousAccount] callback if configured,
/// which may prevent account creation if the endpoint is protected.
@override
_ida.Future<_iacc.AuthSuccess> login({String? token}) =>
caller.callServerEndpoint<_iacc.AuthSuccess>(
'anonymousIdp',
'login',
{'token': token},
);
}
/// Your devices: the sessions you're signed in with, and signing one of them
/// out, for example a lost phone.
///
@ -252,121 +199,6 @@ class EndpointDevices extends _isc.EndpointRef {
);
}
/// Signing in with an email address, by a code mailed to it (ADR 0010):
/// the way to keep an anonymous account, as linking GitHub is, and a second
/// way in for someone who signs in with GitHub. Signed in only, as every
/// device is from its first start.
/// {@category Endpoint}
class EndpointEmailCode extends _isc.EndpointRef {
EndpointEmailCode(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'emailCode';
/// Mails a code to [address], worded in [language] (`en`, `de`), and
/// returns the request it answers; refusals as in [sendSignInCode].
_ida.Future<_isc.UuidValue> sendCode({
required String address,
required String language,
}) => caller.callServerEndpoint<_isc.UuidValue>(
'emailCode',
'sendCode',
{
'address': address,
'language': language,
},
);
/// Accepts [code] for [request]: the address becomes the caller's unless
/// it is another person's, whose token then signs in as them
/// (`EmailSignInEndpoint.signIn`); refusals as in [acceptSignInCode].
_ida.Future<_ivlnmdt4.EmailCodeAccepted> verifyCode({
required _isc.UuidValue request,
required String code,
}) => caller.callServerEndpoint<_ivlnmdt4.EmailCodeAccepted>(
'emailCode',
'verifyCode',
{
'request': request,
'code': code,
},
);
}
/// Signs in the person whose address a mailed code proved, when the device
/// switches to them. Called without a session: Serverpod issues no session
/// for another person inside a signed-in call.
/// {@category Endpoint}
class EndpointEmailSignIn extends _isc.EndpointRef {
EndpointEmailSignIn(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'emailSignIn';
/// A session for the person [switchToken] names; refusals as in
/// [signInWithEmail].
_ida.Future<_iacc.AuthSuccess> signIn({required String switchToken}) =>
caller.callServerEndpoint<_iacc.AuthSuccess>(
'emailSignIn',
'signIn',
{'switchToken': switchToken},
);
}
/// Signs in the person who already has a GitHub account, e.g. when they switch
/// to it on another device. Called without a session: Serverpod issues no
/// session for another person inside a signed-in call.
/// {@category Endpoint}
class EndpointGitHubSignIn extends _isc.EndpointRef {
EndpointGitHubSignIn(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'gitHubSignIn';
/// A session for the person with the GitHub account that [code] proves;
/// refusals as in [signInWithGitHubAccount].
_ida.Future<_iacc.AuthSuccess> signIn({
required String code,
required String codeVerifier,
required String redirectUri,
}) => caller.callServerEndpoint<_iacc.AuthSuccess>(
'gitHubSignIn',
'signIn',
{
'code': code,
'codeVerifier': codeVerifier,
'redirectUri': redirectUri,
},
);
}
/// Links a GitHub account to the signed-in person: the lossless upgrade from
/// an anonymous account. The person, their spaces and their studysets stay
/// where they are; only the way they sign in changes.
/// {@category Endpoint}
class EndpointGitHubUpgrade extends _isc.EndpointRef {
EndpointGitHubUpgrade(_isc.EndpointCaller caller) : super(caller);
@override
String get name => 'gitHubUpgrade';
/// Links the GitHub account that [code] proves to the caller; refusals as in
/// [linkGitHubAccount].
_ida.Future<void> upgrade({
required String code,
required String codeVerifier,
required String redirectUri,
}) => caller.callServerEndpoint<void>(
'gitHubUpgrade',
'upgrade',
{
'code': code,
'codeVerifier': codeVerifier,
'redirectUri': redirectUri,
},
);
}
/// The dictionaries the instance holds, to look words up in and to take
/// words from by a label (all JLPT N5 words). A dictionary is one source
/// in one meaning language (`jmdict` in `en`). Reference data: any
@ -918,6 +750,8 @@ class EndpointInbox extends _isc.EndpointRef {
}
/// What this instance offers. Public: the app asks before anyone is signed in.
/// Released apps read the oldest build the server works with here, so it
/// keeps every field it had: halkyon_base's own basics fill those it shares.
/// {@category Endpoint}
class EndpointInstance extends _isc.EndpointRef {
EndpointInstance(_isc.EndpointCaller caller) : super(caller);
@ -1128,11 +962,14 @@ class EndpointPublish extends _isc.EndpointRef {
class Modules {
Modules(Client client) {
base = _ihk4p5t7.Caller(client);
serverpod_auth_idp = _iaic.Caller(client);
offline_sync = _ipulbpi2.Caller(client);
serverpod_auth_core = _iacc.Caller(client);
}
late final _ihk4p5t7.Caller base;
late final _iaic.Caller serverpod_auth_idp;
late final _ipulbpi2.Caller offline_sync;
@ -1169,13 +1006,7 @@ class Client extends _isc.ServerpodClientShared {
) {
asset = EndpointAsset(this);
assistant = EndpointAssistant(this);
account = EndpointAccount(this);
anonymousIdp = EndpointAnonymousIdp(this);
devices = EndpointDevices(this);
emailCode = EndpointEmailCode(this);
emailSignIn = EndpointEmailSignIn(this);
gitHubSignIn = EndpointGitHubSignIn(this);
gitHubUpgrade = EndpointGitHubUpgrade(this);
dictionary = EndpointDictionary(this);
discussions = EndpointDiscussions(this);
discussion = EndpointDiscussion(this);
@ -1193,20 +1024,8 @@ class Client extends _isc.ServerpodClientShared {
late final EndpointAssistant assistant;
late final EndpointAccount account;
late final EndpointAnonymousIdp anonymousIdp;
late final EndpointDevices devices;
late final EndpointEmailCode emailCode;
late final EndpointEmailSignIn emailSignIn;
late final EndpointGitHubSignIn gitHubSignIn;
late final EndpointGitHubUpgrade gitHubUpgrade;
late final EndpointDictionary dictionary;
late final EndpointDiscussions discussions;
@ -1233,13 +1052,7 @@ class Client extends _isc.ServerpodClientShared {
Map<String, _isc.EndpointRef> get endpointRefLookup => {
'asset': asset,
'assistant': assistant,
'account': account,
'anonymousIdp': anonymousIdp,
'devices': devices,
'emailCode': emailCode,
'emailSignIn': emailSignIn,
'gitHubSignIn': gitHubSignIn,
'gitHubUpgrade': gitHubUpgrade,
'dictionary': dictionary,
'discussions': discussions,
'discussion': discussion,
@ -1254,6 +1067,7 @@ class Client extends _isc.ServerpodClientShared {
@override
Map<String, _isc.ModuleEndpointCaller> get moduleLookup => {
'base': modules.base,
'serverpod_auth_idp': modules.serverpod_auth_idp,
'offline_sync': modules.offline_sync,
'serverpod_auth_core': modules.serverpod_auth_core,

View file

@ -11,6 +11,7 @@
// ignore_for_file: dead_code, unnecessary_type_check
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:halkyon_base_client/halkyon_base_client.dart' as _ihk4p5t7;
import 'package:halkyon_learn_client/src/protocol/assistant/assistant_connection_info.dart'
as _iplrdti2;
import 'package:halkyon_learn_client/src/protocol/auth/device.dart'
@ -44,12 +45,6 @@ import 'asset/file_usage.dart' as _iurwo9k8;
import 'assistant/assistant_code.dart' as _i8xi9jqo;
import 'assistant/assistant_connection_info.dart' as _icukzjbz;
import 'auth/device.dart' as _i6c5ny7n;
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 'card/card.dart' as _ipiw1cmx;
import 'card/card_revision.dart' as _ipbyuef1;
import 'common/not_found_exception.dart' as _i8oee73b;
@ -108,12 +103,6 @@ export 'asset/file_usage.dart';
export 'assistant/assistant_code.dart';
export 'assistant/assistant_connection_info.dart';
export 'auth/device.dart';
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 'card/card.dart';
export 'card/card_revision.dart';
export 'common/not_found_exception.dart';
@ -2272,6 +2261,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
],
managed: true,
),
..._ihk4p5t7.Protocol() is _isd.DatabaseSerializationManager
? (_ihk4p5t7.Protocol() as _isd.DatabaseSerializationManager)
.getTargetTableDefinitions()
: [],
..._iaic.Protocol() is _isd.DatabaseSerializationManager
? (_iaic.Protocol() as _isd.DatabaseSerializationManager)
.getTargetTableDefinitions()
@ -2334,24 +2327,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _i6c5ny7n.Device) {
return _i6c5ny7n.Device.fromJson(data) as 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 == _ipiw1cmx.Card) {
return _ipiw1cmx.Card.fromJson(data) as T;
}
@ -2534,35 +2509,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (t == _isc.getType<_i6c5ny7n.Device?>()) {
return (data != null ? _i6c5ny7n.Device.fromJson(data) : null) as T;
}
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<_ipiw1cmx.Card?>()) {
return (data != null ? _ipiw1cmx.Card.fromJson(data) : null) as T;
}
@ -2870,6 +2816,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
.toList()
as T;
}
try {
return _ihk4p5t7.Protocol().deserialize<T>(data, t);
} on _isc.DeserializationTypeNotFoundException catch (_) {}
try {
return _iaic.Protocol().deserialize<T>(data, t);
} on _isc.DeserializationTypeNotFoundException catch (_) {}
@ -2891,12 +2840,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
_i8xi9jqo.AssistantCode => 'AssistantCode',
_icukzjbz.AssistantConnectionInfo => 'AssistantConnectionInfo',
_i6c5ny7n.Device => 'Device',
_iys46ui1.EmailCodeAccepted => 'EmailCodeAccepted',
_ieyvcag1.EmailSignInRefusal => 'EmailSignInRefusal',
_iztfgag5.EmailSignInRefusedException => 'EmailSignInRefusedException',
_ir1gd9p5.GitHubSignInRefusal => 'GitHubSignInRefusal',
_i0xfcu5n.GitHubSignInRefusedException => 'GitHubSignInRefusedException',
_ie6tbh85.SignInWays => 'SignInWays',
_ipiw1cmx.Card => 'Card',
_ipbyuef1.CardRevision => 'CardRevision',
_i8oee73b.NotFoundException => 'NotFoundException',
@ -2979,18 +2922,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
return 'AssistantConnectionInfo';
case _i6c5ny7n.Device():
return 'Device';
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 _ipiw1cmx.Card():
return 'Card';
case _ipbyuef1.CardRevision():
@ -3094,6 +3025,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
case _ie8t3ar4.Tag():
return 'Tag';
}
className = _ihk4p5t7.Protocol().getClassNameForObject(data);
if (className != null) {
return className.contains('.') ? className : 'halkyon_base.$className';
}
className = _iaic.Protocol().getClassNameForObject(data);
if (className != null) {
return className.contains('.')
@ -3142,24 +3077,6 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'Device') {
return deserialize<_i6c5ny7n.Device>(data['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 == 'Card') {
return deserialize<_ipiw1cmx.Card>(data['data']);
}
@ -3313,6 +3230,10 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (dataClassName == 'Tag') {
return deserialize<_ie8t3ar4.Tag>(data['data']);
}
if (dataClassName.startsWith('halkyon_base.')) {
data['className'] = dataClassName.substring(13);
return _ihk4p5t7.Protocol().deserializeByClassName(data);
}
if (dataClassName.startsWith('serverpod_auth_idp.')) {
data['className'] = dataClassName.substring(19);
return _iaic.Protocol().deserializeByClassName(data);
@ -3329,6 +3250,7 @@ class Protocol extends _isd.DatabaseSerializationManager {
}
void _registerHostProtocols() {
_ihk4p5t7.Protocol().registerHostProtocol('halkyon_learn', this);
_iaic.Protocol().registerHostProtocol('halkyon_learn', this);
_ipulbpi2.Protocol().registerHostProtocol('halkyon_learn', this);
_iacc.Protocol().registerHostProtocol('halkyon_learn', this);
@ -3336,6 +3258,15 @@ class Protocol extends _isd.DatabaseSerializationManager {
@override
_isd.Table? getTableForType(Type t) {
{
var protocol = _ihk4p5t7.Protocol();
var table = protocol is _isd.DatabaseSerializationManager
? (protocol as _isd.DatabaseSerializationManager).getTableForType(t)
: null;
if (table != null) {
return table;
}
}
{
var protocol = _iaic.Protocol();
var table = protocol is _isd.DatabaseSerializationManager
@ -3426,6 +3357,9 @@ class Protocol extends _isd.DatabaseSerializationManager {
if (record == null) {
return null;
}
try {
return _ihk4p5t7.Protocol().mapRecordToJson(record);
} catch (_) {}
try {
return _iaic.Protocol().mapRecordToJson(record);
} catch (_) {}

View file

@ -7,6 +7,12 @@ environment:
resolution: workspace
dependencies:
# The client side of halkyon_base (ADR 0014), as the server's.
halkyon_base_client:
git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_base_client
ref: af1de799f402ef76ad4513052c3e0008155a92fb
serverpod_auth_idp_client: 4.0.4
serverpod_client: 4.0.4
serverpod_offline_sync_client: 0.0.10

View file

@ -2,7 +2,7 @@ import 'dart:async';
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:halkyon_learn_client/halkyon_learn_client.dart'
import 'package:halkyon_base_client/halkyon_base_client.dart'
show GitHubSignInRefusal, GitHubSignInRefusedException;
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_repository.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/signed_out_mark.dart';

View file

@ -3,13 +3,13 @@ import 'dart:developer';
import 'dart:io';
import 'package:flutter_web_auth_2_platform_interface/flutter_web_auth_2_platform_interface.dart';
import 'package:halkyon_learn_client/halkyon_learn_client.dart'
import 'package:halkyon_base_client/halkyon_base_client.dart'
show
Client,
EmailSignInRefusal,
EmailSignInRefusedException,
GitHubSignInRefusal,
GitHubSignInRefusedException;
import 'package:halkyon_learn_client/halkyon_learn_client.dart' show Client;
import 'package:halkyon_learn_flutter/src/feature/auth/data/auth_gateway.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/data/loopback_web_auth.dart';
import 'package:halkyon_learn_flutter/src/feature/auth/model/auth_status.dart';
@ -19,7 +19,8 @@ import 'package:halkyon_learn_flutter/src/feature/auth/model/sign_in_ways.dart';
import 'package:serverpod_auth_idp_flutter/serverpod_auth_idp_flutter.dart';
/// [AuthGateway] on top of the Serverpod client and its session manager,
/// which keeps the session on the device (secure storage).
/// which keeps the session on the device (secure storage). Signing in and the
/// account are halkyon_base's (ADR 0014): `client.modules.base`.
class ServerpodAuthGateway(final Client _client) implements AuthGateway {
this {
_client.auth.authInfoListenable.addListener(_onAuthInfoChanged);
@ -69,7 +70,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
@override
Future<void> signInAnonymously() async {
final authSuccess = await _client.anonymousIdp.login();
final authSuccess = await _client.modules.base.anonymousIdp.login();
await _client.auth.updateSignedInUser(authSuccess);
}
@ -80,7 +81,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
@override
Future<void> deleteAccount() async {
await _client.account.delete();
await _client.modules.base.account.delete();
// The server ended every session of the person; forget this device's
// copy.
await _client.auth.updateSignedInUser(null);
@ -97,7 +98,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
return GitHubLinkOutcome.cancelled;
}
try {
await _client.gitHubUpgrade.upgrade(
await _client.modules.base.gitHubUpgrade.upgrade(
code: authorization.code,
codeVerifier: authorization.codeVerifier,
redirectUri: authorization.redirectUri,
@ -135,7 +136,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
final signedOut = Client(_client.host);
final AuthSuccess person;
try {
person = await signedOut.gitHubSignIn.signIn(
person = await signedOut.modules.base.gitHubSignIn.signIn(
code: authorization.code,
codeVerifier: authorization.codeVerifier,
redirectUri: authorization.redirectUri,
@ -217,7 +218,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
String address, {
required String language,
}) => _asEmailFailure(() async {
final request = await _client.emailCode.sendCode(
final request = await _client.modules.base.emailCode.sendCode(
address: address,
language: language,
);
@ -227,7 +228,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
@override
Future<EmailCodeOutcome> verifyEmailCode(String request, String code) =>
_asEmailFailure(() async {
final accepted = await _client.emailCode.verifyCode(
final accepted = await _client.modules.base.emailCode.verifyCode(
request: UuidValue.withValidation(request),
code: code,
);
@ -244,7 +245,9 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
final signedOut = Client(_client.host);
final AuthSuccess person;
try {
person = await signedOut.emailSignIn.signIn(switchToken: switchToken);
person = await signedOut.modules.base.emailSignIn.signIn(
switchToken: switchToken,
);
} finally {
signedOut.close();
}
@ -254,7 +257,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
@override
Future<SignInWays> signInWays() async {
final ways = await _client.account.signInWays();
final ways = await _client.modules.base.account.signInWays();
return SignInWays(gitHub: ways.gitHub, email: ways.email);
}
@ -288,7 +291,7 @@ class ServerpodAuthGateway(final Client _client) implements AuthGateway {
final client = Client(_client.host)..authSessionManager = sessionManager;
try {
await sessionManager.updateSignedInUser(anonymous);
await client.account.delete();
await client.modules.base.account.delete();
} on Exception catch (error, st) {
log(
'Deleting the anonymous account left behind failed',

View file

@ -73,6 +73,14 @@ dependencies:
# schedules.
fsrs: 2.0.1
go_router: ^18.0.2
# The client side of halkyon_base (ADR 0014): what the app calls to sign in
# and to manage the account, and the refusals it reads. As the server's,
# pinned to a signed commit of halkyon_shared's main.
halkyon_base_client:
git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_base_client
ref: af1de799f402ef76ad4513052c3e0008155a92fb
halkyon_learn_client:
path: ../halkyon_learn_client
# Halkyon's look, shared by its apps (halkyon_shared): the theme, the bundled Noto
@ -83,7 +91,7 @@ dependencies:
git:
url: https://code.halkyon.app/project-halkyon/halkyon_shared.git
path: packages/halkyon_ui
ref: 80fa7b96286b7a00791c4b7ea42af408f4ea548a
ref: af1de799f402ef76ad4513052c3e0008155a92fb
# Anki's fields are HTML; the editor brings this parser along already.
html: ^0.15.6
# Talks to Jellyfin servers; Serverpod's client brings it along already.

View file

@ -41,7 +41,7 @@ const _serverUrl = 'http://localhost:8080/';
Future<AuthSuccess> signInAnonymously() async {
final client = protocol.Client(_serverUrl);
try {
return await client.anonymousIdp.login();
return await client.modules.base.anonymousIdp.login();
} finally {
client.close();
}

Some files were not shown because too many files have changed in this diff Show more