halkyon/docs/adr/0020-sign-in-with-simplelogin.md

512 lines
39 KiB
Markdown
Raw Permalink Normal View History

# ADR 0020: Signing in with SimpleLogin only
feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35) SimpleLogin's sheet, its password and its recovery phrase both lost: under the lost-password step's phrase field, Lost your recovery phrase too? says what stays and what is lost and asks for new keys (`keys.startReset`) with the sign-in of a moment ago, which the device then drops. The next sign-in says when while it waits, and once due offers new keys, the sign-up's own password and phrase steps (`keys.completeReset`), or the password, which calls it off (D31). Every app's home shows `KeysResetBanner` where "too old" shows, and the account section's SimpleLogin row the same words (`KeysResetNotice`), each with Call it off, the password's proof alone (`keys.callOffResets`, D32), and, once due on a device that holds the keys, Finish here, which wraps the same keys anew (D33, D35). Both show only a reset of an account that signs in with SimpleLogin alone; ADR 0017's resets show in its own section, which now shares `AlertBox` with them. An account whose password signs in has no Forgot your password? there, and is asked its password and a code once while its keys lack the proof key, at a sign-in or on adding SimpleLogin (`password.setProofKey`, D34). `PasswordUnlock` carries the proof key, which its forgetting leaves, and the keys' password proves for a purpose given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 22:37:17 +02:00
- **Status:** decided 23 Oct 2026, by the developer (D1–D20 below; D21–D35 on 24
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
Oct, D36 on 25 Oct), the *Proposal* with it (D15), as D18 to D20 change it, D24 drops
the cut-off's machinery (D8's code, D10, D22, 10's `ANONYMOUS_SESSIONS_END`, 11, step
5) and D36 takes GitHub out of the apps and the instances.
- **Built (23 Oct 2026), step 1:** the forwarding page (3),
`/simplelogin-callback` on the register's web server (halkyon_base's
`addSimpleLoginCallbackRoute`, Caddy forwarding the path), ahead of the sign-in, so
development signs in through learn.halkyon.eu's page, which its SimpleLogin app
registers too. The state is `<app>-<way back>.<rest>`: the marker one of the module's
fixed table (Learn and Todo, the loopback on ports 43823 and 43824, or the app's
scheme; named `<app>-<platform>` until D9's correction), the rest 1 to 512 characters
of base64url. A 303 carries the code or
SimpleLogin's error (`deny`) with the state and nothing else; whatever is malformed,
repeated or not a GET is refused with a page that repeats nothing. No caching, no
referrer, nothing logged. Android's `assetlinks.json` for Halkyon Learn signed with
the release key is answered by `deploy/Caddyfile` on learn.halkyon.eu alone, held to
`tool/release/android-certificate.sha256` by a test: built for D9's verified links,
which its correction dropped, and kept, as it costs nothing.
- **Built (23 Oct 2026), step 2:** the sign-in on the server's side (1 to 5), on
halkyon_oidc (ADR 0023) with its SimpleLogin preset: `simpleLogin.begin` and
`simpleLogin.complete` in halkyon_base, a member asking the register for both; the
instance's basics with `simpleLogin`, the ways in with it; the attempts and the
newcomers' identities forgotten hourly with the sign-up attempts. Otherwise than
proposed: the identities are halkyon_oidc's (`halkyon_oidc_identity`: account,
provider, subject; the provider `simplelogin`), not a table of halkyon_base's; the app
and platform are the state's marker rather than a column; a signed-in caller of a
member becomes a person on the register as the attempt begins, as linking would make
them; GitHub links nobody more where SimpleLogin is offered, as where passwords are
(ADR 0017); `SIGN_IN_WAYS` counts on the register, which members ask, and stops any
server with a name it doesn't know as it starts; sessions name the method
`simplelogin` (these deviations accepted by the developer, 23 Oct). Someone new gets
no account yet: their identity waits (D16), with a
handle suggested (D17), for the sign-up of step 3. halkyon_oidc's `testing.dart` gives
hosts a fake SimpleLogin signing real ID tokens, which the module's tests use too.
feat(base): SimpleLogin accounts' keys without SRP, signing up in one call (ADR 0020, step 3) An account that signs in with SimpleLogin keeps its keys under a password that signs nobody in (D19): `account_password` keeps the password's own proof key (Ed25519, `passwordProofKey`) in place of SRP's verifier, which becomes nullable. The migration adds the column and drops the NOT NULL, nothing else. - `simpleLogin.signUp(attempt, handle, setup)`: someone new whose identity waits under the attempt gets, in one transaction, an account (identified), the link, a profile with the handle and the keys; then a session (D18). Refused, the identity still waiting, for a setup out of range and a handle refused as saving a profile refuses it. - `simpleLogin.checkHandle(attempt, handle)`: whether someone new could take a handle, at most 30 an hour per attempt and 60 per network. - `simpleLogin.complete` says, for an account, whether it has its keys and its handle, and keeps the sign-in ten minutes under its attempt, the account as its caller: D20's confirmation for the changes to come. - `keys.begin`, `keys.unlock`: the keys for a signed-in device once the password is proven with its proof key, for a nonce, bound to the base URL the device addressed; wrong proofs counted per session as ADR 0017's. - An authenticator is enrolled only for a password that signs in. - Members ask the register for all of it; `RemoteRegister` and the instance API carry the four new operations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 21:49:44 +02:00
- **Built (23 Oct 2026), step 3 so far:** the password's own proof key (`halkyon_keys`'
subkey 3 of the master key, D19), which `account_password` keeps in place of SRP's
verifier for an account that signs in with SimpleLogin (the verifier becomes nullable:
a migration that drops nothing); `simpleLogin.signUp` in one call (D18) and
`simpleLogin.checkHandle`, both for someone new's identity alone; `keys.begin` and
`keys.unlock`, the proof counted as ADR 0017's; every sign-in into an account kept ten
feat(base): the changes of SimpleLogin accounts' keys, each with the password and a fresh sign-in (ADR 0020, D20) Every change takes the password's proof by its own proof key (the recovery phrase's for a lost password) and a SimpleLogin sign-in of the last ten minutes on the device (`KeysOwnerProof`, its attempt), as ADR 0017's take the password and a code. The sign-in is checked before the proof and taken with the change, so a wrong proof leaves it standing and each sign-in serves one change. - `keys.set(attempt, setup, handle?)`: the first keys of an account that SimpleLogin was added to (anonymous before, or signing in with GitHub or a code), with the handle where it has none; the sign-in alone, as there is no password to prove yet. - `keys.changePassword`, `keys.beginPhrase` and `keys.resetPassword` (a lost password, with the phrase): every other session ends, in every service, as ADR 0017's change has it. - `keys.setRecoveryKeys`: a new phrase. - `account.delete(keysProof)`: an account without SRP's verifier deletes itself with the proof and the sign-in; `requireOwnerToDelete` no longer asks such an account for SRP. - `newKeysPasswordDigest` on both sides, held to each other by a test; a challenge purpose of its own for the phrase's proof while signed in. - Members ask the register for all of it; `RemoteRegister` and the instance API carry the five new operations and the deletion's proof. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:11:49 +02:00
minutes under its attempt as D20's confirmation. Then the changes, each through the
register: the first keys (and handle, where none) of an account SimpleLogin was added
to (`keys.set`), with the sign-in alone, as there is no password to prove yet; a new
password, a lost one with the phrase (`keys.beginPhrase`, `keys.resetPassword`), a new
phrase and the deletion (`account.delete`'s `keysProof`), each with the proof and the
sign-in, which the change takes: one change for each sign-in. A new or lost password
feat(base): mending a broken SimpleLogin link with the handle and the recovery phrase (ADR 0020, D7) Someone whose SimpleLogin identity is new to the register but who has an account ("I already have one") names their handle and proves the account's recovery phrase; the new identity takes the old one's place. - `simpleLogin.beginMend(attempt, handle)`: the keys wrapped under the phrase's key, the public keys and a nonce, for the account with that handle (whatever its case). Only an account with keys and a SimpleLogin identity is mended, a link it had, as nobody adds SimpleLogin signed out (D10); a handle nobody has, or any other account's, gets a decoy from `signInDecoyKey`, the same each time. At most 10 an hour per handle and 30 per network. - `simpleLogin.mend(attempt, proof)`: the phrase's proof for `mendLink`, binding the attempt (`mendLinkDigest`, on both sides), so a proof relayed from another sign-in links nobody's identity but the device's own; checked for the waiting identity first, which leaves the proof unspent. Every session of the account ends, in every service, and the sign-in stands ten minutes for a change, such as a lost password. - SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's three keys (`signInDecoyKeyOf`); `addInstanceRoutes` stops a server with a SimpleLogin app and without it (`checkSimpleLoginMending`). - Members ask the register for both; `RemoteRegister` and the instance API carry them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 22:26:58 +02:00
ends every other session, as ADR 0017's change does. Then mending a broken link
(8, D7): `simpleLogin.beginMend(attempt, handle)` and `simpleLogin.mend(attempt,
proof)`, the phrase's proof binding the attempt, so a relayed proof links nobody
else's identity. Otherwise than proposed: only an account with a SimpleLogin identity
is mended (D10: nobody adds SimpleLogin signed out), every other answered with a
decoy; mending ends every session of the account, as whoever came in through the old
link may still be in; and SimpleLogin needs `SIGN_IN_DECOY_KEY` alone of ADR 0017's
three keys, a server with a SimpleLogin app and without it stopping as it starts.
Then handles (9, D5): `/@handle` and `profiles.byHandle` answer as for a handle nobody
has until its person goes public (a handle fixed by a publish, or a display name, a
bio, a link or an avatar), the host's own code still finding whose a handle is.
Otherwise than proposed: not only asking whether a handle is free is limited, but
every answer about a new handle, counted together (`countHandleCheck`, 30 an hour per
person or attempt, 60 per network): saving a profile with one, signing up and the
first keys with one, and Halkyon Learn's blocks by handle, as each tells the same, so
a limit on checking alone would limit nothing; a person's own handle and what is no
handle aren't counted. The handle suggested at sign-up is not counted: it tells at
most that the name SimpleLogin shared is someone's handle already, once a sign-in.
Step 3's deviations accepted by the developer (23 Oct, night).
- **Built (24 Oct 2026), step 4 so far:** halkyon_app's sign-in with SimpleLogin (the
start screen's Continue with SimpleLogin and its sheet), then adding it to an account
(D21, D23): the apps read where their person stands (`SimpleLoginStandingCubit`:
nothing to show, linked, or missing) and show it in halkyon_app's
`SimpleLoginSection`, which reads it once where a `SimpleLoginStandingScope` is above
it, as Learn's Profile has it for its first line and GitHub's button too. A cut-off
day in the basics (`SIMPLELOGIN_REQUIRED_FROM`) with D22's banner was built and taken
out again before any release (D24). Then the apps' half of D25 (24 Oct, night): no
app makes an anonymous account. The start screen shows whenever nobody is signed in,
a first start included, and offers no start without an account. It says so when the
instance is out of reach (Try again), and when the instance offers no way to create
an account. An anonymous account's session left from before is deleted as the app
starts (`AuthRepository.endAnonymousSession`), so it goes at once rather than with
the next day's pruning. Where the instance can't be reached, the device only signs
it out. Adding GitHub, the Profile tab's GitHub sign-in with its Cancel, the
signed-out mark and an anonymous device's own keys went with it. Then the server's
half: halkyon_base signs nobody in anonymously and has no endpoint adding GitHub; a
password is set only by an account with a way in (an anonymous one left from before
is refused, `wrongCredentials`, before any code is used), and SimpleLogin is added
only to a caller with `identified` (`wrongCaller`, also on a member, which then asks
the register nothing). The daily pruning (`pruneAnonymousPeople`, renamed
`prune-accounts` in Learn and Todo, the entry under its old name cancelled as they
start) deletes every anonymous account left, whatever its age, never one with
`identified`. Learn keeps no files for an account without `identified`
(`FILE_LIMIT_ANONYMOUS_MB` went), and `minimumAppBuild` is 23 (v0.20.0), as builds up
to 22 start anonymously. Learn's sync tests make their people and sessions by SQL,
as Serverpod's server-side sessions make them, in place of an anonymous sign-in.
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>
2026-10-24 18:09:04 +02:00
Then the handle at password sign-up (D26, D28–D30): `passwordSignIn.signUp` takes
one and `password.set` takes one from an account without (`SignInWays.handle` tells
the app), each checked before the code is used, so a refusal (`ProfileRefusedException`
as the Profile's check gives it) leaves the code for another try; a new account's
profile is private (D5). `passwordSignIn.checkHandle` answers while the code travels,
only for a sign-up's code request still waiting (`codeExpired` otherwise), counted per
request (30 an hour) and network (60), as the register's members ask the register.
An existing account given the address's code keeps its own handle where it has one,
the typed one still checked, so the answer tells nothing of the address. In the apps,
one `HandleField` with its `HandleCheckCubit` (asked 300 ms after typing pauses, only
for the handle typed last, the instance's suggestion free without asking) serves the
password setup's handle step, between the phrase check and the code, and
SimpleLogin's sign-up and keys steps. Then D27: `profile.remove` (and the register's
`removeProfile`, members asking it) empties the display name, bio, links and their
checks and the avatar, keeps the row with its handle, and returns what is left; Learn's
profile form starts afresh from it and offers no removing while nothing is filled in.
Then a lost password with the phrase (7, D20), in SimpleLogin's sheet: the unlock
step's Forgot your password? opens one step, the recovery phrase and a new password
twice, with no address or code, as the sign-in of a moment ago stands for them. The
device opens the keys with the phrase, wraps them under the new password and signs the
phrase's proof bound to it (`keys.beginPhrase`, `keys.resetPassword`); the keys stay,
every other session ends.
Then the reset of the keys without the phrase, the server's half (D31–D33):
`keys.startReset` with a SimpleLogin sign-in, taken, `keys.completeReset` once due
with another (`notDue` with when, before), and `keys.callOffResets` with the
password's proof (the new purpose `call off resets`), each through the register; every
right proof of the password's or the phrase's key, and a mend's, calls the reset off.
ADR 0017's reset by mail takes only an account whose password signs in. And D34's
`password.setProofKey`, the owner's proof binding the key (the new purpose `keys proof
key`); ADR 0017's new password takes the key away. Only an account that signs in with
SimpleLogin alone changes through `keys` (`opensKeysAlone`). `simpleLogin.complete`
says whether the password signs in too (`passwordSignsIn`), whether the keys have the
proof key (`hasProofKey`) and when a reset takes effect (`accountResetAt`). No
migration: the reset is ADR 0017's `account_reset` row of kind `account`.
feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35) SimpleLogin's sheet, its password and its recovery phrase both lost: under the lost-password step's phrase field, Lost your recovery phrase too? says what stays and what is lost and asks for new keys (`keys.startReset`) with the sign-in of a moment ago, which the device then drops. The next sign-in says when while it waits, and once due offers new keys, the sign-up's own password and phrase steps (`keys.completeReset`), or the password, which calls it off (D31). Every app's home shows `KeysResetBanner` where "too old" shows, and the account section's SimpleLogin row the same words (`KeysResetNotice`), each with Call it off, the password's proof alone (`keys.callOffResets`, D32), and, once due on a device that holds the keys, Finish here, which wraps the same keys anew (D33, D35). Both show only a reset of an account that signs in with SimpleLogin alone; ADR 0017's resets show in its own section, which now shares `AlertBox` with them. An account whose password signs in has no Forgot your password? there, and is asked its password and a code once while its keys lack the proof key, at a sign-in or on adding SimpleLogin (`password.setProofKey`, D34). `PasswordUnlock` carries the proof key, which its forgetting leaves, and the keys' password proves for a purpose given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 22:37:17 +02:00
Then the app's half (D31–D35): under the lost-password step's phrase field, Lost your
recovery phrase too? explains what stays and what is lost and asks for new keys with
the sign-in of a moment ago, which the device then drops; the next sign-in says when
while it waits, and once due offers new keys (the sign-up's password and phrase steps)
or the password. An account whose password signs in has no Forgot your password? there
and is asked its password and a code once, while its keys lack the proof key (D34),
with two Argon2id runs (the proof, then the unlock), as it happens once. Every app's
home shows `KeysResetBanner` where "too old" shows, and the account section's
SimpleLogin row the same words (`KeysResetNotice`), each with Call it off (the
password, D32) and, once due on a device that holds the keys, Finish here (D33); both
show only a reset of an account that signs in with SimpleLogin alone, as ADR 0017's
resets show in its own section. The security section's boxes and this one share
`AlertBox`. Left for later: an account whose password signs in, on an instance that
offers SimpleLogin alone, has no password sign-in to reset a lost password from.
- **Builds on:** ADR 0010 (email sign-in), ADR 0011 (profile pages), ADR 0016 (the
register), ADR 0017 (a password, TOTP and the keys).
- **Changes:** ADR 0017's way in on an instance that offers SimpleLogin (its keys, their
wrapping and the recovery phrase stay as they are), and every instance's anonymous
accounts, which go (ADR 0010's start without an account, ADR 0016's 6, ADR 0017's 9).
## Context
The developer asked on 23 Oct to limit signing in to "only SimpleLogin/Proton single sign
in". Proton offers no OpenID Connect sign-in to other apps; SimpleLogin, which belongs to
Proton, does ("Sign in with SimpleLogin"), and its own login page offers "Log in with
Proton". What SimpleLogin does, from its source (`simple-login/app`, 22 Oct) and from tries
against app.simplelogin.io on 23 Oct:
- **OpenID Connect's code flow:** `https://app.simplelogin.io/oauth2/authorize`,
`/oauth2/token`, `/jwks`; ID tokens signed with RS256, carrying the `nonce` the
authorization asked for and what the person shares (address, name, avatar); each code is
taken once. **No PKCE**; the client secret is always required, so only a server can
exchange a code.
- **New apps are closed by default since 22 Oct** (`ALLOW_NEW_OAUTH_CLIENTS`); "existing
websites keep working as usual". app.simplelogin.io still let the developer make apps on
23 Oct: one for development, one for learn.halkyon.eu ("Halkyon") and a few in reserve.
Other operators may not get an app any more.
- **Dev Mode:** until SimpleLogin approves an app, its consent page warns "… is in Dev
Mode and isn't approved (yet) by SimpleLogin". Approval is asked for on the app's page.
- **Redirects:** each must be registered exactly (an unregistered loopback address was
refused on 23 Oct), and the settings page takes https addresses only.
- **The ID (`sub`) belongs to one person in one app** (`sub` is the person's link to the
app, `ClientUser`), whatever the discovery document's `public` says. It changes when the
person removes the app in SimpleLogin's list, deletes the alias they shared with it, or
deletes their SimpleLogin account. Once someone has allowed an app, later sign-ins skip
the consent page and give the same ID.
- **The consent page always shares an address, a name and an avatar:** the app can't
choose (`Client.get_scopes`). The address is the person's own or an alias, by default a
new one, which counts against their aliases.
## Decisions (23 Oct 2026, the developer's)
- **D1 SimpleLogin only** on learn.halkyon.eu (over SimpleLogin as an extra way in beside
ADR 0017's, which was recommended): no address, no TOTP and no password signs in there.
- **D2 A password still unlocks the keys** (recommended, over dropping the keys): it never
leaves the device; ADR 0017's keys, wrapping and recovery phrase stay.
- **D3 Per instance:** each operator chooses the ways; operators without a SimpleLogin app
keep ADR 0017's.
- **D4 No anonymous accounts, anywhere** (recommended): every account has a way in and a
handle; the first start needs a connection and an account.
- **D5 A handle for everyone**, suggested at sign-up from the name SimpleLogin shares
(recommended), confirmed or changed by the person; the name is then dropped. Its public
page exists only once the person goes public (recommended): something published or the
profile filled in.
- **D6 An ID nobody has, signed out: ask first** (recommended): "New here?" makes an
account; "I already have one" takes the handle and the recovery phrase (8).
- **D7 A broken link is mended with the handle and the recovery phrase** (the developer's,
over the phrase alone, which was recommended): every account has a handle now (D5).
- **D8 A hard cut-off on learn.halkyon.eu, 30 days after the release** (over the day
GitHub ends, 22 Jan 2027, which was recommended): from then on every session of an
account without SimpleLogin ends, GitHub and email codes sign nobody in there, and the
instance offers SimpleLogin alone.
- **D9 Apps come back through their scheme or the loopback** (corrected 23 Oct, night,
by the developer, after research; decided first as verified links for phones on
learn.halkyon.eu, Android's App Links and iOS's https callbacks): Android, iOS and
macOS come back through the app's scheme, which the system's browser sheet catches
(Chrome's Auth Tab, since Chrome 132, even after the forwarding page's redirect;
`ASWebAuthenticationSession`), Linux and Windows through the loopback.
`simpleLogin.begin` takes the way back (`SimpleLoginReturn`: `loopback`, `scheme`),
not the platform. Verified links protected nothing here: a code caught on its way is
worthless without the attempt's ID (4), and a hostile app beginning an attempt of its
own can ask for the scheme or the loopback, which other platforms need open (an
Android app can listen on `127.0.0.1` too). They cost: debug builds and other
instances need the scheme anyway, Firefox has no Auth Tab, Samsung Internet stays on
the page, and flutter_web_auth_2 (v5.1.0) has open bugs with https callbacks (#183,
#215). The scheme is what AppAuth calls the most widely supported.
- **D10 Accounts without SimpleLogin are deleted 30 days after the cut-off**
(recommended, over keeping them unreachable): nobody can add SimpleLogin signed out;
the reminder names both days.
- **D11 An account left unfinished (no keys) is deleted after a day** (recommended, over
finishing it at the next sign-in); its handle is free again.
- **D12 The ID token checked with `dart_jsonwebtoken` itself** (23 Oct, evening,
recommended), already in the lock through Serverpod's sign-ins. The developer first
chose Serverpod's own verifier (`IdTokenVerifier`, which Google's and Firebase's
sign-ins use; not exported, so imported from its `src/`), which can't: SimpleLogin's
ID tokens carry no `typ` in their header (`jose_utils.make_id_token` writes `alg` and
`kid` alone, and jwcrypto 0.8 adds nothing), and the verifier calls
`JWT.verify` with its default `checkHeaderType: true`, so it refuses every one, and
takes no options to change that. It also tries every key rather than the one the
`kid` names, never fetches the keys again for an unknown `kid` (an hour of failed
sign-ins after SimpleLogin changes its key) and fetches them without a timeout. So:
`checkHeaderType` off, `RS256` alone, the key the `kid` names, fetched again for one
unknown, with a timeout (over adapting a copy of Serverpod's verifier, or `jose`); in
the OpenID Connect module the sign-in is built on (ADR 0023).
- **D13 Attempts in a table** (recommended, over the server's memory): a restart or a
deploy in the middle of a sign-in loses none, and the database takes each once.
- **D14 Steps 2 to 4 in one release** (recommended, over the server's side alone first):
the module's side, the keys without SRP with handles and without anonymous accounts,
and the apps' screens come as one release, so people get a sign-in that works; the
cut-off (step 5) follows 30 days after it.
- **D15 The proposal (1–12) as written** (23 Oct, evening); what building it shows
otherwise comes back to the developer.
- **D16 A newcomer's identity waits 30 minutes** (23 Oct, night, recommended, over the
attempt's ten minutes or a day): long enough to choose a handle and make the keys, or
to find the recovery phrase and mend the link (8); after that they sign in with
SimpleLogin again, whose consent page they then skip. Nothing is reserved meanwhile,
the suggested handle neither.
- **D17 Handles suggested as `anna_mueller`** (23 Oct, night, recommended): the name in
lower case, its words joined by underscores; German's letters as German writes them
without (ü as ue, ß as ss), æ, œ and þ as ae, oe and th, every other Latin letter
without its mark (é as e, ø as o); apostrophes dropped, anything else between words;
cut to 30 characters, and where the handle is taken (whatever its case) or kept by the
instance, a number from 2 to 99 after it. None where nothing of the name is left (a
name in another script): the person types one.
- **D18 Signing up in one call** (23 Oct, night, recommended, over the proposal's two
steps): the device takes the handle, makes the keys, asks the password twice and shows
the recovery phrase (three of its words typed back), then `simpleLogin.signUp` makes
the account and its session together, the newcomer's identity taken once. No account is
ever unfinished, so D11's deletion after a day has nothing left to delete.
- **D19 The password proven with a proof key of its own, the keys only after it** (23
Oct, night, recommended, over the keys coming with the session, as Bitwarden's SSO
has it, and over keeping SRP): a third subkey of the password's Argon2id seeds an
Ed25519 key, whose public half the register keeps; the device signs a one-time nonce
with it as it signs with the recovery phrase's (ADR 0017, the same challenges and
message). A new device gets the wrapped keys once it has proven the password, so
someone who took over a SimpleLogin account can only guess the password online,
against the limits, never offline. SimpleLogin accounts keep no SRP verifier.
- **D20 Changes take the password and a fresh SimpleLogin sign-in** (23 Oct, night,
recommended, over the password's proof alone): a new password, a new recovery phrase
and deleting the account take the password's proof (the phrase's, for a lost
password) and a SimpleLogin sign-in of the last ten minutes, the two factors of ADR
0017's third reading with SimpleLogin's and Proton's own second factor in TOTP's
place.
- **D21 Adding SimpleLogin is a shared part of the account section** (24 Oct,
recommended, over a sheet of its own or each app's own screen): halkyon_app's
SimpleLogin part, which each app places above "Password and authenticator" (Learn in
its Profile tab's account card, below "Ways to sign in"; Todo in its account sheet):
a heading and Add SimpleLogin ("Sign in with SimpleLogin, or with your Proton
account."), which opens the sign-in sheet to add it; once added, one more way in, a
row without heading lined up with the others, and "SimpleLogin added." Learn's ways
in (GitHub, the address) stay listed. (Its reminder in red went with D24.)
- **D22 The reminder is a banner at the top of each app's home** (24 Oct, recommended,
over the section's red line alone or a notice once a week), in the place and colours
of the banner saying the app is too old for the instance, from the release on, not
dismissible, until SimpleLogin is added: both days and Add SimpleLogin. Dropped by
D24 the same day.
- **D23 Anonymous people keep their account with SimpleLogin alone** (24 Oct,
recommended, over offering GitHub beside it until the cut-off): where the instance
offers SimpleLogin, Learn's Profile offers an anonymous person Add SimpleLogin ("Add
SimpleLogin to keep it") and no longer "Sign in with GitHub", and GitHub is added to
nobody there (`GitHubOffer.linking`); the start screen still signs in a GitHub
account someone has, until GitHub ends.
- **D24 No cut-off machinery while the developer is learn.halkyon.eu's only person**
(24 Oct, the developer: "There is no other user than me, currently. So, is that really
necessary?", then "Please continue" to dropping it): the cut-off (D8) becomes
configuration. The developer adds SimpleLogin to their account (D21), then the
operator takes GitHub's OAuth app out of `deploy/.env` (`GITHUB_CLIENT_ID`,
`GITHUB_CLIENT_SECRET`), `SIGN_IN_WAYS=simplelogin` keeping passwords and codes off.
So no cut-off day, no reminder (11, D22, the section's red line), no ending of
sessions and no deletion 30 days later (D10), no step 5, and no
`ANONYMOUS_SESSIONS_END` (10), as no other instance has anonymous people to move.
Built and unreleased, the day and the banner went again at once (over keeping them
idle): nothing has read the basics' fields, and an instance with people to move can
take them back from git (`f60d52e7` on `feat/simplelogin-sign-in` before it was
amended). Anonymous accounts still go (D4) and handles are still required (D5): that
is where the design ends, not a migration.
- **D25 Anonymous accounts that exist are deleted** (24 Oct, night, recommended, over a
way to keep them until the developer has moved): the developer's own account isn't
anonymous. From the release on, an app signs an anonymous session out, and the
module's daily pruning deletes every anonymous person left, whatever their age,
through the same deletion as any account's (each service's own data with it). Nothing
turns an anonymous account into one with a way in any more: D23's anonymous half,
10's "until the cut-off", GitHub's upgrade, a password or SimpleLogin added to one.
- **D26 No handle suggested at password sign-up** (24 Oct, night, recommended, over the
address's part before the @): the field starts empty and says whether the handle is
free as it is typed; a suggestion from the address would show part of it to everyone.
- **D27 Removing a profile makes it private** (24 Oct, night, recommended, over
refusing it): the handle stays the account's, what is filled in goes (display name,
bio, links, avatar), and `/@handle` answers as for an unknown handle until the person
goes public again (D5). Once they have published something it is refused as before:
others refer to the handle.
- **D28 No prompt for an account without a handle** (24 Oct, night, recommended, over a
step after signing in): the developer sets theirs in Profile; a handle is asked where
one is required and the account has none (a password added, the keys set).
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>
2026-10-24 18:09:04 +02:00
- **D29 The handle is chosen while the code travels** (24 Oct, recommended, over a step
before the address, which would let anyone ask about handles without mailing a code,
and over one after the code, which would use the code up on a taken handle): the step
comes after the phrase check has mailed the code and before it is typed, and its free
check is tied to that code request.
- **D30 One handle field everywhere** (24 Oct, recommended, over keeping SimpleLogin's
own): the same field, checking as it is typed, for password sign-up, setting a
password, SimpleLogin's sign-up and its keys step; the handle the instance suggested
counts as free without asking.
- **D31 A reset of the keys after 72 hours, with a banner** (24 Oct, evening,
recommended, over 7 and 14 days): the password and the recovery phrase both lost, a
SimpleLogin sign-in asks for new keys (`keys.startReset`), which a SimpleLogin sign-in
carries out 72 hours later (`keys.completeReset`). No mail tells the owner, as
SimpleLogin is their way in and its mailbox may be what was taken over: every
signed-in device of theirs shows a banner meanwhile, and the next SimpleLogin sign-in
says when it takes effect. Any proof of the password or the phrase calls it off: the
keys opened on any device, a lost password set with the phrase, a link mended. The row
is ADR 0017's account reset (kind `account`, with its lapses, no hold-outs); only an
account that signs in with SimpleLogin alone has one this way, and ADR 0017's reset by
mail is for passwords that sign in alone, so a mailbox never resets a SimpleLogin
account. The handle, the profile, the link and what was published stay; what was
encrypted under keys no device holds any more is lost (Learn shows it as Locked).
- **D32 Called off with the password's proof** (24 Oct, recommended, over the password
and a fresh SimpleLogin sign-in, which guards little, as a SimpleLogin account taken
over is what starts a reset, and over the session alone, which would let a thief who
holds one block the owner): `keys.callOffResets` from a signed-in device, under the
banner.
- **D33 A device that holds the keys keeps them** (24 Oct, recommended, over new keys
always): finishing the reset there wraps the same keys under the new password and
phrase, so nothing is lost.
- **D34 A password that signs in gets the keys' proof key** (24 Oct, recommended, over
leaving the gap for later, and over unlocking with SRP): found while planning D31, an
account with ADR 0017's password that added SimpleLogin had keys but no proof key, so
no new device signed in with SimpleLogin could open them. Its password and a code (ADR
0017's owner proof) set the proof key that same password makes
(`password.setProofKey`): on adding SimpleLogin, and again after a new password of ADR
0017's, which takes it away, as it was made of the old one (over carrying it in ADR
0017's new password, which would change those proofs' digests). Such an account opens
its keys through `keys.unlock` but changes nothing through `keys`: its changes and
resets stay ADR 0017's, which keep SRP's verifier in step. `simpleLogin.complete` says
whether the password signs in and has the proof key.
feat(app): a reset of the keys without the phrase, its banner, and the keys' proof key for a password that signs in (ADR 0020, D31–D35) SimpleLogin's sheet, its password and its recovery phrase both lost: under the lost-password step's phrase field, Lost your recovery phrase too? says what stays and what is lost and asks for new keys (`keys.startReset`) with the sign-in of a moment ago, which the device then drops. The next sign-in says when while it waits, and once due offers new keys, the sign-up's own password and phrase steps (`keys.completeReset`), or the password, which calls it off (D31). Every app's home shows `KeysResetBanner` where "too old" shows, and the account section's SimpleLogin row the same words (`KeysResetNotice`), each with Call it off, the password's proof alone (`keys.callOffResets`, D32), and, once due on a device that holds the keys, Finish here, which wraps the same keys anew (D33, D35). Both show only a reset of an account that signs in with SimpleLogin alone; ADR 0017's resets show in its own section, which now shares `AlertBox` with them. An account whose password signs in has no Forgot your password? there, and is asked its password and a code once while its keys lack the proof key, at a sign-in or on adding SimpleLogin (`password.setProofKey`, D34). `PasswordUnlock` carries the proof key, which its forgetting leaves, and the keys' password proves for a purpose given. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-24 22:37:17 +02:00
- **D35 The banner on each app's home and in the account section** (24 Oct,
recommended, over the account section alone, as ADR 0017's resets show, which the
owner sees only on opening it, and over the home alone): at the top of the home,
where an app too old for its instance shows, and under the SimpleLogin row, each with
Call it off.
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
- **D36 GitHub goes from the apps and the instances** (25 Oct, the developer: "Can we
now purge GitHub from the application and server?", once their account signed in
with SimpleLogin on both their devices and learn.halkyon.eu's `deploy/.env` held
GitHub's OAuth app no more; over keeping it until ADR 0017's D2 ended it on
`GITHUB_SIGN_IN_ENDS`, 22 Jan 2027, which D24 had left in place): nothing signs in
with GitHub. halkyon_base loses the `gitHubSignIn` endpoint with its refusals, the
GitHub identity provider, `githubClientId` and `githubClientSecret`,
`GITHUB_SIGN_IN_ENDS` and the pruning that forgot GitHub IDs after it
(`forgetGitHubAccounts`), the register's `account-with-github`, the basics'
`gitHubClientId` and `gitHubSignInEnds` (Learn's `instance.info()` its
`gitHubClientId`), `SignInWays.gitHub`, and `PasswordRefusal`'s `signInAgain` and
`noAddress`. A first password takes a code mailed to the person's own address, so an
account without one (SimpleLogin's alone, or one with a GitHub ID left) sets none
(`wrongCredentials`), and the account section offers it none. halkyon_app loses the
start screen's GitHub button and its problems, the password offered right after a
GitHub sign-in, and `ServerpodAuthGateway`'s browser half, which only GitHub used;
`LoopbackWebAuth` and the apps' schemes stay, SimpleLogin's ways back.
- **`minimumAppBuild` 24** (recommended, over keeping `SignInWays.gitHub` as a field
always false so that build 23 goes on working): builds up to 23 (v0.20.0) require
the field, so the server without it deploys with the release of build 24, as the
module's did with build 21.
- **The IDs left are the operator's to delete** (recommended, over a migration
deleting them, which would run on every instance whether its people had moved or
not, and over a step of the pruning): Serverpod's `serverpod_auth_idp_github_account`
stays as Serverpod's migrations make it (dropping it would set the module's
migrations against Serverpod's), and a row goes with its account (`ON DELETE
CASCADE`). An account GitHub alone signed in has no way in any more: the first query
lists them, the second deletes every ID (on learn.halkyon.eu through `docker compose
exec -T postgres psql -U halkyon_learn -d halkyon_learn`). The backups keep the IDs
until their snapshots roll over, three months at most (`deploy/backups.md`).
```sql
-- The people GitHub alone signed in: no SimpleLogin, no address.
SELECT g."authUserId" FROM "serverpod_auth_idp_github_account" g
WHERE NOT EXISTS (SELECT 1 FROM "halkyon_oidc_identity" o
WHERE o."authUserId" = g."authUserId")
AND NOT EXISTS (SELECT 1 FROM "email_identity" e
WHERE e."authUserId" = g."authUserId");
-- Then every GitHub ID: nothing reads them any more.
DELETE FROM "serverpod_auth_idp_github_account";
```
- **Released with the next changes** (over a release of its own).
- **Halkyon Train (29 Oct 2026):** the forwarding page's table gains Train's ways back,
`train-loopback` on port 43825 and `train-scheme` (`app.halkyon.train://`), so that
Train signs up and in with SimpleLogin as Learn does; Train's app had taken Todo's port
43824 over with its shell, which would have sent a sign-in begun on one to the other on
the same desktop. The page is the register's, so Train signs in with SimpleLogin once
learn.halkyon.eu runs it; a development server of Train's needs the instance's
SimpleLogin app (`simpleLoginClientId`, `simpleLoginClientSecret`) and
`SIMPLELOGIN_CALLBACK_URL` naming learn.halkyon.eu's page.
- **Also (23 Oct):** the developer's own account moves by adding SimpleLogin to it while
signed in with GitHub; the instance's SimpleLogin app goes up for approval once
halkyon.app/learn exists, before learn.halkyon.eu switches.
## Proposal
1. **The instance's ways.** The operator's SimpleLogin app: `simpleLoginClientId` and
`simpleLoginClientSecret` in the passwords, on the register (ADR 0016), which alone
exchanges codes. `SIGN_IN_WAYS`: `simplelogin`, `password` or both; unset, every way
whose keys are there. On learn.halkyon.eu `simplelogin`. The instance's basics say
which ways it offers.
2. **One SimpleLogin app per instance, the register's.** Every service of the instance
signs in through it (Todo's too), as ADR 0016 has them sign in through the register;
one app means one ID per person on the instance.
3. **The flow.** The app asks its server to begin (`simpleLogin.begin`, with or without a
session; a member forwards to the register): the register keeps an attempt (a random
ID the app holds, a `nonce`, which app and platform, the caller if signed in; ten
minutes, taken once) and answers the authorization address. The app opens it in the
browser (`flutter_web_auth_2`, as GitHub's). SimpleLogin sends the browser to
`https://<register's host>/simplelogin-callback`, the one address registered. The
instance's page there forwards code and `state` to the app (D9): the loopback on
Linux and Windows (`http://127.0.0.1:43823/simplelogin-callback`, each app its own
port) or the app's scheme on Android, iOS and macOS
(`app.halkyon.learn://simplelogin-callback`). Its targets
are fixed per app and way back, chosen by a marker in `state`, so it sends nobody
elsewhere; SimpleLogin's errors (a denial) are forwarded alike. The app hands the code
with its attempt's ID to `simpleLogin.complete`.
4. **Checking.** The register exchanges the code and checks the ID token: RS256 against
SimpleLogin's keys (`/jwks`, kept, fetched again for an unknown key ID), the issuer
`https://app.simplelogin.io`, the audience its client ID, not expired, and the `nonce`
the attempt's. So a code caught on its way to the app is worthless: it can only
complete the attempt it was asked for, whose ID never left the app. (A hostile app
on the device beginning an attempt of its own isn't stopped, verified links or not
(D9): SimpleLogin answers without a consent page for an app already allowed.) It keeps the `sub` alone
(`simplelogin_identity`: account ID, `sub`, each unique); the name serves only as the
handle's suggestion at sign-up (D5); address, name and avatar are never stored, and
SimpleLogin's userinfo is never asked.
5. **A known ID** gives a session for its account (on a member, the member's own, ADR
0016). A signed-in caller adds SimpleLogin to their account; a `sub` another account
has is refused, and nobody is switched to another account.
6. **Signing up** (D6, "New here?"): the handle (suggested, D5), then the keys on the
device: made (ADR 0017, 3), a password to unlock them (asked twice, never sent), the
recovery phrase shown once and three of its words typed back; the register keeps what
ADR 0017 keeps of them (`keys.set`: salt, costs, public keys, both wrappings), without
SRP's verifier, an address or TOTP where SimpleLogin is the way in. Until the keys are
set the account is unfinished: its session does nothing else, and one unfinished for a
day is deleted (D11).
7. **A new device** gets the wrapped keys and the salt with the session, and the password
unwraps them there. Changing it takes the old one on the device (the keys rewrapped and
sent); a lost password with the phrase rewraps them; without it, new keys, and what was
encrypted with the old ones is lost (ADR 0017, 6). No TOTP of ours: SimpleLogin and
Proton have their own second factor.
8. **Mending a broken link** (D7, "I already have one"): the handle and the recovery
phrase. The register answers the keys wrapped under the phrase and a nonce; the device
unwraps them and signs (`halkyon key proof v1|`, as ADR 0017's resets); the new `sub`
takes the old one's place. A handle nobody has gets a decoy wrapping and the same
refusal, as ADR 0017 does for addresses; at most 10 tries an hour per handle and 30
per network.
9. **Handles** (D5): required at sign-up, on every instance; `/@handle` answers as for an
unknown handle until the person publishes something or fills in their profile; asking
whether a handle is free stays limited per person and network, since its answer tells
that the handle exists.
10. **Anonymous accounts go** (D4): `anonymousIdp` is removed; the first start offers the
instance's ways and other instances. On learn.halkyon.eu, anonymous people can add
SimpleLogin (and a handle and keys) until the cut-off (D8); another instance's
anonymous people add a password until a cut-off their operator sets
(`ANONYMOUS_SESSIONS_END`, a day, as `GITHUB_SIGN_IN_ENDS`).
11. **The cut-off on learn.halkyon.eu** (D8), 30 days after the release: the app reminds
everyone without SimpleLogin from the release on, saying the day and what follows;
then their sessions end, and 30 days later their accounts are deleted (D10).
12. **The privacy page** says that SimpleLogin learns when someone signs in to Halkyon,
that the instance keeps only SimpleLogin's ID for its app, and that the name helps
suggest a handle once and the rest of what the consent page shares is dropped; the
sign-in screen says so before the browser opens. Store listings, the README and the
website stop saying that the app starts with an anonymous account.
## Consequences
- People need a SimpleLogin account (a Proton account gives one) and a connection to sign
up or sign in on a new device; nothing works signed out, on any instance.
- The instance depends on SimpleLogin's sign-in for apps, which SimpleLogin closed to new
apps on 22 Oct: if it ends altogether, the operator turns `password` on, and people set
a password and an address (ADR 0017); their keys stay.
- Operators who can't get a SimpleLogin app keep ADR 0017's way in; the code keeps both.
- Everyone has a handle, which stays private until they go public.
- The keys' password is one more thing to keep, and the recovery phrase also mends a lost
SimpleLogin link.
- A hostile app on a person's device can begin a sign-in of its own and, once the person
has allowed Halkyon at SimpleLogin, come back with a session without a consent page
(4, D9); what it gets is the account's server-side data, while private studysets,
tags and settings stay sealed under the keys (ADR 0019). Whether SimpleLogin honours
OpenID Connect's `prompt=consent`, which would show the consent page every time, is
still to be checked.