halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
# halkyon_app
|
|
|
|
|
|
|
|
|
|
What every Halkyon app (Learn, Todo, Train) is built around, on an instance's
|
|
|
|
|
`halkyon_base` module: the instances a device keeps an account on, signing in,
|
|
|
|
|
each person's data opened and closed, and syncing it. Taken from Halkyon Learn,
|
|
|
|
|
which it was written for, so it behaves as Learn's own did; the texts are in
|
|
|
|
|
British English, American English and German, and where they name the app,
|
|
|
|
|
they name yours.
|
|
|
|
|
|
|
|
|
|
Written as the apps are: Dart 3.13's primary constructors, Material from
|
|
|
|
|
`package:material_ui`, Lucide icons, blocs (`flutter_bloc`), LeanCode's lints.
|
|
|
|
|
|
|
|
|
|
## What it holds
|
|
|
|
|
|
|
|
|
|
- **Instances** (`lib/src/instance/`): `Instance` (an address as people type
|
2026-10-21 15:12:43 +02:00
|
|
|
it; `isSecure`: over HTTPS, or over plain HTTP on this machine alone),
|
|
|
|
|
`InstanceChoice` (those the device keeps an account on, one in use;
|
|
|
|
|
`FileInstanceStore`; `allows` an instance reached securely, or in a
|
|
|
|
|
development build the build's own over HTTP, and `leave`s one it no longer
|
|
|
|
|
allows, which `InstanceGate` never opens: it says why and offers another or
|
|
|
|
|
removing it), `InstanceStorage` (each instance's people's folders and
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
session key), `InstanceChecker` (does an address answer, where does it keep
|
|
|
|
|
people's data; `ServerpodInstanceChecker` asks its `instance.basics()`),
|
|
|
|
|
`InstanceScope`. The screens: `FirstStartScreen` (the build's instance and
|
|
|
|
|
another, as tiles, `InstanceTile`), `InstanceStep` (an address, checked),
|
|
|
|
|
`showAccountSwitcher` and `AddAccountScreen`, and where an instance keeps
|
|
|
|
|
people's data, as a flag and words (`InstanceLocationBuilder`,
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
`InstanceFlag`, `instanceLocationWords`): `EU`, a country or a subdivision
|
|
|
|
|
(ISO 3166-2, `US-CA`) as its operator says, "Germany (EU)" for a member of
|
|
|
|
|
the European Union (`europeanUnionMembers`) and "California, United States"
|
|
|
|
|
for a subdivision, with the most specific flag there is (a few subdivisions
|
refactor(halkyon_app): the account section, About, the language and countries' names, shared
What Learn and Todo each held now comes from halkyon_app:
- AccountTiles: whose account on which instance, switching, signing
out (asking first for an anonymous account, as it deletes it) and
deleting it (asking first), the app's own words passed in.
- showAppAbout: the app's name, version, icon and legalese as it gives
them, the source of what the instance runs (AppDependencies gains
sourceCodeUrl), the app's own parts, the licence list.
- resolveAppLocale: the first preferred language the app speaks, never
by country alone, British English otherwise.
- AssetCountryNames, the default of InstanceGate and InstanceScope:
Unicode CLDR 48.2's territory names in English and German
(tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and
Palestine by their short names as Learn's world map has them, so
Todo says "Germany (EU)" where it said "DE (EU)".
Learn's texts read as before in every language; its About bloc and
gateway, its country names from the world map and the keys only the
moved code used are gone, as are Todo's copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 23:51:52 +02:00
|
|
|
have their own, such as Scotland; else the country's). Countries and
|
|
|
|
|
subdivisions are named by Unicode CLDR's names in English and German, which
|
|
|
|
|
come with the package (`AssetCountryNames`, `AssetSubdivisionNames`; the
|
|
|
|
|
package's `tool/country_names.py` and `tool/subdivision_names.py` write
|
|
|
|
|
them, from one release); tests give names of their own (`CountryNames`,
|
|
|
|
|
`SubdivisionNames`).
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
- **Gates** (`lib/src/app/`): `InstanceGate` opens the app's dependencies on
|
|
|
|
|
the instance in use (`AppDependencies`, handed down by `AppScope`), anew on
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
a switch, the previous ones closed first; `SessionGate` shows the app of
|
|
|
|
|
whoever is signed in and asks how to start while nobody is
|
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.
- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
GitHub refusals; no `githubClientId`/`githubClientSecret`, no
`GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
loses `account-with-github`. `SignInWays.gitHub`, the basics'
`gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
`signInAgain` and `noAddress` go. A first password takes a code
mailed to the person's own address; an account without one sets none
(`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
(v0.20.0) require `SignInWays.gitHub`: the server deploys with the
release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
password offered after a GitHub sign-in, and `ServerpodAuthGateway`
without the browser half only GitHub used (`(client, base:)`). The
account section offers a first password only to an account with an
address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
migrations make it; a row goes with its account, and ADR 0020's D36
gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
AGENTS.md files and ADR 0017 amended.
BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
|
|
|
(`StartScreen`: SimpleLogin, a password, another instance; an instance out of reach is said so, with Try again,
|
feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)
Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):
- halkyon_app: InstanceOperatorRepository on
ServerpodInstanceOperatorGateway (the module's instanceOperator
endpoint; refusals as OperatorFailed: notFound, isOperator,
lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
OperatingCubit counts the accounts that wait: asked as the person's
app opens, after every change, and every 5 minutes only while they
operate the instance, so nobody else's app keeps asking.
InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
nothing at all for anyone else. InstanceOperatorScreen, laid out as
Settings: the accounts that wait, each showing its four words to read
out before Approve (D33), Refuse for new ones after asking; who
operates, Take back after asking, the last always kept; Name an
operator by handle; Find an account by the start of its handle, with
Suspend after asking and Name as operator. English and German texts
("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
cogs' count leads to as their dot leads to Sync; the count on the
Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
Todo's server is a member, whose `operating()` is always null, so
nothing shows there until it holds the register.
Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 15:55:28 +01:00
|
|
|
and one offering no way to create an account too; where new accounts wait
|
|
|
|
|
for the operator's approval, or none are made, it says so first), at a
|
|
|
|
|
first start as after signing out; while an account signed in or up to
|
|
|
|
|
here waits for the operator (Halkyon Learn's ADR 0021, D31), `WaitScreen`
|
|
|
|
|
shows its handle and the four words the operator reads out for the person
|
|
|
|
|
to match, asks every 30 seconds and as the app comes back, deletes a new
|
|
|
|
|
account after asking, and once approved leads to signing in once more
|
|
|
|
|
(the wait kept in the keyring per instance, `SecureAccountWaitStore`, so a
|
|
|
|
|
restart shows it again); an anonymous account's session, left from before
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
there were none, is deleted as the app starts (ADR 0020, D25), falling
|
|
|
|
|
back to signing out on the device; `PersonScope` opens each person's own
|
refactor(halkyon_app): the account section, About, the language and countries' names, shared
What Learn and Todo each held now comes from halkyon_app:
- AccountTiles: whose account on which instance, switching, signing
out (asking first for an anonymous account, as it deletes it) and
deleting it (asking first), the app's own words passed in.
- showAppAbout: the app's name, version, icon and legalese as it gives
them, the source of what the instance runs (AppDependencies gains
sourceCodeUrl), the app's own parts, the licence list.
- resolveAppLocale: the first preferred language the app speaks, never
by country alone, British English otherwise.
- AssetCountryNames, the default of InstanceGate and InstanceScope:
Unicode CLDR 48.2's territory names in English and German
(tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and
Palestine by their short names as Learn's world map has them, so
Todo says "Germany (EU)" where it said "DE (EU)".
Learn's texts read as before in every language; its About bloc and
gateway, its country names from the world map and the keys only the
moved code used are gone, as are Todo's copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 23:51:52 +02:00
|
|
|
dependencies and closes them when the person goes. `resolveAppLocale`
|
|
|
|
|
picks the app's language as every Halkyon app does: the first preferred
|
|
|
|
|
language it speaks, never by country alone, British English otherwise.
|
feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)
Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):
- halkyon_app: InstanceOperatorRepository on
ServerpodInstanceOperatorGateway (the module's instanceOperator
endpoint; refusals as OperatorFailed: notFound, isOperator,
lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
OperatingCubit counts the accounts that wait: asked as the person's
app opens, after every change, and every 5 minutes only while they
operate the instance, so nobody else's app keeps asking.
InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
nothing at all for anyone else. InstanceOperatorScreen, laid out as
Settings: the accounts that wait, each showing its four words to read
out before Approve (D33), Refuse for new ones after asking; who
operates, Take back after asking, the last always kept; Name an
operator by handle; Find an account by the start of its handle, with
Suspend after asking and Name as operator. English and German texts
("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
cogs' count leads to as their dot leads to Sync; the count on the
Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
Todo's server is a member, whose `operating()` is always null, so
nothing shows there until it holds the register.
Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 15:55:28 +01:00
|
|
|
- **The instance's operators** (`lib/src/operator/`, Halkyon Learn's ADR
|
|
|
|
|
0021, D21 to D24 and D34): `InstanceOperatorRepository` on
|
|
|
|
|
`ServerpodInstanceOperatorGateway` (the module's `instanceOperator`
|
|
|
|
|
endpoint, refusals as `OperatorFailed`), `OperatingCubit` (how many
|
|
|
|
|
accounts wait, null for anyone who doesn't operate the instance; asked as
|
|
|
|
|
the person's app opens, after each change, and every 5 minutes only while
|
|
|
|
|
they operate it), `InstanceOperatorCard` or `InstanceOperatorTiles` for an
|
|
|
|
|
app's settings and `WaitingAccountsBadge` for its Settings cog, both
|
|
|
|
|
nothing at all for anyone else, and `InstanceOperatorScreen`: the accounts
|
|
|
|
|
that wait, approved once their person confirmed the words read out, or
|
|
|
|
|
refused; who operates, named by handle and taken back (never the last);
|
|
|
|
|
accounts found by the start of their handle, suspended or named. Only the
|
|
|
|
|
register's service answers: on a member, nothing shows.
|
feat(app,learn,todo): the assistants in Settings and on the operator's screen
ADR 0021, step 2's app half. halkyon_app asks the module's assistant
endpoints (ServerpodAssistantGateway, ServerpodAssistantOperatorGateway)
and a person's own provider directly (halkyon_llm), behind
AssistantRepository: an AssistantChoice is a GrantedAssistant, asked
through the instance, which counts it, or a PersonalAssistant with the
person's own key, which the instance never sees. assistant_messages.dart
turns a conversation into the endpoint's models and back.
AssistantsCard, for an app's settings: what is granted and left of its
limits, agreed to once on a sheet saying what goes where, the person's
use by UTC month, the instance's privacy page, and their own assistants
where the operator allows own keys (kept ones stay listed, unasked,
where it doesn't). AssistantGrantsCard on InstanceOperatorScreen: grants
by handle with their limits in the pricing's unit, changed and taken
back after asking, everyone's use by month; a found account's menu
grants too. Nothing shows where nothing is offered or can be read.
package:halkyon_app/assistant_models.dart holds the models in pure Dart.
Learn keeps own assistants in the person's settings (ownAssistants,
schema 12, SettingsOwnAssistants); SettingsRepository.update refuses
(OwnAssistantRefused) to write a key where the settings would not be
sealed (SettingsGateway.seals). Learn depends on halkyon_llm directly.
Todo wires the gateways; a member offers no assistant. No feature asks
an assistant yet (step 3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 18:59:41 +01:00
|
|
|
- **Assistants** (`lib/src/assistant/`, Halkyon Learn's ADR 0021, step 2):
|
|
|
|
|
`AssistantRepository` on `ServerpodAssistantGateway` (the module's
|
|
|
|
|
`assistant` endpoint; refusals as `AssistantFailed`) asks an
|
|
|
|
|
`AssistantChoice` in halkyon_llm's terms: a `GrantedAssistant` through the
|
|
|
|
|
instance, which counts it, or a `PersonalAssistant` with the person's own
|
|
|
|
|
key directly (`LlmClient`), the instance seeing neither key nor question;
|
|
|
|
|
`assistant_messages.dart` turns a conversation into the endpoint's models
|
|
|
|
|
and back, part by part. `AssistantsCard` is an app's settings' card: what
|
|
|
|
|
is granted and left of its limits, agreed to once (the consent sheet),
|
|
|
|
|
the person's use by UTC month, the instance's privacy page, and the
|
|
|
|
|
person's own assistants where the operator allows them, kept where the
|
|
|
|
|
app says (`OwnAssistantStore`: Learn's sealed settings). For the operator,
|
|
|
|
|
`AssistantOperatorRepository` (grants by exact handle) and
|
|
|
|
|
`AssistantGrantsCard` on `InstanceOperatorScreen`: grants with their
|
|
|
|
|
limits, granted, changed and taken back, and everyone's use by month.
|
|
|
|
|
`package:halkyon_app/assistant_models.dart` holds the models alone, in
|
|
|
|
|
pure Dart, for an app's own models.
|
refactor(halkyon_app): the account section, About, the language and countries' names, shared
What Learn and Todo each held now comes from halkyon_app:
- AccountTiles: whose account on which instance, switching, signing
out (asking first for an anonymous account, as it deletes it) and
deleting it (asking first), the app's own words passed in.
- showAppAbout: the app's name, version, icon and legalese as it gives
them, the source of what the instance runs (AppDependencies gains
sourceCodeUrl), the app's own parts, the licence list.
- resolveAppLocale: the first preferred language the app speaks, never
by country alone, British English otherwise.
- AssetCountryNames, the default of InstanceGate and InstanceScope:
Unicode CLDR 48.2's territory names in English and German
(tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and
Palestine by their short names as Learn's world map has them, so
Todo says "Germany (EU)" where it said "DE (EU)".
Learn's texts read as before in every language; its About bloc and
gateway, its country names from the world map and the keys only the
moved code used are gone, as are Todo's copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 23:51:52 +02:00
|
|
|
- **About** (`showAppAbout`): the app's name (its frame's), its version,
|
|
|
|
|
icon and legalese where it gives them, where the source of what the
|
|
|
|
|
instance runs is (`InstanceSourceCode`, from `AppDependencies.sourceCodeUrl`;
|
|
|
|
|
both licences require offering it), the app's own parts (its credits), and
|
|
|
|
|
the licence list.
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
- **The account** (`AccountTiles`): on which instance the account is signed
|
|
|
|
|
in, switching to the account on another, signing out and deleting it
|
|
|
|
|
(after asking), for an
|
2026-10-21 19:49:31 +02:00
|
|
|
app's settings, in the app's words where only it can say them. While one
|
|
|
|
|
runs, the person's app stays and its tile shows a spinner; one that fails
|
|
|
|
|
keeps them signed in and says why once, in a snack bar (in a sheet, give
|
|
|
|
|
it `hasSnackBars`). An instance out of reach signs out on the device all
|
|
|
|
|
the same (Serverpod's `signOutDevice`), so only the device's own storage
|
|
|
|
|
fails a sign-out.
|
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.
- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
GitHub refusals; no `githubClientId`/`githubClientSecret`, no
`GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
loses `account-with-github`. `SignInWays.gitHub`, the basics'
`gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
`signInAgain` and `noAddress` go. A first password takes a code
mailed to the person's own address; an account without one sets none
(`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
(v0.20.0) require `SignInWays.gitHub`: the server deploys with the
release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
password offered after a GitHub sign-in, and `ServerpodAuthGateway`
without the browser half only GitHub used (`(client, base:)`). The
account section offers a first password only to an account with an
address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
migrations make it; a row goes with its account, and ADR 0020's D36
gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
AGENTS.md files and ADR 0017 amended.
BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
|
|
|
- **The session** (`lib/src/auth/`): `AuthRepository` on an `AuthGateway`;
|
|
|
|
|
`ServerpodAuthGateway` follows the session the app's Serverpod client
|
|
|
|
|
keeps, signs out, deletes the account through the instance's
|
|
|
|
|
`halkyon_base`, and puts a session granted elsewhere in place
|
|
|
|
|
(`useSession`: a password's or SimpleLogin's sign-in; GitHub signs nobody
|
|
|
|
|
in since ADR 0020, D36); `SessionBloc` and `SignInWaysCubit`;
|
|
|
|
|
`SignInAddresses`, `PersonFolders`.
|
2026-10-21 21:50:29 +02:00
|
|
|
- **Content sealed with data keys** (`lib/src/privacy/`, ADR 0019):
|
|
|
|
|
`ContentCipher` (`SodiumContentCipher`, on the isolate it is used on, as an
|
|
|
|
|
app seals and opens rows one by one; `FakeContentCipher` in tests) makes a
|
|
|
|
|
store's data keys, sealed to the account and signed by it, opens them, and
|
|
|
|
|
seals and opens a row's content for the row it is stored in (its table, id
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
and column), the payload holding the signature before the ciphertext.
|
2026-10-20 19:34:02 +02:00
|
|
|
- **A password and TOTP** (`lib/src/password/`, ADR 0017): `PasswordRepository`
|
|
|
|
|
on a `PasswordGateway` (`ServerpodPasswordGateway`), an `AccountCrypto`
|
|
|
|
|
(`SodiumAccountCrypto`: halkyon_keys and SRP's client half, each step on an
|
|
|
|
|
isolate of its own, whose results are copied before they cross back:
|
|
|
|
|
sodium's are views of memory freed with the isolate that made them) and an
|
|
|
|
|
`AccountKeyStore` (`SecureAccountKeyStore`: the account's unlocked keys in
|
|
|
|
|
the system's keyring, per instance and person, gone once another person
|
2026-10-21 16:23:20 +02:00
|
|
|
signs in or nobody; on iOS and macOS `unlocked_this_device`, so readable
|
|
|
|
|
only while the device is unlocked, never in a backup restored elsewhere nor
|
2026-10-21 18:17:08 +02:00
|
|
|
in iCloud Keychain, each write deleting the entry before it, and forgetting
|
|
|
|
|
finding too the entries earlier builds kept with another accessibility, a
|
|
|
|
|
keyring that can't be read now stopping nothing). Signing in (SRP's M2 checked and the keys opened before
|
2026-10-20 19:34:02 +02:00
|
|
|
anything is trusted), signing up (the recovery phrase shown once and three of
|
|
|
|
|
its words typed back before anything reaches the instance), a setup never
|
|
|
|
|
finished, the authenticators (QR codes drawn with pub.dev's `qr`), recovery
|
|
|
|
|
codes, changing the password, a new phrase, and getting back in. The
|
|
|
|
|
screens: the start screen's Sign in and Create an account,
|
|
|
|
|
`showPasswordSignInSheet`, `showAccountSetupSheet`,
|
|
|
|
|
`showAccountRecoverySheet`, and `AccountSecuritySection` for an app's
|
2026-10-21 10:45:46 +02:00
|
|
|
account part. Every change of how the account is secured asks for the
|
|
|
|
|
password with the code, one answer per sheet (`OwnerAnswer`, the owner's
|
2026-10-21 14:18:23 +02:00
|
|
|
proof: a session alone gets no code checked), each answer bound to the
|
|
|
|
|
instance's base URL and to the change it approves (its purpose and the
|
|
|
|
|
digest of what it sets, `proof_digests.dart`), so nobody between the device
|
|
|
|
|
and the instance swaps anything in; deleting by a code by mail
|
2026-10-21 10:45:46 +02:00
|
|
|
follows `secondFactorWorks`, as the instance tells it, and asks for the
|
2026-10-21 15:12:43 +02:00
|
|
|
address only where the device doesn't know it. Secrets shown stay the
|
2026-10-21 16:11:41 +02:00
|
|
|
person's: nothing secret is copied (no app can mark a copy as sensitive
|
|
|
|
|
through Flutter, so clipboard histories and cloud clipboards would keep
|
|
|
|
|
it): the recovery phrase and codes are saved or written down, an
|
|
|
|
|
authenticator's key scanned, opened or typed; all of them show in a
|
2026-10-21 18:17:08 +02:00
|
|
|
`SecretScreen` (the phrase typed back too, its fields offering Paste and
|
|
|
|
|
Select all alone, their copy keys doing nothing), kept out of screenshots,
|
2026-10-21 16:40:43 +02:00
|
|
|
screen recordings and recents where the app's window answers
|
|
|
|
|
`app.halkyon/screen_guard`: Android's `FLAG_SECURE` (as Learn's and Todo's
|
|
|
|
|
`ScreenGuard.kt` in their `MainActivity`), macOS's `sharingType = .none`
|
2026-10-21 18:17:08 +02:00
|
|
|
(`MainFlutterWindow.swift`; screenshots, but not ScreenCaptureKit's
|
|
|
|
|
captures on macOS 15 and later) and Windows' `WDA_EXCLUDEFROMCAPTURE`
|
|
|
|
|
from Windows 10 2004 on, `WDA_MONITOR` (black in captures) before, by the
|
|
|
|
|
version `RtlGetVersion` tells (`flutter_window.cpp`); neither run yet;
|
|
|
|
|
covered on iOS in the app switcher's picture while the app isn't in front,
|
|
|
|
|
screenshots there not held back; nothing on Linux; keyboards learn nothing typed into a password, a code or
|
2026-10-21 15:12:43 +02:00
|
|
|
the phrase; no state's text (which a debug build's bloc observer logs) holds
|
|
|
|
|
a secret, a code or the address; only an `otpauth:` URI is shown as a QR
|
|
|
|
|
code or opened; and anything a step didn't expect (bytes of the wrong
|
|
|
|
|
length from an instance, libsodium's or the keyring's own errors) fails it
|
|
|
|
|
rather than leaving its sheet busy. `register_interop_test.dart` holds the
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
real cryptography to the register's own checks. Every build of an app
|
|
|
|
|
needs a C compiler and make: sodium's build hook compiles libsodium.
|
feat(app): signing in with SimpleLogin, its sheet and the start screen's offer (ADR 0020)
Where the instance offers SimpleLogin, the start screen offers it first,
saying that the instance keeps only the SimpleLogin ID, and opens a sheet
that waits for SimpleLogin's page in the browser (Cancel ends the wait),
then: for someone new, the choice between creating an account (the
suggested handle or another, checked with the instance, the password
twice, the recovery phrase shown once and three of its words typed back,
nothing reaching the instance before) and linking theirs again with its
handle and recovery phrase; for an account, its password opening its
keys; for an account of before, the keys made with the handle it lacks.
SimpleLoginCubit holds the steps, each refusal shown where it is mended
(a handle's on the handle, a sign-in to begin again on the browser's),
and closing the sheet stops waiting on the loopback.
The suggested handle is never checked: the instance suggests none that
anybody has, and every answer about a handle counts against its limit
(D5). The fake's handle rule is now the module's.
Learn and Todo build it with SimpleLogin's browser (halkyon_oidc_flutter,
now a direct dependency of both, with their names on the loopback's
page), their schemes and their loopback ports (43823, 43824), as the
instance's forwarding page has them. Their tests offer no SimpleLogin;
halkyon_app's test the sheet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 01:21:29 +02:00
|
|
|
- **Signing in with SimpleLogin** (`lib/src/simplelogin/`, ADR 0020):
|
|
|
|
|
`SimpleLoginRepository` on a `SimpleLoginGateway` (`ServerpodSimpleLoginGateway`,
|
|
|
|
|
halkyon_base's `simpleLogin` endpoint) and halkyon_oidc_flutter's
|
|
|
|
|
`OidcBrowser`: SimpleLogin's page in the system's browser sheet, back by
|
|
|
|
|
the app's scheme, or on Linux and Windows in the browser, back to the
|
|
|
|
|
app's port on the loopback, as the instance's forwarding page names them
|
|
|
|
|
(`callbackScheme`, `loopbackPort`: Learn's 43823, Todo's 43824). Someone
|
|
|
|
|
new creates an account (a handle, the one the instance suggests being
|
|
|
|
|
free; the password twice, which makes the account's keys on the device;
|
|
|
|
|
the recovery phrase shown once and three of its words typed back; nothing
|
|
|
|
|
reaches the instance before) or links theirs again in place of a lost
|
|
|
|
|
SimpleLogin account with its handle and recovery phrase, which open the
|
|
|
|
|
keys on the device before any proof is sent; an account opens its keys
|
|
|
|
|
with its password; an account of before gets its keys, and its handle
|
|
|
|
|
where it has none. With `link`, SimpleLogin joins the account of the
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
device's person instead. The
|
2026-10-24 11:56:26 +02:00
|
|
|
screens: the start screen's Continue with SimpleLogin, first where the
|
|
|
|
|
instance offers it, and `showSimpleLoginSheet` (`SimpleLoginCubit`),
|
|
|
|
|
which waits for the browser with Cancel and stops waiting on the loopback
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
once it closes. Adding it to an account (ADR 0020, D21): where the
|
2026-10-24 11:56:26 +02:00
|
|
|
person stands (`SimpleLoginRepository.standing`, read anew after every
|
|
|
|
|
change, `SimpleLoginStandingCubit`: nothing to show, linked, or
|
|
|
|
|
missing), shown by `SimpleLoginSection` (each app places it above
|
|
|
|
|
`AccountSecuritySection`: a heading and Add SimpleLogin; once added a row
|
|
|
|
|
among the ways in), which reads it once where a
|
feat(app)!: no anonymous accounts on the device (ADR 0020, D4, D25)
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 14:50:55 +02:00
|
|
|
`SimpleLoginStandingScope` is above it.
|
2026-10-20 19:34:02 +02:00
|
|
|
- **Files** (`FileSaver`, `PlatformFileSaver`): keeping a file the app made
|
|
|
|
|
where the person chooses (a recovery phrase, recovery codes, an app's
|
fix(app): a saved secret is 0600 on Apple's Arm too, and macOS saves at all
The short reading's H1: open(2) takes its mode among variable arguments,
which Apple's Arm passes on the stack, so bound with three fixed ones it
read the mode from whatever the stack held: a phrase or recovery codes
saved on an Apple silicon Mac could be readable by everyone. It is bound
as variadic now (VarArgs), and every file written is made 0600 through its
descriptor (fchmod), checked.
M1: macOS's sandbox lets an app reach the file the person chose and
nothing beside it, so the file made beside it was refused and nothing
could be saved; Learn's sandbox allowed reading chosen files alone, so its
exports never saved on macOS either. There, writePrivately now writes the
chosen file itself (decided by the developer on 21 Oct): opened without
following a link or waiting on a pipe and without truncating, refused with
nothing written unless fstat finds a file of the own user's, then made
0600 and emptied. Both apps' entitlements grant reading and writing the
files the person chooses. Linux keeps the new file renamed over it.
The reading's nits: the buffer is wiped whatever happens, an interrupted
call is made again (EINTR), the file made beside a chosen one is named
apart from it so a name as long as a folder takes still saves, the folder
is flushed after the rename, and Linux reads a file's type and owner with
statx, alike on every architecture, where __lxstat's version differed
(riscv64's was wrong). The test of another user's file is skipped as root.
The in-place path is tested on Linux; none of it has run on a Mac.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 17:57:44 +02:00
|
|
|
exports): on Linux and macOS the own user's alone (`writePrivately`). On
|
|
|
|
|
Linux a new file beside it, made 0600 with `O_EXCL`, written through its
|
|
|
|
|
descriptor, renamed over the one chosen, never written into, and the
|
|
|
|
|
folder flushed. On macOS, whose sandbox lets an app reach the file chosen
|
|
|
|
|
and nothing beside it, the file itself, opened without following a link
|
|
|
|
|
or waiting on a pipe, refused unless it is a file of the own user's, made
|
|
|
|
|
0600 and emptied before anything is written; whoever held it open reads
|
|
|
|
|
what is saved. Each app's macOS entitlements grant reading and writing
|
|
|
|
|
the files the person chooses (`com.apple.security.files.user-selected.read-write`),
|
|
|
|
|
without which nothing could be saved there, Learn's exports neither. On
|
|
|
|
|
both, a symbolic link or another user's file is refused with nothing
|
|
|
|
|
written. On Windows written in place with its folder's rights, on iOS
|
|
|
|
|
without the copy file_picker leaves in the temporary folder
|
|
|
|
|
(`forgetPickerCopy`). None of it has run on a Mac yet.
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
- **Sync** (`lib/src/sync/`): `SyncService` (once at start, shortly after
|
|
|
|
|
local changes, on resume and every few minutes, one sync at a time, counting
|
|
|
|
|
the changes not on the server yet, `PendingChangesStore`), `AppCompatibility`
|
|
|
|
|
and `compatibleSync` (the instance's minimum app build, the engine's table
|
|
|
|
|
check), `SyncStatusBloc`, `syncAndWait`, and the widgets: `SyncStatusCard`,
|
|
|
|
|
`AppMismatchBanner`, `SyncTroubleBadge`.
|
|
|
|
|
- **A client of `halkyon_base` alone** (`BaseClient`): to ask an instance
|
|
|
|
|
about itself, and to sign in as another person than the app's client holds.
|
|
|
|
|
- **Testing** (`package:halkyon_app/testing.dart`): fakes of every boundary
|
feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)
Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):
- halkyon_app: InstanceOperatorRepository on
ServerpodInstanceOperatorGateway (the module's instanceOperator
endpoint; refusals as OperatorFailed: notFound, isOperator,
lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
OperatingCubit counts the accounts that wait: asked as the person's
app opens, after every change, and every 5 minutes only while they
operate the instance, so nobody else's app keeps asking.
InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
nothing at all for anyone else. InstanceOperatorScreen, laid out as
Settings: the accounts that wait, each showing its four words to read
out before Approve (D33), Refuse for new ones after asking; who
operates, Take back after asking, the last always kept; Name an
operator by handle; Find an account by the start of its handle, with
Suspend after asking and Name as operator. English and German texts
("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
cogs' count leads to as their dot leads to Sync; the count on the
Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
Todo's server is a member, whose `operating()` is always null, so
nothing shows there until it holds the register.
Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 15:55:28 +01:00
|
|
|
(`FakeAuthGateway`, with waits, `fakeWaitOf`; `FakePasswordGateway`,
|
|
|
|
|
`FakeAccountCrypto`, `FakeInstanceChecker`, `FakeInstanceOperatorGateway`,
|
|
|
|
|
the stores in memory), a frame
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
(`testFrame`), `testApp`, and `TestAppDependencies`.
|
|
|
|
|
|
|
|
|
|
## Using it in an app
|
|
|
|
|
|
|
|
|
|
1. **The server** includes `halkyon_base` with the nickname `base` (its
|
|
|
|
|
README), so the app's generated client has `client.modules.base`.
|
2026-10-19 20:39:09 +02:00
|
|
|
2. **Depend on it** by path, within the repository's workspace, as on
|
|
|
|
|
`halkyon_ui`:
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
|
|
|
|
|
```yaml
|
|
|
|
|
dependencies:
|
|
|
|
|
halkyon_app:
|
2026-10-19 20:39:09 +02:00
|
|
|
path: ../../../packages/halkyon_app
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
3. **Texts**: add `HalkyonAppLocalizations.delegate` to every `MaterialApp`'s
|
|
|
|
|
delegates, beside `HalkyonUiLocalizations.delegate` and material_ui's
|
|
|
|
|
`GlobalMaterialLocalizations.delegates`.
|
|
|
|
|
4. **The frame** (`AppFrame`): the app's name and wordmark word, its icon, an
|
refactor(halkyon_app): the account section, About, the language and countries' names, shared
What Learn and Todo each held now comes from halkyon_app:
- AccountTiles: whose account on which instance, switching, signing
out (asking first for an anonymous account, as it deletes it) and
deleting it (asking first), the app's own words passed in.
- showAppAbout: the app's name, version, icon and legalese as it gives
them, the source of what the instance runs (AppDependencies gains
sourceCodeUrl), the app's own parts, the licence list.
- resolveAppLocale: the first preferred language the app speaks, never
by country alone, British English otherwise.
- AssetCountryNames, the default of InstanceGate and InstanceScope:
Unicode CLDR 48.2's territory names in English and German
(tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and
Palestine by their short names as Learn's world map has them, so
Todo says "Germany (EU)" where it said "DE (EU)".
Learn's texts read as before in every language; its About bloc and
gateway, its country names from the world map and the keys only the
moved code used are gone, as are Todo's copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 23:51:52 +02:00
|
|
|
example address, its theme, delegates, languages (`resolveAppLocale` as
|
feat(base,app)!: GitHub signs nobody in any more (ADR 0020, D36)
Every account signs in with SimpleLogin or a password now, so GitHub
goes from the module, the shared app package, Learn and Todo, ahead of
ADR 0017's GITHUB_SIGN_IN_ENDS.
- halkyon_base: no `gitHubSignIn` endpoint, GitHub identity provider or
GitHub refusals; no `githubClientId`/`githubClientSecret`, no
`GITHUB_SIGN_IN_ENDS` and no `forgetGitHubAccounts`; the register
loses `account-with-github`. `SignInWays.gitHub`, the basics'
`gitHubClientId` and `gitHubSignInEnds`, and `PasswordRefusal`'s
`signInAgain` and `noAddress` go. A first password takes a code
mailed to the person's own address; an account without one sets none
(`wrongCredentials`).
- Learn's server: `instance.info()` without `gitHubClientId`, the
pruning without GitHub, and `minimumAppBuild` 24, as builds up to 23
(v0.20.0) require `SignInWays.gitHub`: the server deploys with the
release of build 24.
- halkyon_app: no GitHub button or problems on the start screen, no
password offered after a GitHub sign-in, and `ServerpodAuthGateway`
without the browser half only GitHub used (`(client, base:)`). The
account section offers a first password only to an account with an
address. `LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's
ways back.
- Learn's Profile lists no GitHub; the texts go (English, German).
- Serverpod's `serverpod_auth_idp_github_account` stays, as its
migrations make it; a row goes with its account, and ADR 0020's D36
gives operators the SQL to delete those left.
- deploy/: compose.yaml and .env.example without GitHub. The READMEs,
AGENTS.md files and ADR 0017 amended.
BREAKING CHANGE: builds up to 23 can't read the instance's answers
about an account's ways in; minimumAppBuild is 24.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 12:52:52 +01:00
|
|
|
its resolution) and launch screen, and the sentence only it can say: what
|
|
|
|
|
the first start's instance keeps of the person's.
|
2026-10-21 15:12:43 +02:00
|
|
|
5. **Android**: the app's `MainActivity` answers `app.halkyon/screen_guard`
|
|
|
|
|
(copy Learn's or Todo's `ScreenGuard.kt` and call `ScreenGuard.attach` in
|
|
|
|
|
`configureFlutterEngine`), or secrets show in screenshots and recents.
|
|
|
|
|
6. **The dependencies**: the app's composition root implements
|
feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)
Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):
- halkyon_app: InstanceOperatorRepository on
ServerpodInstanceOperatorGateway (the module's instanceOperator
endpoint; refusals as OperatorFailed: notFound, isOperator,
lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
OperatingCubit counts the accounts that wait: asked as the person's
app opens, after every change, and every 5 minutes only while they
operate the instance, so nobody else's app keeps asking.
InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
nothing at all for anyone else. InstanceOperatorScreen, laid out as
Settings: the accounts that wait, each showing its four words to read
out before Approve (D33), Refuse for new ones after asking; who
operates, Take back after asking, the last always kept; Name an
operator by handle; Find an account by the start of its handle, with
Suspend after asking and Name as operator. English and German texts
("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
cogs' count leads to as their dot leads to Sync; the count on the
Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
Todo's server is a member, whose `operating()` is always null, so
nothing shows there until it holds the register.
Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 15:55:28 +01:00
|
|
|
`AppDependencies<P>` (`serverName`, `auth`, `passwords`, `simpleLogin`,
|
|
|
|
|
`instanceOperators`, `fileSaver`, `sourceCodeUrl`, `openPerson`,
|
|
|
|
|
`dispose`), its person's dependencies `Disposable`. Its `passwords` is a `PasswordRepository` on
|
2026-10-20 19:34:02 +02:00
|
|
|
`ServerpodPasswordGateway(client, base: client.modules.base)`,
|
|
|
|
|
`SodiumAccountCrypto()`, `SecureAccountKeyStore(storage.keysOf(instance))`,
|
|
|
|
|
the auth gateway and the app's `SignInAddresses`. Its `auth` is an
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
`AuthRepository` on `ServerpodAuthGateway(client, base:
|
feat(app,learn,todo): the operator's "This instance" card, screen and badge (ADR 0021, step 1)
Whoever operates the instance approves, refuses and suspends accounts
and names other operators in the app (D21 to D24, D34):
- halkyon_app: InstanceOperatorRepository on
ServerpodInstanceOperatorGateway (the module's instanceOperator
endpoint; refusals as OperatorFailed: notFound, isOperator,
lastOperator, notAllowed for 401 and 403, failed), in AppDependencies.
OperatingCubit counts the accounts that wait: asked as the person's
app opens, after every change, and every 5 minutes only while they
operate the instance, so nobody else's app keeps asking.
InstanceOperatorCard (or its tiles) and WaitingAccountsBadge show
nothing at all for anyone else. InstanceOperatorScreen, laid out as
Settings: the accounts that wait, each showing its four words to read
out before Approve (D33), Refuse for new ones after asking; who
operates, Take back after asking, the last always kept; Name an
operator by handle; Find an account by the start of its handle, with
Suspend after asking and Name as operator. English and German texts
("Admin" as the role word in German).
- Learn: the card right after the Sync card in Settings, which the
cogs' count leads to as their dot leads to Sync; the count on the
Settings cogs of Home and Profile.
- Todo: the tiles in the account sheet and the count on its button.
Todo's server is a member, whose `operating()` is always null, so
nothing shows there until it holds the register.
Tests: the gateway's mapping, the repository, both cubits with their
denials, the card, the badge and the screen; Learn's Settings shows the
card and the count to an operator and neither to anyone else.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-25 15:55:28 +01:00
|
|
|
client.modules.base, waits:
|
|
|
|
|
SecureAccountWaitStore(storage.waitOf(instance)))`, whose `initialize()`
|
|
|
|
|
it awaits before anything asks who is signed in. Its `instanceOperators`
|
|
|
|
|
is an `InstanceOperatorRepository` on
|
|
|
|
|
`ServerpodInstanceOperatorGateway(client.modules.base)`; the person's app
|
|
|
|
|
provides an `OperatingCubit` on it, for the card and the badge.
|
2026-10-21 15:12:43 +02:00
|
|
|
7. **The tree**:
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
|
|
|
|
|
```dart
|
|
|
|
|
InstanceGate(
|
|
|
|
|
frame: frame,
|
|
|
|
|
choice: choice,
|
|
|
|
|
checker: ServerpodInstanceChecker(app: 'todo'),
|
|
|
|
|
readLook: …, // the device's theme mode and language
|
|
|
|
|
open: Dependencies.create,
|
|
|
|
|
app: SessionGate(
|
|
|
|
|
look: (context) => …,
|
|
|
|
|
app: (person) => PersonApp(person: person), // with PersonScope inside
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
A person's widgets find their dependencies with
|
|
|
|
|
`PersonScope.of<PersonDependencies>(context)` and the app's with
|
|
|
|
|
`AppScope.of<Dependencies>(context)`.
|
2026-10-21 15:12:43 +02:00
|
|
|
8. **Licences**: call `registerHalkyonAppLicenses()` once at startup (the
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
flags' and Unicode CLDR's notices), beside halkyon_ui's
|
|
|
|
|
`registerFontLicenses()`.
|
halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps
What every Halkyon app's shell needs, taken from Halkyon Learn's and set
on halkyon_base's client: the instances a device keeps an account on,
the first start's tiles and where an instance keeps people's data, the
account switcher; signing in anonymously, with GitHub or an email code,
switching person and the start screen; the gates that build an app's
dependencies per instance and per person; and the sync service with its
status, Settings card and banner. Texts of its own in English and
German take the app's name; what only the app can say, it passes in.
Fakes for apps' tests are in testing.dart.
An instance's basics now name the app it serves (`app`, set by the
host's configuration), and an app accepts only an instance of its own.
SerialQueue and CoalescingRunner move into halkyon_core.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 15:18:49 +02:00
|
|
|
|
|
|
|
|
## Licence
|
|
|
|
|
|
|
|
|
|
GNU Affero General Public License 3.0 or later (`LICENSE`). The flags are
|
|
|
|
|
lipis' flag-icons, drawn by the `flag` package, under the MIT licence
|
halkyon_app: data locations as precise as a country or a state
An operator's DATA_LOCATION and BACKUP_LOCATION may name a subdivision
(ISO 3166-2, US-CA, DE-BY) besides the EU or a country. The app adds
"(EU)" to a member country and the country to a subdivision ("Germany
(EU)", "California, United States", "Bavaria, Germany (EU)"), in
English and German, and shows the most specific flag there is: a
subdivision's own where the flag package has one, else its country's.
Subdivision names come from Unicode CLDR 48.2 (tool/subdivision_names.py),
bundled with their licence, which the app registers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-19 18:47:25 +02:00
|
|
|
(`assets/flags/LICENSE-flag-icons.txt`). The subdivisions' names are Unicode
|
|
|
|
|
CLDR's (release 48.2), under the Unicode License v3
|
|
|
|
|
(`assets/subdivisions/LICENSE-CLDR.txt`, `assets/subdivisions/ATTRIBUTION.md`).
|