A handle alone is private: its page and `profiles.byHandle` answer as
for a handle nobody has until its person fills in their profile or
publishes something. The hint under it said "Others find you as @…"
all the same, also after removing a profile (D27). It now says it is
private until then, by the rule the instance's `isPublic` applies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
An account that signs in with SimpleLogin alone, its password and its
recovery phrase both lost, asks for new keys with a SimpleLogin sign-in
(`keys.startReset`); another sign-in carries it out 72 hours later
(`keys.completeReset`, `notDue` with when before), with the same keys
rewrapped where the device still holds them (D33). No mail: every
signed-in device shows it, and any right proof of the password's or the
phrase's key, or a mend, calls it off; `keys.callOffResets` takes the
password's proof alone (D32). The row is ADR 0017's account reset of kind
`account`, so no migration; ADR 0017's reset by mail now takes only an
account whose password signs in.
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: `password.setProofKey` sets the one that same password makes, with
the owner's proof binding it (D34), and ADR 0017's new password takes it
away. Only an account that signs in with SimpleLogin alone changes
through `keys`. `simpleLogin.complete` says whether the password signs
in, whether the keys have the proof key, and when a reset takes effect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
An account that signs in with SimpleLogin opens its keys with its
password. Where that is lost, the unlock step's "Forgot your password?"
opens one step: the recovery phrase, and a new password twice. There is
no address and no code: the SimpleLogin sign-in of a moment ago stands
for them (D20).
- The device opens the keys with the phrase (`keys.beginPhrase`) and
wraps them under the new password (`AccountCrypto.newKeysPassword`).
It then signs the phrase's proof bound to that password
(`signKeysResetProof`), which `keys.resetPassword` takes with the
sign-in. The keys stay, and every other session ends.
- A phrase that doesn't open the keys is refused on the device, and the
instance gets no proof to count. Two passwords that differ are
refused before anything is sent.
- The gateway's `beginPhrase` and `resetPassword` run on the account's
own session, not yet the device's. The repository's `resetPassword`
ties them together, and the cubit and sheet have their `forgot` step.
Three texts are new, in English and German.
- halkyon_base_server exports `isKeysPassword` for the interop test,
which checks against the register's checks, with real libsodium, what
the device sends.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
CI failed on d5651c5b (run 165): the path editor's test waited 60 ms
for a save its 20 ms pause sets off, and on the runner, its CPU capped,
the timer had not fired yet. The code was the same that had passed on
the laptop.
- The path editor's test waits until the bloc has saved what was typed,
as the step editor's test does.
- The study session's test waits until the deletion held through the
session has gone, and the Linux Pomodoro platform's tests until the
alert, the registration and the suggestion have come over D-Bus,
through `eventually` (test/support), each where a fixed wait stood.
- Waits before checking that nothing happened stay as they are: a slow
machine can only make those pass more easily.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Every account keeps a handle now, so removing a public profile no
longer deletes it and frees the handle: it takes away what the profile
shows and leaves it private, as a profile is until its person goes
public (D5).
- `profile.remove` and the register's `removeProfile` (members ask the
register) empty the display name, bio, links with their checks and
the avatar, keep the row with its handle, and return what is left,
null for none. `/@handle` and `profiles.byHandle` then answer as for
a handle nobody has. Once the person has published, removing is
refused as before.
- Learn: the profile form starts afresh from what is left, "Remove
profile" shows only while something is filled in, and the dialog says
the handle stays.
- The module's README and ADR 0020 amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
The server's half of D25: every account has a way in, and the anonymous
accounts left from before go with the daily pruning.
- The module signs nobody in anonymously: `anonymousIdp` and
`gitHubUpgrade` are gone, and so is linking GitHub at the register.
- A password is set only by someone whose address is theirs (a
`firstPassword` code) or who signs in with GitHub alone (a fresh
sign-in and a `signUp` code); an anonymous account is refused
`wrongCredentials` before any code is used.
- SimpleLogin refuses to link onto a caller without `identified`
(`wrongCaller`), the member's endpoint too, so nothing turns an
anonymous account into one with a way in.
- `pruneAnonymousPeople` deletes every anonymous account left, however
new, never one identified. Learn and Todo schedule it as
`prune-accounts` and cancel the old `prune-abandoned-anonymous-people`
entry once.
- Learn: someone without `identified` keeps no files;
`FILE_LIMIT_ANONYMOUS_MB` is gone. `minimumAppBuild` is 23, as builds
up to 22 start with an anonymous account.
- Learn's sync tests make their person and session by SQL in the
development database, as Serverpod stores sessions.
- Docs, store listings and ADRs 0010, 0014, 0016, 0017 and 0019 amended.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Every account has a way in, so the apps make no anonymous account any
more. The start screen shows whenever nobody is signed in, a first
start included, and offers SimpleLogin, a password and GitHub for those
who have it, with no start without an account. An instance out of reach
is said so, with Try again, and one offering no way to create an account
too. An anonymous account's session left from before is deleted as the
app starts (D25), falling back to signing out on the device.
What only anonymous accounts needed goes: signing in anonymously, the
signed-out mark, adding GitHub to an account and switching to a GitHub
account from the Profile tab (with its Cancel, the only one), an
anonymous device's own keys, the warning that signing out deletes the
account, and the texts about keeping progress. Settings says on which
instance the account is signed in. Learn's publishing, reports and
suggestions and its learner count no longer ask whether the person has
an identity: everyone signed in has one.
The server still offers anonymous sign-in until the next commit, so
older apps keep working until v0.20.0 raises the minimum build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
halkyon_app reads where the device's person stands with SimpleLogin
(nothing to show, linked, or missing) and shows it in SimpleLoginSection,
which each app places above the password part: Add SimpleLogin, once
added one more way in. GitHub is added to nobody where SimpleLogin is
offered, and Learn's Profile offers an anonymous person SimpleLogin
alone there (D23).
No cut-off day, reminder or banner (D24): with the developer the only
person on learn.halkyon.eu, the cut-off is the operator taking GitHub's
OAuth app out of the deploy's settings once that account has SimpleLogin.
ADR 0020 records it and drops step 5.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
SimpleLoginRepository signs in with SimpleLogin in the browser
(OidcBrowser: the app's scheme on Android, iOS and macOS, the app's port
on the loopback on Linux and Windows, D9), then makes the account's keys
and signs someone new up, opens an account's keys with the password, gives
an account of before its first keys, or links a new SimpleLogin account in
place of a lost one with the recovery phrase (D7), which opens the keys on
the device first: a phrase that doesn't fit sends no proof, and no
password is asked after. An anonymous person adding SimpleLogin keeps the
keys their device holds. Nothing is the device's until `enter`.
SimpleLoginGateway calls halkyon_base's `simpleLogin` and `keys`
endpoints (ServerpodSimpleLoginGateway), with a fake for tests, as does
FakeOidcBrowser. The calls made on clients of their own (signed out, or as
a session not yet the device's) move from the password gateway into
BaseCalls, which both share.
Fixes BaseProtocol, the client of halkyon_base alone, which didn't read
halkyon_oidc's classes: every refusal of SimpleLogin's sign-in on a
signed-out call would have reached the app as a failure. OidcBrowser keeps
its loopback private, so that a fake can implement it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
AccountCrypto makes the keys of an account that signs in with SimpleLogin
(newKeys): new key pairs or an anonymous person's device keys, wrapped
under the password and a new recovery phrase, with the public half of the
password's proof key in place of SRP's verifier. It proves the password to
open them (proveKeysPassword: one Argon2id for the proof and the unwrapping)
and the recovery phrase to link a new SimpleLogin account in place of a
lost one (signMendProof, bound to the attempt). The interop tests check all
three against the register's own checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
OidcBrowser opens the page of an attempt the server began (OidcStart),
waits until the browser comes back by an OidcReturn (the app's scheme,
a verified https address, or the loopback) and returns the provider's
code (OidcCode), its refusal (OidcDenied), or nothing when the person
closed the browser. It checks the returned state against the attempt's,
as the server never compares it; a wrong state, neither one code nor
one refusal, no browser or a failed way back are OidcCallbackExceptions.
halkyon_app's LoopbackWebAuth moves in with its tests, relicensed from
AGPL v3 to BSD-3-Clause by its author, the developer; GitHub's sign-in
keeps using it from there until it ends. The package is BSD-3-Clause,
publish_to: none, as the module (ADR 0023, D2, D3).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
simpleLogin.begin takes the way back (SimpleLoginReturn: loopback,
scheme) rather than the platform, so macOS can come back through its
scheme as iOS and Android do; the markers become learn-loopback,
learn-scheme, todo-loopback and todo-scheme. Not breaking: the
endpoint has never been released.
D9 corrected by the developer after research: Android, iOS and macOS
come back through the app's scheme, Linux and Windows through the
loopback. Verified links protected nothing here: a code caught on its
way is worthless without the attempt's ID, and a hostile app beginning
an attempt of its own can ask for a way back that other platforms need
open. The ADR notes the residual risk and the check of prompt=consent
still to do. assetlinks.json stays served.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A handle's page 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 (isPublic). A person's own profile, and
the host's own lookups (personWithHandle), are unchanged.
So every answer about a new handle now tells that an account has it,
and countHandleCheck counts them together on the register, 30 an hour
per person (or attempt) and 60 per network, refused beyond with
ProfileRefusal.tooManyHandleChecks: profile.checkHandle and saving a
profile with a handle new for the person, simpleLogin.checkHandle and
signUp, keys.set with a handle, and Halkyon Learn's inbox.block and
unblock, which look people up by handle. A person's own handle and what
is no handle aren't counted. The SimpleLogin limiters of commit 3 give
way to it, so a network's 60 are shared between people signed in and
people new. The register's checkHandle, saveProfile,
signUpWithSimpleLogin and setFirstKeys carry the visitor's network from
the member that asked.
Halkyon Learn's handle dialog words the new refusal (English, German).
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
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>
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>
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>
`takeKept` takes a `transaction`, so a host's step that fails (a name
taken meanwhile) gives the identity back instead of losing it; `isKept`
tells whether an identity waits under an attempt for its caller, so a
host can check before asking more of the person.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A third subkey of the password's Argon2id (context `hlkpass1`, subkey 3)
seeds an Ed25519 pair: `PasswordKeys.proofKeyPair` and `proofPublicKey`.
An account that keeps no SRP verifier proves its password by signing the
register's nonce with it, as it proves the recovery phrase; neither SRP's
verifier nor the unwrapped account keys a signed-in device keeps can make
it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
D18 to D20, the developer's, before step 3: the device makes the handle,
the keys and the recovery phrase before one `simpleLogin.signUp`, so no
account is ever unfinished; a third subkey of the password's Argon2id
seeds an Ed25519 key the register checks, and a new device gets the
wrapped keys only after proving the password with it; a new password, a
new phrase and deleting the account take the password's proof and a
SimpleLogin sign-in of the last ten minutes. Step 2's deviations from the
proposal are noted as accepted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The `simpleLogin` endpoint on halkyon_oidc's SimpleLogin preset: `begin`
for the app and platform (the state's marker for the forwarding page), at
most 30 an hour per network; `complete` with SimpleLogin's code, signed out
a session for the account with that SimpleLogin ID, signed in SimpleLogin
added to the caller's account, who becomes identified and anonymous no
longer. Someone new gets no account yet: their identity waits 30 minutes
under the attempt's ID (D16), with a handle suggested from their name,
`anna_mueller` (D17), for the sign-up of step 3.
The register alone holds the instance's SimpleLogin app
(`simpleLoginClientId`, `simpleLoginClientSecret`) and exchanges codes; a
member asks it for both steps and issues its own session. `SIGN_IN_WAYS`
says which ways the register offers, the instance's basics and the ways
in carry `simpleLogin`, GitHub links nobody more where SimpleLogin is
offered, and Learn's and Todo's pruning forgets the unfinished sign-ins
hourly. No migration: the tables are halkyon_oidc's, already in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The module sends the client's ID and secret form-encoded inside Basic
authentication, as RFC 6749 (2.3.1) has them; the fake compared the header
with them raw, so a host testing with a secret holding a space or a colon
saw every code refused. A test now holds the module to the encoding.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The module's test provider moves into `package:halkyon_oidc_server/testing.dart`,
so a host tests its own sign-in against the module's real checks rather than
a copy: `TestKey` makes and publishes an RSA key and signs ID tokens with
headers written by hand (no `typ`, as SimpleLogin's), and `FakeProvider`
answers discovery, the keys and the token endpoint through an HTTP client
the host hands the module. `FakeProvider.simpleLogin` has SimpleLogin's
issuer and endpoints, without PKCE, as the preset expects; a test now signs
in through the preset end to end. pointycastle, already in the lock through
dart_jsonwebtoken, becomes a dependency for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
halkyon_base depends on halkyon_oidc_server, and its forwarding page
is now the module's (`addOidcCallbackForwarding`) with Halkyon's table
of apps and its page in English or German; its tests keep what is
Halkyon's own, the rest is tested in the module.
Every host now includes the module: migrations `…-oidc-module` in
halkyon_base, Learn and Todo create its two tables (no data touched)
and record its version; Learn's devices get them empty, as they have
halkyon_base's server-only tables. halkyon_base_client depends on
halkyon_oidc_client, whose models its generated code uses, and Learn's
server image copies both packages into its narrowed workspace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
halkyon_oidc_server and its generated client halkyon_oidc_client: any
provider following OpenID Connect's code flow, beside Serverpod's own
providers, with a preset for SimpleLogin (no PKCE, no `typ`). Nothing
of Halkyon's is in it; BSD-3-Clause, `publish_to: none` for now.
- Attempts in `halkyon_oidc_attempt` (ADR 0020's D13): the ID
PostgreSQL's gen_random_uuid(), a state and a nonce of 32 random
bytes, PKCE's verifier where used, the caller if signed in; ten
minutes, taken once. A host may begin the state with a marker.
- The code exchanged by the module itself, so a provider out of reach
(a timeout, a 5xx) is told apart from a refused code; the ID token
checked with dart_jsonwebtoken's algorithms (ADR 0020's D12): the
provider's algorithms alone, the key its `kid` names, of the right
type; issuer, audience and `azp`, expiry, time issued, `nbf`, a
minute's leeway; the attempt's nonce; a `sub`.
- Keys kept an hour, fetched again for an unknown `kid` at most once
a minute; discovery checked against the issuer; every request with
a timeout.
- Identities in `halkyon_oidc_identity`: the provider's key and the
`sub`, one per person and provider, deleted with the person.
- The stock endpoint (`OidcIdpBaseEndpoint`), and the parts for a host
with rules of its own: begin, verify, keep and takeKept, link,
accountOf, unlink; merging people; a limit per network.
- The forwarding page, moved here from halkyon_base in the next commit.
The package's main library is the code generator's, as halkyon_base's;
a host imports `package:halkyon_oidc_server/oidc.dart`. ADR 0023's
point 5 no longer names OAuth2PkceUtil, which the module doesn't use.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The developer's decisions of 23 Oct: the sign-in of ADR 0020 becomes a
Serverpod module others can use, for any OpenID Connect provider with
SimpleLogin as a preset, under BSD-3-Clause, in this repository until it
runs on learn.halkyon.eu, built now as ADR 0020's step 2. Serverpod v4.0.4
has a contract for providers of one's own but nothing of OpenID Connect.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The developer's decisions of 23 Oct, evening (D12–D15). Serverpod's own
ID-token verifier, chosen first, refuses every SimpleLogin token: they
carry no typ header, and it checks one with dart_jsonwebtoken's default,
with no way to turn that off. So the register checks them with
dart_jsonwebtoken itself. Attempts go into a table, steps 2 to 4 come in
one release, and the proposal stands as written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
On 22 Oct a cancelled job left its analysis server running in the
runner, 4 GB, until someone noticed. forgejo-runner v13.1.0 starts every
host step as a process group and sends the group SIGKILL on a cancel or
at timeout-minutes (act/common/cmdgroup.go), and after that no later step
runs, `if: always()` ones neither (they get the cancelled context; only
actions' post steps get a fresh one), so a cleanup at the job's end
can't be relied on. The check job's second step, after the sandbox's
proof, stops every process of a job's working folder (`$HOME/work/`,
also one removed since) that is neither the runner nor the step itself,
TERM then KILL after 5 s, and names each in the log. The runner takes
one job at a time (capacity: 1), so nothing of a running job is touched.
No change on the server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
serverpod_auth_idp_flutter sets `uses-material-design: true`, and Todo's
app and halkyon_app didn't: Flutter warned in every test run ("the
primary pubspec contains uses-material-design: false"), and Material's
icons would draw as boxes in Todo. Learn's app has it already. The lock
stays as it was.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
On 23 Oct a push went red in CI because an endpoint's changed comment
hadn't been generated into the module's client: only CI compared the
committed generated code with the generator's output. tool/check.dart now
runs every server's generation (Melos' `generate`, about 15 s) after
fetching the packages and fails, naming the files, where the working
tree's generated code changed by it; the files stay written anew, to be
committed. It compares the files that differ from the committed ones
before and after by their content, so uncommitted changes count as they
do everywhere else in the checks.
It refuses a Serverpod CLI at another version than the lock holds
Serverpod at (`dart install serverpod_cli@<version>`), and the root's
tests hold CI's SERVERPOD_VERSION to the lock. CI keeps installing the
CLI at that version and puts it on the job's PATH; its own comparison
step goes, as the checks run it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Build 21 (v0.18.0) was the release tool/release.sh was kept for, the one
going with ADR 0014's breaking deploy; every release since is
`dart run tool/release.dart learn`. The comments that named the script
(the F-Droid script, the certificate pin, the Gradle signing) now name
the Dart tool.
Learn's release uploads the snap to the Snap Store's edge channel last,
where v0.18.0 went by hand; the release refuses to start unless
`snapcraft whoami` shows a login.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
SimpleLogin takes only https redirect addresses, each registered exactly,
so the instance's SimpleLogin app registers one page on the register's
web server, /simplelogin-callback, which sends the browser on to the app
that began the sign-in: a 303 to the desktop's loopback (127.0.0.1, a port
per app) or the phone's scheme, chosen by the marker the state begins with
(`<app>-<platform>.<rest>`, the rest 1 to 512 characters of base64url),
from a fixed table, so it sends nobody anywhere else. It forwards the code
or SimpleLogin's error (`deny`) with the state and nothing else, refuses
anything malformed, repeated or not a GET with a page that repeats nothing
it was given, and keeps and logs nothing (no-store, no referrer). It is
built ahead of the sign-in itself so development can sign in through
learn.halkyon.eu's page, which the development app registers too.
Android's assetlinks.json for Halkyon Learn signed with the release key
is answered by the Caddyfile on learn.halkyon.eu alone: the one domain the
project's builds name, and the server knows no app's key. A root test
holds its fingerprint to tool/release/android-certificate.sha256. The
app's intent filter comes with the sign-in; iOS's association waits for
an Apple team ID.
The profile page's language choice and look move to common/web_page.dart,
shared by both pages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The developer decided on 23 Oct to drop the hand-off to an AI assistant
outside the app, and Claude's connector with it, in v0.19.0, the app and
the server together: build 22 raises minimumAppBuild anyway, so no app
calling the connector's endpoints uses the instance after it. An instance
is to have one way to an assistant, its operator's (ADR 0021, proposed).
The app: the field at the foot of Home, and the button in the Library, on
a studyset and while studying, search the web alone, through the engine
chosen in Settings or Android's own search, with the parts of a card the
person picks; a studyset's name and description are never offered. Gone:
the assistants a question opened in (AiAssistant, AssistantHandOff), the
switch between them and the web, Settings' choice of assistant, Connect
Claude with its route, cubit, repository and gateway, and their texts. A
settings file from before keeps the old keys until the next change of a
setting writes it anew; nothing reads them.
The server: lib/src/assistant/ (MCP, its OAuth, the five tools, connect
codes and tokens), AssistantEndpoint, the daily forgetting of the
connector's secrets (its scheduled call cancelled at start), its lines in
the deletion hook, instance.info().assistantConnectorUrl (nullable, so an
older app reads it as none), and Caddy's forwarding of /mcp, /oauth/* and
/.well-known/oauth-*. The migration drops the connector's five tables,
children first:
DROP TABLE "assistant_token" CASCADE; -- tokens of a connection
DROP TABLE "assistant_grant" CASCADE; -- OAuth codes of a connection
DROP TABLE "assistant_connection" CASCADE; -- a person's connected client
DROP TABLE "assistant_connect_code" CASCADE;
DROP TABLE "assistant_client" CASCADE; -- OAuth clients registered
No other table references them, so CASCADE drops only their own foreign
keys. For the minute between the maintenance run and the switch, the old
server answers the connector with errors and fails to delete an account
(its hook still deletes from these tables); ADR 0022 says so.
BREAKING CHANGE: a connected Claude gets no answer from the deploy on, and
the app no longer opens a question in an AI assistant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0022, decided 23 Oct: the hand-off to assistants outside the app and
the Claude connector (ADR 0006) go in v0.19.0, app and server together;
the field keeps the web search. ADR 0021 takes the developer's D4-D6: the
operator's keys for several providers, granted per person; personal keys
on the person's devices, only where the operator allows them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
`91e5b00` changed the doc comments of `asset.finishUpload` and
`learningCount.reportCopies` without running the generator; CI compares the
committed client with `serverpod generate`'s output and would have failed.
Written by the generator as `serverpod start` ran it, not by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Seed 218 had the person make a studyset private on the laptop while the
laptop, kept by the instance from the phone's make-readable, showed it
private already: the app offers only Make non-private there, so no record
of it came. Make private and Make non-private now follow the acting
device's own view; the laptop held at its earlier record is the residual
risk ADR 0019 names.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Found by the hostile-instance test (seed 186): the instance withheld the
laptop's personal key from the phone, so the phone wrote Count me off in a
row of its own beside the laptop's on; then it deleted the phone's row and
handed the key back, and the phone read the laptop's on alone, neither
row's version newer than the other's. A row a device saw as newest that is
gone, without this device folding it and without a row covering it (as
another device's fold always leaves), now leaves out every row it doesn't
cover: Count me falls back to off rather than to what the missing row
outweighed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0019 gets "The final reading (22 Oct 2026)": what decides a key is
bound into what is sealed or signed, the walk up the tree, a device
without the keys writing nothing, settings' newest versions and Count me
folding off-wins, publishing and Finish as shown, the rewrites; the
hostile-instance test, its invariant and what it found, on the final code
and on the code before; the residual risks as they stand. Learn's
AGENTS.md gets the rule that every column deciding a key is bound into
what is sealed or signed, and the test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Seed 77 found the phone showing Count me on after the laptop turned it
off: the instance withheld the laptop's row from the phone and handed back
the phone's own older one. That is a device held at an earlier genuine
record, ADR 0019's residual risk; what holds is that no device goes back
behind what it saw itself, which is what the test now checks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Half of its card revisions pointed elsewhere are sealed ones pointed at a
card of another tree, the final reading's H1, which a choice among all
revisions and cards made rare.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The final reading of 22 Oct: rather than one reading at a time finding a
variant, a seeded test plays the instance. Each run interleaves what the
person does on a laptop, a phone and a device without the keys
(studysets, topics, cards, notes, paths, steps, answers, PDFs, files,
moves, making private or readable, tags, Count me, Finish, publishing,
reading back) with what an instance that alters rows does to the local
database, as a sync would bring it (unsigned columns changed, rows
replayed under their ids, revisions and answers pointed elsewhere,
unsigned rows planted and signed ones copied from elsewhere, key records
withheld, handed back or forged, tags linked, files withheld, settings
folded away and handed back, the register answering no), each followed by
the upkeep after a merge. After every step, against a model kept from the
person's actions alone: nothing private when written is readable in any
row kept or file stored unless the person made it so, a release sends
only the tag names shown, and Count me is on only if the person turned it
on last. 50 seeds of 40 steps (about a minute) on the real gateways over
a local database; HOSTILE_SEEDS, HOSTILE_STEPS and HOSTILE_FROM run more,
or one seed again, and a failure names its seed and every step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>