feat(base)!: a handle at password sign-up (ADR 0020, D26, D28–D30)

Every new account gets a handle, and an account without one gets its
first as it sets a password. The handle is private until its person
goes public (D5).

- `passwordSignIn.signUp` takes a handle. `passwordSignIn.checkHandle`
  says whether a sign-up could take one, only while that sign-up's
  mailed code waits (`codeExpired` otherwise), counted per code request
  (30 an hour) and per network (60) (D29). Members ask the register.
- `password.set` takes the first handle of an account without one
  (D28); `SignInWays.handle` tells the app whether there is one.
- Each checks the handle before the code is used, so a refusal leaves
  the code for another try. An existing account at the address keeps
  its own handle where it has one, and the typed handle is still
  checked, so the answer says nothing about the address.
- `first_handle.dart` holds what SimpleLogin, the keys and passwords
  share: `refusalOfNewHandle`, `insertFirstProfile`, `hasHandle`.
- halkyon_app: one `HandleField` with its `HandleCheckCubit` (asked
  300 ms after typing pauses, only about the handle typed last; the
  instance's suggestion is free without asking) (D30). The password
  setup asks for the handle between the phrase check and the code, and
  a refusal at the code sends back to it. SimpleLogin's sign-up and
  keys steps use the same field and now check as it is typed.
- The module's README and ADR 0020 (D29, D30, the built state) amended.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-24 18:09:04 +02:00
commit 630965d920
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
5 changed files with 2305 additions and 395 deletions

View file

@ -86,6 +86,7 @@
"passwordQrCode": "QR-Code für deine Authenticator-App",
"passwordMailCode": "Code aus der E-Mail",
"passwordMailCodeSent": "Wir haben dir einen Code an {address} geschickt. Er gilt 15 Minuten.",
"passwordHandleIntro": "Ein Code ist unterwegs an {address}. Wähle inzwischen dein Handle: Darüber finden dich andere. Es bleibt privat, bis du etwas veröffentlichst oder dein Profil ausfüllst, und lässt sich bis zu deiner ersten Veröffentlichung ändern.",
"passwordResendCode": "Neuen Code senden",
"passwordTotpCode": "Code aus deiner Authenticator-App",
"passwordRecoveryCode": "Wiederherstellungscode",
@ -247,9 +248,10 @@
"simpleLoginCreateHint": "Wähle ein Handle und ein Passwort; du bekommst eine Wiederherstellungsphrase.",
"simpleLoginHaveOne": "Ich habe schon eins",
"simpleLoginHaveOneHint": "Verknüpfe es wieder mit deinem Handle und deiner Wiederherstellungsphrase.",
"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.",
"simpleLoginHandle": "Handle",
"simpleLoginHandleFree": "@{handle} ist frei",
"handleIntro": "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.",
"handleLabel": "Handle",
"handleFree": "@{handle} ist frei",
"handleChecking": "Wird geprüft …",
"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.",
"simpleLoginCreate": "Konto erstellen",
"simpleLoginUnlockTitle": "Dein Passwort",
@ -270,10 +272,10 @@
"simpleLoginTakenByAnotherPerson": "Dieses SimpleLogin-Konto gehört hier zu einem anderen Konto.",
"simpleLoginOtherIdentityLinked": "Dein Konto hat schon ein anderes SimpleLogin-Konto.",
"simpleLoginTooManyAttempts": "Von diesem Netz aus wurden zu viele Anmeldungen begonnen. Versuch es in einer Stunde noch einmal.",
"simpleLoginHandleInvalid": "Ein Handle besteht aus 1 bis 30 Buchstaben (a bis z), Ziffern oder Unterstrichen.",
"simpleLoginHandleReserved": "Dieses Handle ist der Instanz vorbehalten.",
"simpleLoginHandleTaken": "Dieses Handle hat schon jemand anderes.",
"simpleLoginTooManyHandleChecks": "Es wurden zu viele Handles ausprobiert. Versuch es in einer Stunde noch einmal.",
"handleInvalid": "Ein Handle besteht aus 1 bis 30 Buchstaben (a bis z), Ziffern oder Unterstrichen.",
"handleReserved": "Dieses Handle ist der Instanz vorbehalten.",
"handleTaken": "Dieses Handle hat schon jemand anderes.",
"handleTooManyChecks": "Es wurden zu viele Handles ausprobiert. Versuch es in einer Stunde noch einmal.",
"simpleLoginSectionTitle": "SimpleLogin",
"simpleLoginSignsYouIn": "Meldet dich an",
"simpleLoginAddHint": "Melde dich mit SimpleLogin an, oder mit deinem Proton-Konto.",

View file

@ -259,6 +259,14 @@
}
}
},
"passwordHandleIntro": "A code is on its way to {address}. Meanwhile, choose your handle: others find you by it. It stays private until you publish something or fill in your profile, and can change until your first publish.",
"@passwordHandleIntro": {
"placeholders": {
"address": {
"type": "String"
}
}
},
"passwordResendCode": "Send a new code",
"passwordTotpCode": "Code from your authenticator app",
"passwordRecoveryCode": "Recovery code",
@ -560,16 +568,17 @@
"simpleLoginCreateHint": "Choose a handle and a password; you get a recovery phrase.",
"simpleLoginHaveOne": "I already have one",
"simpleLoginHaveOneHint": "Link it again with your handle and your recovery phrase.",
"simpleLoginHandleIntro": "Others find you by your handle. It stays private until you publish something or fill in your profile, and can change until your first publish.",
"simpleLoginHandle": "Handle",
"simpleLoginHandleFree": "@{handle} is free",
"@simpleLoginHandleFree": {
"handleIntro": "Others find you by your handle. It stays private until you publish something or fill in your profile, and can change until your first publish.",
"handleLabel": "Handle",
"handleFree": "@{handle} is free",
"@handleFree": {
"placeholders": {
"handle": {
"type": "String"
}
}
},
"handleChecking": "Checking…",
"simpleLoginPasswordIntro": "Choose a password: you type it when you sign in on another device. It never leaves this device: the instance can't read it, and can't reset it without what it protects being lost.",
"simpleLoginCreate": "Create the account",
"simpleLoginUnlockTitle": "Your password",
@ -597,10 +606,10 @@
"simpleLoginTakenByAnotherPerson": "This SimpleLogin account belongs to another account here.",
"simpleLoginOtherIdentityLinked": "Your account has another SimpleLogin account already.",
"simpleLoginTooManyAttempts": "Too many sign-ins began from this network. Try again in an hour.",
"simpleLoginHandleInvalid": "A handle is 1 to 30 letters (a to z), digits or underscores.",
"simpleLoginHandleReserved": "This handle is kept for the instance.",
"simpleLoginHandleTaken": "Someone else has this handle.",
"simpleLoginTooManyHandleChecks": "Too many handles were tried. Try again in an hour.",
"handleInvalid": "A handle is 1 to 30 letters (a to z), digits or underscores.",
"handleReserved": "This handle is kept for the instance.",
"handleTaken": "Someone else has this handle.",
"handleTooManyChecks": "Too many handles were tried. Try again in an hour.",
"simpleLoginSectionTitle": "SimpleLogin",
"simpleLoginSignsYouIn": "Signs you in",
"simpleLoginAddHint": "Sign in with SimpleLogin, or with your Proton account.",

View file

@ -619,6 +619,12 @@ abstract class HalkyonAppLocalizations {
/// **'We emailed a code to {address}. It\'s valid for 15 minutes.'**
String passwordMailCodeSent(String address);
/// No description provided for @passwordHandleIntro.
///
/// In en, this message translates to:
/// **'A code is on its way to {address}. Meanwhile, choose your handle: others find you by it. It stays private until you publish something or fill in your profile, and can change until your first publish.'**
String passwordHandleIntro(String address);
/// No description provided for @passwordResendCode.
///
/// In en, this message translates to:
@ -1585,23 +1591,29 @@ abstract class HalkyonAppLocalizations {
/// **'Link it again with your handle and your recovery phrase.'**
String get simpleLoginHaveOneHint;
/// No description provided for @simpleLoginHandleIntro.
/// No description provided for @handleIntro.
///
/// In en, this message translates to:
/// **'Others find you by your handle. It stays private until you publish something or fill in your profile, and can change until your first publish.'**
String get simpleLoginHandleIntro;
String get handleIntro;
/// No description provided for @simpleLoginHandle.
/// No description provided for @handleLabel.
///
/// In en, this message translates to:
/// **'Handle'**
String get simpleLoginHandle;
String get handleLabel;
/// No description provided for @simpleLoginHandleFree.
/// No description provided for @handleFree.
///
/// In en, this message translates to:
/// **'@{handle} is free'**
String simpleLoginHandleFree(String handle);
String handleFree(String handle);
/// No description provided for @handleChecking.
///
/// In en, this message translates to:
/// **'Checking…'**
String get handleChecking;
/// No description provided for @simpleLoginPasswordIntro.
///
@ -1723,29 +1735,29 @@ abstract class HalkyonAppLocalizations {
/// **'Too many sign-ins began from this network. Try again in an hour.'**
String get simpleLoginTooManyAttempts;
/// No description provided for @simpleLoginHandleInvalid.
/// No description provided for @handleInvalid.
///
/// In en, this message translates to:
/// **'A handle is 1 to 30 letters (a to z), digits or underscores.'**
String get simpleLoginHandleInvalid;
String get handleInvalid;
/// No description provided for @simpleLoginHandleReserved.
/// No description provided for @handleReserved.
///
/// In en, this message translates to:
/// **'This handle is kept for the instance.'**
String get simpleLoginHandleReserved;
String get handleReserved;
/// No description provided for @simpleLoginHandleTaken.
/// No description provided for @handleTaken.
///
/// In en, this message translates to:
/// **'Someone else has this handle.'**
String get simpleLoginHandleTaken;
String get handleTaken;
/// No description provided for @simpleLoginTooManyHandleChecks.
/// No description provided for @handleTooManyChecks.
///
/// In en, this message translates to:
/// **'Too many handles were tried. Try again in an hour.'**
String get simpleLoginTooManyHandleChecks;
String get handleTooManyChecks;
/// No description provided for @simpleLoginSectionTitle.
///

View file

@ -330,6 +330,11 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
return 'Wir haben dir einen Code an $address geschickt. Er gilt 15 Minuten.';
}
@override
String passwordHandleIntro(String address) {
return 'Ein Code ist unterwegs an $address. Wähle inzwischen dein Handle: Darüber finden dich andere. 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 passwordResendCode => 'Neuen Code senden';
@ -933,17 +938,20 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
'Verknüpfe es wieder mit deinem Handle und deiner Wiederherstellungsphrase.';
@override
String get simpleLoginHandleIntro =>
String get handleIntro =>
'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';
String get handleLabel => 'Handle';
@override
String simpleLoginHandleFree(String handle) {
String handleFree(String handle) {
return '@$handle ist frei';
}
@override
String get handleChecking => 'Wird geprüft …';
@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.';
@ -1020,19 +1028,17 @@ class HalkyonAppLocalizationsDe extends HalkyonAppLocalizations {
'Von diesem Netz aus wurden zu viele Anmeldungen begonnen. Versuch es in einer Stunde noch einmal.';
@override
String get simpleLoginHandleInvalid =>
String get handleInvalid =>
'Ein Handle besteht aus 1 bis 30 Buchstaben (a bis z), Ziffern oder Unterstrichen.';
@override
String get simpleLoginHandleReserved =>
'Dieses Handle ist der Instanz vorbehalten.';
String get handleReserved => 'Dieses Handle ist der Instanz vorbehalten.';
@override
String get simpleLoginHandleTaken =>
'Dieses Handle hat schon jemand anderes.';
String get handleTaken => 'Dieses Handle hat schon jemand anderes.';
@override
String get simpleLoginTooManyHandleChecks =>
String get handleTooManyChecks =>
'Es wurden zu viele Handles ausprobiert. Versuch es in einer Stunde noch einmal.';
@override

View file

@ -326,6 +326,11 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
return 'We emailed a code to $address. It\'s valid for 15 minutes.';
}
@override
String passwordHandleIntro(String address) {
return 'A code is on its way to $address. Meanwhile, choose your handle: others find you by it. It stays private until you publish something or fill in your profile, and can change until your first publish.';
}
@override
String get passwordResendCode => 'Send a new code';
@ -915,17 +920,20 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
'Link it again with your handle and your recovery phrase.';
@override
String get simpleLoginHandleIntro =>
String get handleIntro =>
'Others find you by your handle. It stays private until you publish something or fill in your profile, and can change until your first publish.';
@override
String get simpleLoginHandle => 'Handle';
String get handleLabel => 'Handle';
@override
String simpleLoginHandleFree(String handle) {
String handleFree(String handle) {
return '@$handle is free';
}
@override
String get handleChecking => 'Checking…';
@override
String get simpleLoginPasswordIntro =>
'Choose a password: you type it when you sign in on another device. It never leaves this device: the instance can\'t read it, and can\'t reset it without what it protects being lost.';
@ -1002,18 +1010,17 @@ class HalkyonAppLocalizationsEn extends HalkyonAppLocalizations {
'Too many sign-ins began from this network. Try again in an hour.';
@override
String get simpleLoginHandleInvalid =>
String get handleInvalid =>
'A handle is 1 to 30 letters (a to z), digits or underscores.';
@override
String get simpleLoginHandleReserved =>
'This handle is kept for the instance.';
String get handleReserved => 'This handle is kept for the instance.';
@override
String get simpleLoginHandleTaken => 'Someone else has this handle.';
String get handleTaken => 'Someone else has this handle.';
@override
String get simpleLoginTooManyHandleChecks =>
String get handleTooManyChecks =>
'Too many handles were tried. Try again in an hour.';
@override