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
// ignore: unused_import
import ' package:intl/intl.dart ' as intl ;
import ' halkyon_app_localizations.dart ' ;
// ignore_for_file: type=lint
/// The translations for German (`de`).
class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
HalkyonAppLocalizationsDe ( [ String locale = ' de ' ] ) : super ( locale ) ;
@ override
String get tryAgain = > ' Erneut versuchen ' ;
@ override
String instanceWelcome ( String app ) {
return ' Willkommen bei $ app ' ;
}
@ override
String get instanceBuiltInTagline = > ' In der App voreingestellt ' ;
@ override
String get instanceAnother = > ' Andere Instanz ' ;
@ override
String get instanceAnotherTagline = > ' Gib ihre Adresse ein ' ;
@ override
String get instanceAddress = > ' Adresse der Instanz ' ;
@ override
String get instanceAddressInvalid = > ' Das ist keine Adresse. ' ;
fix(app)!: instances over plain HTTP are refused, but on this machine
What the app sends an instance (the proofs that sign in, the keys'
wrappings, every request a session carries) must not be read or changed on
the way, so an instance is reached over HTTPS, or over plain HTTP only on
this machine (localhost, 127.0.0.0/8, ::1), as Jellyfin servers are. An
address over plain HTTP elsewhere is refused before anything is sent there.
One kept from before is never opened: the app says why, and offers another
instance or removing it from the device, never dropping it silently.
Development builds still use their own instance over HTTP (an emulator
reaches the developer's machine as 10.0.2.2); releases never.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 14:35:21 +02:00
@ override
String instanceAddressInsecure ( String app ) {
return ' $ app erreicht Instanzen nur über HTTPS, damit unterwegs niemand mitliest oder verändert, was es sendet. Frag nach der HTTPS-Adresse der Instanz. ' ;
}
@ override
String instanceInsecure ( String name , String app ) {
return ' $ name wird über einfaches HTTP erreicht, wo unterwegs mitgelesen und verändert werden könnte, was $ app sendet. Deshalb öffnet $ app es nicht mehr. Das Konto bleibt auf $ name und auf diesem Gerät. Nutze eine andere Instanz oder entferne diese von diesem Gerät. ' ;
}
@ override
String get instanceInsecureUseAnother = > ' Andere Instanz nutzen ' ;
@ override
String get instanceInsecureRemove = > ' Von diesem Gerät entfernen ' ;
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
@ override
String get instanceAddressRequired = > ' Gib die Adresse einer Instanz ein. ' ;
@ override
String get instanceContinue = > ' Weiter ' ;
@ override
String instanceUnreachable ( String app , String name ) {
return ' Unter $ name antwortet keine Instanz von $ app . ' ;
}
@ override
String instanceUseBuiltIn ( String name ) {
return ' $ name verwenden ' ;
}
@ override
String instanceOpenFailed ( String name ) {
return ' Die App auf $ name konnte nicht geöffnet werden. ' ;
}
@ override
String instanceDataAndBackupsIn ( String place ) {
return ' Daten und Sicherungen: $ place ' ;
}
@ override
String instanceDataIn ( String place ) {
return ' Daten: $ place ' ;
}
@ override
String instanceBackupsIn ( String place ) {
return ' Sicherungen: $ place ' ;
}
@ override
String get instanceNoBackups = > ' Keine Sicherungen ' ;
@ override
String get instanceEuropeanUnion = > ' Europäische Union ' ;
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
@ override
String instanceCountryInEu ( String country ) {
return ' $ country (EU) ' ;
}
@ override
String instanceSubdivisionOf ( String subdivision , String country ) {
return ' $ subdivision , $ country ' ;
}
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
@ override
String get accountsTitle = > ' Konten ' ;
@ override
String get accountAdd = > ' Konto hinzufügen ' ;
@ override
String get accountAddSubtitle = >
' Wähle die Instanz für das Konto, das du hinzufügst. ' ;
@ override
String get accountSignedIn = > ' Angemeldet ' ;
@ override
String get accountAnonymous = > ' Anonym ' ;
@ override
String get accountRemoveTooltip = > ' Konto entfernen ' ;
@ override
String get accountRemoveTitle = > ' Konto entfernen? ' ;
@ override
String accountRemoveMessage ( String name ) {
return ' Entfernt das Konto auf $ name von diesem Gerät, mit allem, was das Gerät davon hat. Was es synchronisiert hat, bleibt auf $ name : Melde dich dort wieder an, um es wiederzuhaben. In ein anonymes Konto kann man sich nicht wieder anmelden. ' ;
}
@ override
String get accountRemoveConfirm = > ' Entfernen ' ;
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
@ override
String accountSignedInOn ( String instance ) {
return ' Angemeldet auf $ instance ' ;
}
@ override
String accountAnonymousOn ( String instance ) {
return ' Anonym auf $ instance ' ;
}
@ override
String get accountSwitch = > ' Konto wechseln ' ;
@ override
String get accountSignOut = > ' Abmelden ' ;
@ override
String get accountSignOutTitle = > ' Abmelden? ' ;
@ override
String get accountDelete = > ' Konto löschen ' ;
@ override
String get accountDeleteTitle = > ' Dein Konto löschen? ' ;
2026-10-21 19:49:18 +02:00
@ override
String get accountSignOutFailed = >
' Abmelden auf diesem Gerät hat nicht geklappt. Versuch es noch einmal. ' ;
@ override
String get accountDeleteFailed = >
' Dein Konto wurde nicht gelöscht: Die Instanz war nicht erreichbar. Versuch es noch einmal. ' ;
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
@ override
String get aboutSourceCode = >
' Der Quellcode dessen, was diese Instanz ausführt: ' ;
@ override
String get aboutSourceUnreachable = >
' Diese Instanz war nicht erreichbar, um nach ihrem Quellcode zu fragen. ' ;
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
@ override
String get sessionUnreachable = > ' Der Server war nicht erreichbar. ' ;
@ override
String get personDataFailed = >
' Die Daten dieses Geräts konnten nicht geöffnet werden. ' ;
@ override
String get startSignedOut = > ' Abgemeldet ' ;
@ override
String startOn ( String instance ) {
return ' Starte auf $ instance : ' ;
}
@ override
String get startWithGitHub = > ' Mit GitHub anmelden ' ;
@ override
String get startWithoutAccount = > ' Ohne Konto starten ' ;
@ override
String get startAnonymous = >
' Dein Fortschritt wird auf diesem Gerät gespeichert. Melde dich an, um ihn zu behalten. ' ;
@ override
String get startGitHubFailed = > ' Anmeldung mit GitHub fehlgeschlagen. ' ;
@ override
String get startNoGitHubAccount = >
2026-10-19 21:32:14 +02:00
' Noch kein Konto hier meldet sich mit diesem GitHub-Konto 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
fix(app): GitHub's refusals say why, and no hint adds GitHub where passwords are offered
Where an instance offers password sign-in, GitHub is added to nobody (ADR
0017, D2), yet the start screen still said "then add GitHub" after a GitHub
account nobody has, and every refusal but an unknown account read
"Couldn't sign in with GitHub." (the end-to-end test of 21 Oct).
- The gateway tells from one read of the basics whether GitHub signs in and
whether it is added to people (`gitHubOffer`, in place of
`gitHubClientId`); the GitHub bloc keeps it (`Ready.linking`) for the
profile, with why an attempt failed (`Ready.problem`).
- The instance's `usePassword`, `deprecated` and `ended` become problems of
their own (`gitHubSignInProblemOf`), worded by `gitHubProblemText` and on
the start screen; an anonymous person refused a link is told the GitHub
account is nobody's and offered the password instead.
- Where passwords are offered, the start screen offers an account to create
in place of the app's hint on adding GitHub.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 19:19:28 +02:00
@ override
String get gitHubCreateAccountInstead = >
' Erstelle stattdessen ein Konto mit deiner E-Mail-Adresse und einem Passwort. ' ;
@ override
String get gitHubNoAccountSetUpPassword = >
' Noch kein Konto hier meldet sich mit diesem GitHub-Konto an. Richte stattdessen ein Passwort ein, um deinen Fortschritt zu behalten. ' ;
@ override
String get gitHubUsePassword = >
' Dieses Konto meldet sich jetzt mit seiner E-Mail-Adresse und seinem Passwort an, nicht mehr mit GitHub. ' ;
@ override
String get gitHubDeprecated = >
' GitHub lässt sich nicht mehr als Anmeldeweg hinzufügen: Deine E-Mail-Adresse und dein Passwort sind es. ' ;
@ override
String get gitHubEnded = >
' Die Anmeldung mit GitHub ist auf dieser Instanz beendet. ' ;
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
@ override
String get startOtherInstance = > ' Andere Instanz verwenden ' ;
@ override
String get syncTitle = > ' Synchronisierung ' ;
@ override
String get syncNow = > ' Jetzt synchronisieren ' ;
@ override
String get syncStateSyncing = > ' Wird synchronisiert… ' ;
@ override
String get syncStateSynced = > ' Alles synchronisiert ' ;
@ override
String get syncStateFailing = > ' Synchronisierung schlägt fehl ' ;
@ override
String get syncStateNotYet = > ' Noch nicht synchronisiert ' ;
@ override
String syncStateMismatch ( String server ) {
return ' Diese App kann nicht mit $ server synchronisieren ' ;
}
@ override
String get syncMismatchDetail = >
' Aktualisiere die App. Ist sie schon aktuell, muss der Server aktualisiert werden. Was du bis dahin tust, bleibt auf diesem Gerät und wird danach synchronisiert. ' ;
@ override
String syncLastCompleted ( String time ) {
return ' Zuletzt abgeschlossen um $ time ' ;
}
@ override
String syncLastFailed ( String time ) {
return ' Letzter Versuch fehlgeschlagen um $ time ' ;
}
@ override
String get syncNoRunYet = >
' Seit dem Start der App wurde noch keine Synchronisierung abgeschlossen ' ;
@ override
String syncPending ( int count ) {
String _temp0 = intl . Intl . pluralLogic (
count ,
locale: localeName ,
other: ' $ count Änderungen warten auf die Synchronisierung ' ,
one: ' 1 Änderung wartet auf die Synchronisierung ' ,
) ;
return ' $ _temp0 ' ;
}
@ override
String get syncTroubleLabel = > ' Synchronisierung benötigt Aufmerksamkeit ' ;
@ override
String syncMismatchBanner ( String server ) {
return ' Diese App kann nicht mehr mit $ server synchronisieren. Aktualisiere sie, um weiter zu synchronisieren. ' ;
}
@ override
String get syncMismatchMore = > ' Mehr ' ;
feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
@ override
String get startSignIn = > ' Anmelden ' ;
@ override
String get startCreateAccount = > ' Konto erstellen ' ;
@ override
String startGitHubEnds ( String date ) {
return ' Die Anmeldung mit GitHub endet am $ date . ' ;
}
@ override
String get passwordAddress = > ' E-Mail-Adresse ' ;
@ override
String get passwordPassword = > ' Passwort ' ;
@ override
String get passwordNewPassword = > ' Neues Passwort ' ;
@ override
String get passwordCurrent = > ' Aktuelles Passwort ' ;
@ override
String get passwordRepeat = > ' Neues Passwort wiederholen ' ;
@ override
String get passwordsDiffer = > ' Die beiden Passwörter stimmen nicht überein. ' ;
@ override
String get passwordShow = > ' Passwort anzeigen ' ;
@ override
String get passwordHide = > ' Passwort verbergen ' ;
@ override
String get passwordContinue = > ' Weiter ' ;
@ override
String get passwordBack = > ' Zurück ' ;
@ override
String get passwordDone = > ' Fertig ' ;
@ override
String get passwordSave = > ' Als Datei speichern ' ;
@ override
String get passwordQrCode = > ' QR-Code für deine Authenticator-App ' ;
@ override
String get passwordMailCode = > ' Code aus der E-Mail ' ;
@ override
String passwordMailCodeSent ( String address ) {
return ' Wir haben dir einen Code an $ address geschickt. Er gilt 15 Minuten. ' ;
}
@ override
String get passwordResendCode = > ' Neuen Code senden ' ;
@ override
String get passwordTotpCode = > ' Code aus deiner Authenticator-App ' ;
@ override
String get passwordRecoveryCode = > ' Wiederherstellungscode ' ;
@ override
String get passwordUseRecoveryCode = >
' Stattdessen einen Wiederherstellungscode verwenden ' ;
@ override
String get passwordUseTotpCode = >
' Stattdessen deine Authenticator-App verwenden ' ;
@ override
String get passwordUnlocking = >
' Dein Passwort wird auf diesem Gerät entsperrt … ' ;
@ override
String get passwordMakingKeys = >
' Deine Schlüssel werden auf diesem Gerät erstellt … ' ;
@ override
String get passwordSignInTitle = > ' Anmelden ' ;
@ override
String get passwordSignInAction = > ' Anmelden ' ;
@ override
String passwordSignInIntro ( String instance ) {
return ' Mit der E-Mail-Adresse und dem Passwort deines Kontos auf $ instance . ' ;
}
@ override
String passwordSecondFactorIntro ( String instance ) {
return ' Gib den Code ein, den deine Authenticator-App für $ instance zeigt. ' ;
}
@ override
String get passwordRecoveryCodeIntro = >
' Gib einen deiner Wiederherstellungscodes ein. Jeder meldet einmal an. ' ;
@ override
String get passwordForgot = > ' Passwort vergessen? ' ;
@ override
String get passwordOtherPassword = > ' Adresse oder Passwort ändern ' ;
@ override
String get passwordLostAuthenticator = > ' Authenticator verloren? ' ;
@ override
String get passwordLostAuthenticatorTitle = > ' Authenticator verloren ' ;
@ override
String get passwordLostAuthenticatorIntro = >
' Mit einem Wiederherstellungscode meldest du dich wie gewohnt an. Ohne einen kannst du deine Authenticators zurücksetzen lassen: Das wirkt 72 Stunden später, und eine Anmeldung mit einem Code stoppt es bis dahin. ' ;
@ override
String get passwordResetWithMail = > ' Mit einem Code per E-Mail zurücksetzen ' ;
@ override
String passwordResetWithMailHint ( String address ) {
return ' Wir schicken einen Code an $ address . ' ;
}
@ override
String get passwordResetWithPhrase = >
' Mit deiner Wiederherstellungsphrase zurücksetzen ' ;
@ override
String get passwordResetWithPhraseHint = >
' Die 24 Wörter, die du dir notiert hast. ' ;
@ override
String get passwordSignInWithoutCode = > ' Ohne Code anmelden ' ;
@ override
String get passwordSignInWithoutCodeHint = >
' Sobald ein Zurücksetzen, um das du gebeten hast, wirksam ist. ' ;
@ override
String get passwordResetAction = > ' Zurücksetzen ' ;
@ override
String passwordAuthenticatorResetAt ( String date ) {
return ' Deine Authenticators werden am $ date zurückgesetzt. Wir haben dir eine E-Mail geschickt: Warst du es nicht, melde dich vorher mit einem Code an, um es zu stoppen. Danach meldest du dich nur mit deinem Passwort an und richtest einen neuen Authenticator ein. ' ;
}
@ override
String passwordUnfinishedIntro ( String address ) {
return ' Dein Konto wurde nie fertig eingerichtet. Wir haben dir einen Code an $ address geschickt: Damit meldest du dich an und richtest einen Authenticator ein. ' ;
}
@ override
String get passwordPhraseField = > ' Wiederherstellungsphrase ' ;
@ override
String get passwordPhraseFieldHint = >
' Die 24 Wörter, durch Leerzeichen getrennt ' ;
@ override
String get passwordPhraseFieldIntro = >
' Gib deine Wiederherstellungsphrase ein: Sie beweist, dass das Konto deines ist. ' ;
@ override
String get passwordAuthenticatorTitle = > ' Authenticator einrichten ' ;
@ override
String get passwordAuthenticatorIntro = >
' Jede Anmeldung braucht einen Code aus einer Authenticator-App wie Aegis, 2FAS oder Proton Authenticator. Scanne diesen Code damit oder tippe den Schlüssel ein. ' ;
@ override
String get passwordAuthenticatorKey = > ' Schlüssel ' ;
@ override
String get passwordAuthenticatorOpen = > ' In deiner Authenticator-App öffnen ' ;
@ override
String get passwordAuthenticatorNoApp = >
' Keine App auf diesem Gerät öffnet ihn. Scanne den Code oder tippe den Schlüssel ein. ' ;
@ override
String get passwordAuthenticatorCodeIntro = >
' Gib dann den Code ein, den sie zeigt. ' ;
2026-10-21 10:42:55 +02:00
@ override
String get passwordAuthenticatorOwnerIntro = >
' Bestätige zum Hinzufügen, dass du es bist: mit deinem Passwort und einem Code von einem bisherigen Authenticator. ' ;
feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
@ override
String get passwordExistingCode = > ' Code von einem bisherigen Authenticator ' ;
@ override
String get passwordRecoveryCodesTitle = > ' Wiederherstellungscodes ' ;
@ override
String get passwordRecoveryCodesIntro = >
' Jeder meldet dich einmal anstelle eines Codes aus deinem Authenticator an, falls du ihn verlierst. Bewahre sie auf, wo nur du sie findest: Sie werden nur jetzt angezeigt. ' ;
@ override
String get passwordCodesKept = > ' Ich habe sie aufbewahrt ' ;
@ override
String get passwordPhraseTitle = > ' Deine Wiederherstellungsphrase ' ;
@ override
String get passwordPhraseIntro = >
' Diese 24 Wörter öffnen die Schlüssel deines Kontos, falls du dein Passwort vergisst: Ohne sie ist Verschlüsseltes verloren. Schreib sie der Reihe nach auf und bewahre sie auf, wo nur du sie findest. Sie werden nur jetzt angezeigt. ' ;
@ override
String get passwordPhraseWrittenDown = > ' Ich habe sie aufgeschrieben ' ;
@ override
String get passwordPhraseCheckTitle = > ' Wiederherstellungsphrase prüfen ' ;
@ override
String get passwordPhraseCheckIntro = >
' Tippe die gefragten Wörter aus deiner Notiz ein. ' ;
@ override
String passwordPhraseWord ( int number ) {
return ' Wort $ number ' ;
}
@ override
String get passwordPhraseWrong = >
' Das sind nicht die Wörter. Sieh noch einmal in deine Notiz. ' ;
@ override
String get passwordPhraseShowAgain = > ' Phrase noch einmal zeigen ' ;
@ override
String get passwordCreateTitle = > ' Konto erstellen ' ;
@ override
String passwordCreateIntro ( String instance ) {
return ' Auf $ instance , mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App für den Code, den jede Anmeldung braucht. ' ;
}
@ override
String get passwordSetUpTitle = > ' Passwort einrichten ' ;
@ override
String get passwordSetUpIntro = >
' Melde dich mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App an, auf diesem Gerät und anderen. Alles, was du hast, bleibt. ' ;
@ override
String get passwordChooseIntro = >
' Wähle ein Passwort. Es verlässt dieses Gerät nie: Die Instanz kann es nicht lesen und nicht zurücksetzen, ohne dass verloren geht, was es schützt. ' ;
@ override
String get passwordFinishResetTitle = > ' Zurücksetzen abschließen ' ;
@ override
String get passwordFinishResetIntro = >
' 72 Stunden nachdem du um das Zurücksetzen deines Kontos gebeten hast, schließt du es hier mit einem neuen Code per E-Mail ab. ' ;
@ override
String get passwordFinishResetHavePhrase = >
' Ich habe meine Wiederherstellungsphrase ' ;
@ override
String get passwordFinishResetKeepsKeys = >
' Dein Konto behält seine Schlüssel: Nichts Verschlüsseltes geht verloren. ' ;
@ override
String get passwordFinishResetNewKeys = >
' Ohne deine Wiederherstellungsphrase bekommt dein Konto neue Schlüssel: Was mit den alten verschlüsselt war, ist verloren. ' ;
@ override
String get passwordRecoveryTitle = > ' Zurück in dein Konto ' ;
@ override
String get passwordRecoveryWithPhrase = >
' Ich habe meine Wiederherstellungsphrase ' ;
@ override
String get passwordRecoveryWithPhraseHint = >
' Setze sofort ein neues Passwort. Alles bleibt. ' ;
@ override
String get passwordRecoveryWithPhraseIntro = >
' Deine Wiederherstellungsphrase öffnet die Schlüssel deines Kontos, die dann dein neues Passwort schützt. Ein Code aus deinem Authenticator oder ein Wiederherstellungscode bestätigt es. ' ;
@ override
String get passwordRecoveryNoCodeHint = >
' Auch deinen Authenticator verloren? Setze dein Konto 72 Stunden später mit deiner Wiederherstellungsphrase zurück: Sie behält alles. ' ;
@ override
String get passwordRecoveryWithoutPhrase = > ' Mein Konto zurücksetzen ' ;
@ override
String get passwordRecoveryWithoutPhraseHint = >
' Wirkt 72 Stunden später. Ohne deine Wiederherstellungsphrase ist Verschlüsseltes verloren. ' ;
@ override
String get passwordRecoveryStartIntro = >
' Wir schicken einen Code an deine Adresse. Dein Konto kann dann 72 Stunden später zurückgesetzt werden; eine Anmeldung mit deinem Passwort stoppt es bis dahin. ' ;
@ override
String get passwordRecoveryFinish = > ' Zurücksetzen abschließen ' ;
@ override
String get passwordRecoveryFinishHint = >
' 72 Stunden nachdem du darum gebeten hast. ' ;
@ override
String passwordResetStarted ( String date ) {
return ' Dein Konto kann ab dem $ date zurückgesetzt werden. Wir haben dir eine E-Mail geschickt: Warst du es nicht, melde dich vorher mit deinem Passwort an, um es zu stoppen. ' ;
}
@ override
String get passwordSectionTitle = > ' Passwort und Authenticator ' ;
@ override
String get passwordSectionFailed = >
' Wie dein Konto sich anmeldet, konnte nicht gelesen werden. ' ;
@ override
String passwordSectionGitHubEnds ( String date ) {
return ' Die Anmeldung mit GitHub endet am $ date . Richte bis dahin ein Passwort ein, um dein Konto zu behalten. ' ;
}
@ override
String get passwordSectionSetUp = > ' Passwort einrichten ' ;
@ override
String get passwordSectionSetUpHint = >
' Melde dich mit deiner E-Mail-Adresse, einem Passwort und einer Authenticator-App an. ' ;
@ override
String get passwordSectionUnfinished = >
' Fertig einrichten: Authenticator hinzufügen ' ;
@ override
String get passwordSectionUnfinishedHint = >
' Dein Passwort meldet dich an, sobald ein Authenticator eingerichtet ist. ' ;
@ override
String get passwordSectionChange = > ' Passwort ändern ' ;
@ override
String get passwordSectionKeysMissing = >
' Melde dich auf diesem Gerät mit deinem Passwort an, um das zu ändern. ' ;
@ override
String get passwordSectionNotConfirmed = > ' Noch nicht eingerichtet ' ;
@ override
String passwordSectionRemove ( String name ) {
return ' $ name entfernen? ' ;
}
@ override
String get passwordSectionRemoveIntro = >
2026-10-21 10:42:55 +02:00
' Gib zur Bestätigung dein Passwort und einen Code aus einem deiner Authenticators ein. ' ;
feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
@ override
String get passwordSectionRemoveAction = > ' Entfernen ' ;
@ override
String get passwordSectionAddAuthenticator = > ' Authenticator hinzufügen ' ;
@ override
String get passwordSectionRecoveryCodes = > ' Neue Wiederherstellungscodes ' ;
@ override
String get passwordSectionRecoveryCodesHint = >
' Die alten gelten dann nicht mehr. ' ;
@ override
String get passwordSectionNewPhrase = > ' Neue Wiederherstellungsphrase ' ;
@ override
String get passwordSectionPhraseHint = >
' Deine alte Phrase gilt dann nicht mehr. ' ;
@ override
String get passwordChangeTitle = > ' Passwort ändern ' ;
@ override
String get passwordChangeIntro = >
' Deine anderen Geräte werden abgemeldet. Deine Schlüssel bleiben: Nichts geht verloren. ' ;
@ override
String get passwordChangeAction = > ' Passwort ändern ' ;
@ override
String get passwordChanged = >
' Passwort geändert. Deine anderen Geräte sind abgemeldet. ' ;
@ override
String get passwordNewCodesIntro = >
2026-10-21 10:42:55 +02:00
' Gib dein Passwort und einen Code aus deiner Authenticator-App ein. Deine alten Wiederherstellungscodes gelten dann nicht mehr. ' ;
feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
@ override
String get passwordNewPhraseIntro = >
' Deine neue Wiederherstellungsphrase. Schreib die 24 Wörter der Reihe nach auf: Sobald du sie bestätigst, öffnet deine alte Phrase nichts mehr. ' ;
@ override
String get passwordPhraseKept = >
' Neue Wiederherstellungsphrase gesetzt. Die alte gilt nicht mehr. ' ;
@ override
String get passwordWrongCredentials = >
' Die Adresse, das Passwort oder der Code stimmt nicht. ' ;
@ override
String get passwordTooManyAttempts = >
' Zu viele Versuche. Versuch es in einer Stunde wieder. ' ;
@ override
String get passwordSetupIncomplete = >
' Dein Konto wurde nie fertig eingerichtet: Richte einen Authenticator ein. ' ;
@ override
String passwordNotDue ( String date ) {
return ' Das wirkt am $ date . ' ;
}
@ override
String get passwordNotDueYet = > ' Das wirkt noch nicht. ' ;
@ override
String get passwordNoReset = > ' Für diese Adresse wartet kein Zurücksetzen. ' ;
@ override
String get passwordExists = >
' Das Konto dieser Adresse hat schon ein Passwort. Melde dich damit an oder komm zurück in dein Konto, falls du es vergessen hast. ' ;
@ override
String get passwordAddressTaken = >
' Diese Adresse gehört zu einem anderen Konto. ' ;
@ override
String get passwordNoAddress = > ' Dein Konto hat noch keine E-Mail-Adresse. ' ;
@ override
String get passwordNoPassword = > ' Dieses Konto hat kein Passwort. ' ;
@ override
String get passwordWrongCode = >
' Dieser Code stimmt nicht. Gib den ein, den deine App gerade zeigt. ' ;
@ override
String get passwordLastAuthenticator = >
' Dein letzter Authenticator kann nicht weg: Jede Anmeldung braucht einen. ' ;
@ override
String get passwordTooManyAuthenticators = >
' Dein Konto hat so viele Authenticators, wie es darf. Entferne zuerst einen. ' ;
@ override
String get passwordUnknownAuthenticator = > ' Dieser Authenticator ist weg. ' ;
@ override
String get passwordInvalidSetup = >
' Die Instanz hat nicht angenommen, was dieses Gerät erstellt hat. Aktualisiere die App oder versuch es noch einmal. ' ;
@ override
String get passwordChallengeExpired = >
' Das hat zu lange gedauert. Versuch es noch einmal. ' ;
@ override
String get passwordUnavailable = >
' Diese Instanz bietet keine Anmeldung mit Passwort. ' ;
@ override
String get passwordNotAnAddress = > ' Das ist keine E-Mail-Adresse. ' ;
@ override
String get passwordTooManyCodes = >
' Zu viele Codes angefordert. Versuch es in einer Stunde wieder. ' ;
@ override
String get passwordWrongMailCode = > ' Dieser Code stimmt nicht. ' ;
@ override
String get passwordTooManyMailTries = >
' Zu viele falsche Codes. Fordere einen neuen an. ' ;
@ override
String get passwordMailCodeExpired = >
' Dieser Code gilt nicht mehr. Fordere einen neuen an. ' ;
@ override
String get passwordMailFailed = >
' Die Instanz konnte die E-Mail nicht senden. Versuch es später noch einmal. ' ;
@ override
String get passwordOtherAddressLinked = >
' Dein Konto hat eine andere E-Mail-Adresse. ' ;
@ override
String get passwordNotAPhrase = >
' Das ist keine Wiederherstellungsphrase: Prüf jedes Wort und ihre Anzahl. ' ;
@ override
String get passwordPhraseDoesNotFit = >
' Die Adresse oder die Wiederherstellungsphrase stimmt nicht. ' ;
@ override
String get passwordUnusable = >
' Dieses Passwort geht nicht: Es ist leer oder enthält Zeichen, die kein Text sind. ' ;
@ override
String get passwordInstanceUnproven = >
' Die Instanz konnte nicht beweisen, dass sie dein Passwort kennt, daher vertraut dieses Gerät ihr nicht. Versuch es noch einmal; passiert es wieder, ist die Instanz nicht die, die sie zu sein behauptet. ' ;
@ override
String get passwordKeysMissing = >
' Die Schlüssel deines Kontos sind nicht auf diesem Gerät. Melde dich hier ab und mit deinem Passwort wieder an. ' ;
2026-10-20 20:04:37 +02:00
@ override
String get passwordUnsafeCosts = >
' Die Instanz wollte dein Passwort auf eine Weise prüfen lassen, die kein Konto nutzt, daher wurde nichts gesendet. Passiert es wieder, ist die Instanz nicht die, die sie zu sein behauptet. ' ;
feat(app): sign in with a password and TOTP on the device
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 15:42:45 +02:00
@ override
String get passwordFailed = >
' Die Instanz war nicht erreichbar. Prüf die Verbindung und versuch es noch einmal. ' ;
2026-10-20 21:25:47 +02:00
@ override
String passwordMinLength ( int count ) {
return ' Mindestens $ count Zeichen ' ;
}
@ override
String passwordTooShort ( int count ) {
return ' Ein neues Passwort braucht mindestens $ count Zeichen: Sollte die Datenbank der Instanz je nach außen gelangen, wird ein kurzes zuerst erraten. ' ;
}
fix(auth): codes wait after ten wrong in a row, and the owner is told
Whoever knew the password could guess codes at 10 an hour forever,
unnoticed, each guess checked against every authenticator (the second
reading's M4). Every code an account's second factor refuses after a
right password (sign-in, a phrase's reset, a change) or from a signed-in
device (removing an authenticator, new recovery codes, the code from one
confirmed before) now counts, per account, from every network; after
every tenth in a row no code is taken (codesWait, saying until when), an
hour, then twice as long each time, a day at the most (longestCodeWait,
second_factor_waits.dart): at a day, ten guesses a day still have about a
1 % chance a year against one authenticator, so the first wait mails the
owner that someone knows their password. The instance keeps no address,
so sign-in and the phrase's reset now send the address the device typed
(and its language), used only for that mail after its hash is checked
against the account's. A code accepted clears it all.
Migration (module, Learn's and Todo's servers, Learn's device schema):
CREATE TABLE "second_factor_failures" ("id" bigserial PRIMARY KEY,
"authUserId" uuid NOT NULL, "failures" bigint NOT NULL, "waits"
bigint NOT NULL, "waitUntil" timestamp, "updatedAt" timestamp NOT
NULL) with a unique index on "authUserId": a new table, nothing
touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 21:47:38 +02:00
@ override
String passwordCodesWait ( String date ) {
return ' Zu viele falsche Codes hintereinander: Bis $ date wird kein Code angenommen. Waren es nicht deine, kennt jemand dein Passwort: Ändere es dann. ' ;
}
2026-10-20 21:59:42 +02:00
@ override
String get passwordDeleteIntro = >
' Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Gib zur Bestätigung dein Passwort und einen Code deiner Authenticator-App ein. ' ;
fix(auth): a session's wrong codes end the session, not the account's codes
Wrong codes for removing an authenticator, new recovery codes or confirming
one counted toward the account's waits (the re-review's third finding): a
stolen session alone, without the password, could keep the owner's codes
waiting for up to a day at a time, renewed without end, holding them out
of signing in and of changing the password, the way to end that session,
and nobody was mailed. Those codes now count per session, where the session
is (the register's own, or a member's, which forwards the code): each try
counted before its code is checked (one statement, so tries sent at once
count each), a code accepted clearing the count, and the tenth wrong in a
row ending the session (sessionEnded; the device says it was signed out).
A try that checked no code (a refusal of something else) is taken back.
The account's waits now count only codes after a right password, and the
per-account ten tries an hour, which a session could fill as well, go.
Migration (module, Learn's and Todo's servers, Learn's device schema):
CREATE TABLE "session_code_failures" ("id" bigserial PRIMARY KEY,
"sessionId" uuid NOT NULL, "authUserId" uuid NOT NULL, "failures"
bigint NOT NULL, "updatedAt" timestamp NOT NULL) with a unique index on
"sessionId": a new table, nothing touched. Its rows go with a code
accepted, with the person, and hourly once their session is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 22:53:20 +02:00
@ override
String get passwordSessionEnded = >
' Zu viele falsche Codes hintereinander von diesem Gerät: Es wurde abgemeldet. Melde dich wieder mit deinem Passwort und einem Code an. ' ;
fix(auth): every call with a code after the password takes the address
Whoever guessed codes decided whether the owner was mailed (the re-review's
fourth finding): the address a sign-in sent was optional, so a guesser left
it out and no mail went, and a wait begun without a mail used up the one
mail there was. Every call carrying a code after the password or the phrase
proved (signing in, the phrase's reset, a change of password, a new phrase,
deletion) now takes the account's address and the language of its mails,
and is refused (wrongCredentials, as anything wrong after the password)
before any code is checked when the address isn't the account's; every
wait mails the owner, not only the first. The app sends the address the
person typed, or for changes while signed in the one this device signed in
with, and says so where it doesn't know it (addressUnknown).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-20 23:08:25 +02:00
@ override
String get passwordAddressUnknown = >
2026-10-21 10:42:55 +02:00
' Gib die E-Mail-Adresse deines Kontos ein: Dorthin geht der Code. ' ;
2026-10-20 23:30:22 +02:00
@ override
String get passwordDeleteIntroByMail = >
2026-10-21 10:42:55 +02:00
' Dein Konto wird mit allem darin gelöscht, in jedem Dienst dieser Instanz. Keiner seiner Authenticators funktioniert gerade, daher bestätigt es ein Code per E-Mail: Gib dein Passwort ein, dann den Code, den wir an deine Adresse schicken. ' ;
2026-10-20 23:30:22 +02:00
@ override
String get passwordDeleteSendCode = > ' Code per E-Mail schicken ' ;
2026-10-20 23:32:49 +02:00
fix(auth)!: a first password takes a fresh proof of the account's way in
The fourth reading's first finding: password.set took a session alone,
so whoever took one made a GitHub-only account, or an address account
without a password, theirs for good (their address or password, their
authenticator; GitHub then refused the owner).
- An account with an address proves it anew: a code mailed to that
address for this (EmailCodePurpose.firstPassword, its own mail), never
the session alone, a code asked for anything else, or another address.
- An account that signs in with GitHub alone does it within 10 minutes of
a GitHub sign-in (the session's method and creation time, where the
session lives; members tell the register), refused signInAgain before
any code is used; the address it adds is proved as before.
- Anonymous people still become identified with a code to the address
they add.
The app mails the code to the person's own address (asking for it where
the device doesn't know it), offers a password right after a GitHub
sign-in (D2's "at their next sign-in") before the person's app, and asks
for a GitHub sign-in anew when the instance refuses an older one, keeping
the code typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 11:31:22 +02:00
@ override
String get passwordSignInAgain = >
' Um einem Konto, das sich mit GitHub anmeldet, ein Passwort zu geben, melde dich erst wieder mit GitHub an: Es ist mehr als ein paar Minuten her. ' ;
@ override
String get passwordSignInWithGitHubAgain = > ' Wieder mit GitHub anmelden ' ;
@ override
String get passwordOfferTitle = > ' Richte ein Passwort ein ' ;
@ override
String get passwordOfferBody = >
' Die Anmeldung mit GitHub endet. Richte ein Passwort und eine Authenticator-App ein, um dein Konto zu behalten. ' ;
@ override
String get passwordOfferLater = > ' Später ' ;
fix(auth)!: the first authenticator takes the password, and a reset ends every session
The fourth reading's second finding: a stolen session outlived the reset
of the authenticators (carrying it out ended no session) and confirmed
the account's first authenticator on its own code alone, ahead of the
owner, so the owner's confirmation and sign-in were refused after.
- Carrying out a reset of the authenticators ends every session of the
account, on the register at once and on every member by notice, as the
other resets do; the sign-in that carries it out gets its session after
that moment (PasswordProof.sessionsEndedBefore, issueSessionAfter).
- Confirming the first authenticator takes the password's proof for a
change's challenge, without a code (proveOwnersPassword), counted per
session as the owner's proofs are: a session of a lost phone, or of a
setup never finished, claims no second factor.
The app sends the password with every confirmation: the sign-up's, a
reset's and the account section's (which asks for it), and a sign-in's
own unlock where the session is still held back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 11:43:22 +02:00
@ override
String get passwordAuthenticatorPasswordIntro = >
' Bestätige zum Hinzufügen mit deinem Passwort, dass du es bist. ' ;
2026-10-21 12:10:38 +02:00
@ override
String get passwordWarningTitle = > ' Jemand hat dein Passwort ausprobiert ' ;
@ override
String passwordWarningBody ( String date ) {
return ' Am $ date hat ein bei deinem Konto angemeldetes Gerät zehnmal hintereinander ein falsches Passwort oder einen falschen Code gesendet und wurde abgemeldet. Warst du das nicht, hat vielleicht jemand anderes dieses Gerät oder seine Sitzung: Ändere dein Passwort. ' ;
}
@ override
String get passwordWarningDismiss = > ' Ausblenden ' ;
@ override
String get passwordWarningLater = > ' Später erinnern ' ;
fix(auth): a reset waits while its owner is held out, and devices show it
The full reading's H1: an owner could be kept from calling a reset off, and
the reset then went through. Whoever knew the password and had the mailbox
asked an authenticators' reset and kept the codes waiting, so the owner's
sign-ins were refused until it took effect; whoever had the mailbox filled
the address's proofs ceiling from three networks and completed an account
reset. Now every hold-out of the owner after a reset was asked (a wait
that begins, an attempt a wait refuses, a sign-in or proof refused by the
address's ceiling) moves the account's resets not carried out to take
effect no sooner than 72 hours after it ends (pushOutResets). A reset
carried out (the password alone signing in until an authenticator is
confirmed) is never pushed, nor a lapsed one brought back.
Signed-in devices are told too: SignInWays carries when a waiting reset
of each kind takes effect, the account section shows them with Call it
off, which takes the owner's proof (password and code; password.
callOffResets, forwarded by members) and calls every reset off, as no wait
holds a signed-in owner's proof back. Texts in English and German.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 13:17:30 +02:00
@ override
String get passwordResetsTitle = > ' Ein Zurücksetzen steht an ' ;
@ override
String passwordResetsAuthenticator ( String date ) {
return ' Am $ date werden deine Authenticator-Apps entfernt: Jemand hat es mit deinem Passwort angefordert. Warst du das nicht, sag es ab und ändere dein Passwort. ' ;
}
@ override
String passwordResetsAccount ( String date ) {
return ' Am $ date bekommt dein Konto ein neues Passwort und neue Schlüssel, und was mit den alten verschlüsselt ist, geht verloren: Jemand hat es mit einem Code per Mail angefordert. Warst du das nicht, sag es ab. ' ;
}
@ override
String get passwordResetsCallOff = > ' Absagen ' ;
@ override
String get passwordCallOffTitle = > ' Zurücksetzen absagen ' ;
@ override
String get passwordCallOffIntro = >
' Dein Passwort und ein Code sagen jedes anstehende Zurücksetzen ab. ' ;
fix(auth): GitHub-only accounts go at GitHub's end; only the anonymous add an address
The full reading's M1 and L6: once GitHub IDs were forgotten at
GITHUB_SIGN_IN_ENDS, an account that had signed in with GitHub alone
counted as anonymous, so a stolen session and any mailbox gave it a first
password and authenticator; and such accounts, with no way in left, were
never pruned. Now:
- at the end, the daily run that forgets the GitHub IDs first deletes the
accounts that sign in with GitHub alone (no address, so never a
password), with everything they have, members told (deletePerson); the
app tells them for the three months before that their account goes then
unless they set up a password (passwordSectionGitHubEndsDeletes, en/de);
- password.set adds an address without GitHub only for someone anonymous
where the session lives (anonymousHere, which a member tells the
register, as it tells freshGitHubSignIn); anyone else is refused before
any code is used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 13:23:40 +02:00
@ override
String passwordSectionGitHubEndsDeletes ( String date ) {
2026-10-21 17:52:31 +02:00
return ' Die Anmeldung mit GitHub endet am $ date . Richte bis dahin ein Passwort ein: Ein Konto ohne wird 30 Tage später mit allem darin gelöscht. ' ;
fix(auth): GitHub-only accounts go at GitHub's end; only the anonymous add an address
The full reading's M1 and L6: once GitHub IDs were forgotten at
GITHUB_SIGN_IN_ENDS, an account that had signed in with GitHub alone
counted as anonymous, so a stolen session and any mailbox gave it a first
password and authenticator; and such accounts, with no way in left, were
never pruned. Now:
- at the end, the daily run that forgets the GitHub IDs first deletes the
accounts that sign in with GitHub alone (no address, so never a
password), with everything they have, members told (deletePerson); the
app tells them for the three months before that their account goes then
unless they set up a password (passwordSectionGitHubEndsDeletes, en/de);
- password.set adds an address without GitHub only for someone anonymous
where the session lives (anonymousHere, which a member tells the
register, as it tells freshGitHubSignIn); anyone else is refused before
any code is used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-21 13:23:40 +02:00
}
2026-10-21 13:32:33 +02:00
@ override
String get passwordSignedInBefore = >
' Nur ein seitdem angemeldetes Gerät kann diese Warnung ausblenden: Melde dich auf diesem neu an oder ändere dein Passwort. ' ;
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
@ override
String get simpleLoginContinue = > ' Weiter mit SimpleLogin ' ;
@ override
String simpleLoginNote ( String instance ) {
return ' SimpleLogin ist die Anmeldung von Proton, und ein Proton-Konto funktioniert dort auch. SimpleLogin erfährt, dass du $ instance nutzt, und $ instance speichert nur deine SimpleLogin-ID: Dein Name schlägt einmal ein Handle vor, sonst wird nichts gespeichert. ' ;
}
@ override
String get simpleLoginTitle = > ' Mit SimpleLogin anmelden ' ;
@ override
String get simpleLoginAddTitle = > ' SimpleLogin hinzufügen ' ;
@ override
String get simpleLoginInBrowser = > ' Mach in deinem Browser weiter. ' ;
@ override
String get simpleLoginNewcomerTitle = > ' Neu hier? ' ;
@ override
String simpleLoginNewcomerIntro ( String instance ) {
return ' Zu deinem SimpleLogin-Konto gibt es auf $ instance noch kein Konto. ' ;
}
@ override
String get simpleLoginCreateHint = >
' Wähle ein Handle und ein Passwort; du bekommst eine Wiederherstellungsphrase. ' ;
@ override
String get simpleLoginHaveOne = > ' Ich habe schon eins ' ;
@ override
String get simpleLoginHaveOneHint = >
' Verknüpfe es wieder mit deinem Handle und deiner Wiederherstellungsphrase. ' ;
@ override
String get simpleLoginHandleIntro = >
' Andere finden dich über dein Handle. Es bleibt privat, bis du etwas veröffentlichst oder dein Profil ausfüllst, und lässt sich bis zu deiner ersten Veröffentlichung ändern. ' ;
@ override
String get simpleLoginHandle = > ' Handle ' ;
@ override
String simpleLoginHandleFree ( String handle ) {
return ' @ $ handle ist frei ' ;
}
@ override
String get simpleLoginPasswordIntro = >
' Wähle ein Passwort: Du gibst es ein, wenn du dich auf einem anderen Gerät anmeldest. Es verlässt dieses Gerät nie: Die Instanz kann es nicht lesen und nicht zurücksetzen, ohne dass verloren geht, was es schützt. ' ;
@ override
String get simpleLoginCreate = > ' Konto erstellen ' ;
@ override
String get simpleLoginUnlockTitle = > ' Dein Passwort ' ;
@ override
String simpleLoginUnlockIntroAs ( String handle ) {
return ' Mit SimpleLogin als @ $ handle angemeldet. Dein Passwort öffnet deine Schlüssel auf diesem Gerät. ' ;
}
@ override
String get simpleLoginUnlockIntro = >
' Mit SimpleLogin angemeldet. Dein Passwort öffnet deine Schlüssel auf diesem Gerät. ' ;
@ override
String get simpleLoginOpen = > ' Öffnen ' ;
@ override
String get simpleLoginMendTitle = > ' Konto verknüpfen ' ;
@ override
String get simpleLoginMendIntro = >
' Dein SimpleLogin-Konto ist nicht mehr das, das dein Konto kennt. Verknüpfe sie wieder mit deinem Handle und deiner Wiederherstellungsphrase: Deine anderen Geräte werden dann abgemeldet. ' ;
@ override
String get simpleLoginMend = > ' Verknüpfen ' ;
@ override
String get simpleLoginWrongPassword = > ' Das Passwort stimmt nicht. ' ;
@ override
String get simpleLoginMendDoesNotFit = >
' Das Handle oder die Wiederherstellungsphrase stimmt nicht. ' ;
@ override
String get simpleLoginUnavailable = >
' Diese Instanz bietet die Anmeldung mit SimpleLogin nicht an. ' ;
@ override
String get simpleLoginDenied = >
' Die Anmeldung bei SimpleLogin wurde abgelehnt, also wurde nichts geteilt. ' ;
@ override
String get simpleLoginNoBrowser = >
' Es gibt keinen Browser, der die Seite von SimpleLogin öffnen kann. ' ;
@ override
String get simpleLoginStartAgain = >
' Die Anmeldung hat zu lange gedauert, oder eine andere ist an ihre Stelle getreten. Beginne sie noch einmal. ' ;
@ override
String get simpleLoginRejected = >
' SimpleLogin hat die Anmeldung nicht bestätigt. Beginne sie noch einmal. ' ;
@ override
String get simpleLoginProviderUnavailable = >
' Die Instanz erreicht SimpleLogin gerade nicht. Versuch es später noch einmal. ' ;
@ override
String get simpleLoginTakenByAnotherPerson = >
' Dieses SimpleLogin-Konto gehört hier zu einem anderen Konto. ' ;
@ override
String get simpleLoginOtherIdentityLinked = >
' Dein Konto hat schon ein anderes SimpleLogin-Konto. ' ;
@ override
String get simpleLoginTooManyAttempts = >
' Von diesem Netz aus wurden zu viele Anmeldungen begonnen. Versuch es in einer Stunde noch einmal. ' ;
@ override
String get simpleLoginHandleInvalid = >
' Ein Handle besteht aus 1 bis 30 Buchstaben (a bis z), Ziffern oder Unterstrichen. ' ;
@ override
String get simpleLoginHandleReserved = >
' Dieses Handle ist der Instanz vorbehalten. ' ;
@ override
String get simpleLoginHandleTaken = >
' Dieses Handle hat schon jemand anderes. ' ;
@ override
String get simpleLoginTooManyHandleChecks = >
' Es wurden zu viele Handles ausprobiert. Versuch es in einer Stunde noch einmal. ' ;
2026-10-24 11:56:26 +02:00
@ override
String get simpleLoginSectionTitle = > ' SimpleLogin ' ;
@ override
String get simpleLoginSignsYouIn = > ' Meldet dich an ' ;
@ override
String get simpleLoginAddHint = >
' Melde dich mit SimpleLogin an, oder mit deinem Proton-Konto. ' ;
@ override
String get simpleLoginAdded = > ' SimpleLogin hinzugefügt. ' ;
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
}