The recovery phrase, recovery codes and an authenticator's QR code and
key now show inside a SecretScreen: on Android it holds FLAG_SECURE while
it shows (through a channel each app's MainActivity answers, counting
holds, as several may show at once; ScreenGuard.kt), so screenshots,
screen recordings and the recents view don't get them; on iOS, which has
no such flag, it covers them while the app isn't in front, so the app
switcher's picture doesn't hold them (Dart-side, not checked on a device).
Desktops do neither: a window loses focus whenever its person turns to a
password manager. Todo's debug APK builds with it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Equatable's own text lists every field, and a debug build's bloc observer
logs every state, so an authenticator's otpauth URI with its secret, the
recovery codes and the address went into the log. The password states
(signing in, getting back in, adding an authenticator, a step of the
account section) and AuthenticatorSetup now say only where they stand, as
the setup and new-phrase states did; Learn's observer takes a writer so
its test holds what it logs to that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017's "Full reading (21 Oct 2026)": each server finding and what was
done (H1, M1, M2, M3, L1, L4, L5, L6, L7, L10), those accepted with their
reasons (L2, L3, L8, L9), and the device's M1, the owner's proof bound to
its operation and the whole base URL, still to do. "The rules as they
stand" brought up to date; the module README and Learn's AGENTS.md too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The full reading's M1 and L6: once GitHub IDs were forgotten at
GITHUB_SIGN_IN_ENDS, an account that had signed in with GitHub alone
counted as anonymous, so a stolen session and any mailbox gave it a first
password and authenticator; and such accounts, with no way in left, were
never pruned. Now:
- at the end, the daily run that forgets the GitHub IDs first deletes the
accounts that sign in with GitHub alone (no address, so never a
password), with everything they have, members told (deletePerson); the
app tells them for the three months before that their account goes then
unless they set up a password (passwordSectionGitHubEndsDeletes, en/de);
- password.set adds an address without GitHub only for someone anonymous
where the session lives (anonymousHere, which a member tells the
register, as it tells freshGitHubSignIn); anyone else is refused before
any code is used.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The full reading's M3: whoever knows the password made the account's codes
wait, and that wait refused the phrase's owner their reset too, so they were
held out of both without end. Codes now count per account and per way they
came (SecondFactorPath: after the password, after the phrase), each with
its own waits; a code accepted either way clears both.
Migration (module, Learn's and Todo's servers; Learn's device schema
rebuilds the server-only table):
DROP INDEX "second_factor_failures_person_idx";
ALTER TABLE "second_factor_failures" ADD COLUMN "path" text NOT NULL
DEFAULT 'password'::text;
CREATE UNIQUE INDEX "second_factor_failures_person_idx" ON
"second_factor_failures" USING btree ("authUserId", "path");
Existing rows are the password's, unique per account, so unique per
account and way too.
Also the hosts' generated future calls, for the pruning's doc comment the
lapsed resets' commit changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The full reading's M2: a reset of the authenticators asked and forgotten
left the account to the password alone for good, and an account's reset
could be carried out a year after it was asked. Now a reset that took
effect and wasn't carried out lapses a week later, as never asked: the
password alone no longer signs in, a code still does, and asked again it
waits anew with a new mail. A reset of the authenticators carried out never
lapses (the password alone signs in until one is confirmed). The daily
pruning forgets lapsed rows (forgetLapsedResets).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The tenth wrong owner proof in a row ends a session (the third
reading); someone holding that session tried the password or its
codes. The register now keeps the moment with the account's password
(`account_password.sessionEndedForProofsAt`): the account's sign-in
ways and its next password sign-in carry it, until the owner dismisses
it (`password.dismissSessionEndedWarning`, their session alone: it
hides a warning and secures nothing) or sets a new password. A member
ending its own session tells the register
(`noteSessionEndedForProofs`); a register out of reach leaves the
owner unwarned, the session ended all the same.
Migration (module, Learn's and Todo's servers, Learn's device schema):
ALTER TABLE "account_password" ADD COLUMN "sessionEndedForProofsAt"
timestamp without time zone; nullable, so every row keeps its values
and has no warning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017 records what the fourth reading found and what was done, and
states who changes what of an account with which proofs, as a table the
full reading can check against; the module README and Learn's AGENTS.md
follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The fourth reading's first finding: password.set took a session alone,
so whoever took one made a GitHub-only account, or an address account
without a password, theirs for good (their address or password, their
authenticator; GitHub then refused the owner).
- An account with an address proves it anew: a code mailed to that
address for this (EmailCodePurpose.firstPassword, its own mail), never
the session alone, a code asked for anything else, or another address.
- An account that signs in with GitHub alone does it within 10 minutes of
a GitHub sign-in (the session's method and creation time, where the
session lives; members tell the register), refused signInAgain before
any code is used; the address it adds is proved as before.
- Anonymous people still become identified with a code to the address
they add.
The app mails the code to the person's own address (asking for it where
the device doesn't know it), offers a password right after a GitHub
sign-in (D2's "at their next sign-in") before the person's app, and asks
for a GitHub sign-in anew when the instance refuses an older one, keeping
the code typed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Codes by mail were tied to nothing but their address (the re-review's
seventh finding): a hostile instance could answer that a setup never
finished, have the person type a code into it, relay it to start the real
instance's account reset, and 72 hours later relay a second to finish it
with its own password and keys; the mail said "to sign in" whatever the
code was for. A code is now asked for a purpose (signUp, finishSetup,
authenticatorReset, accountReset, deletion), only that purpose takes it (any
other is refused as no longer valid, leaving the code as it was), and its
mail says what it would do ("to reset your account on learn.halkyon.eu: a
new password and new keys, and what was encrypted with the old ones is
lost") and to type it only in the app on that instance, nowhere else. The
app asks for each code with the purpose of its step.
Migration (module, Learn's and Todo's servers, Learn's device schema):
ALTER TABLE "email_code_request" ADD COLUMN "purpose" text; nullable,
so the requests of the last 15 minutes before it stay, taken by nothing
and forgotten within the hour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Wrong codes for removing an authenticator, new recovery codes or confirming
one counted toward the account's waits (the re-review's third finding): a
stolen session alone, without the password, could keep the owner's codes
waiting for up to a day at a time, renewed without end, holding them out
of signing in and of changing the password, the way to end that session,
and nobody was mailed. Those codes now count per session, where the session
is (the register's own, or a member's, which forwards the code): each try
counted before its code is checked (one statement, so tries sent at once
count each), a code accepted clearing the count, and the tenth wrong in a
row ending the session (sessionEnded; the device says it was signed out).
A try that checked no code (a refusal of something else) is taken back.
The account's waits now count only codes after a right password, and the
per-account ten tries an hour, which a session could fill as well, go.
Migration (module, Learn's and Todo's servers, Learn's device schema):
CREATE TABLE "session_code_failures" ("id" bigserial PRIMARY KEY,
"sessionId" uuid NOT NULL, "authUserId" uuid NOT NULL, "failures"
bigint NOT NULL, "updatedAt" timestamp NOT NULL) with a unique index on
"sessionId": a new table, nothing touched. Its rows go with a code
accepted, with the person, and hourly once their session is gone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Caddy's network had IPv4 alone, so Docker's userland proxy forwarded
every IPv6 visitor to Caddy from the network's gateway, and the server
counted all of them as one network (the second reading's M5, reasoned).
The edge network gets a unique local /64 (fd4a:6c6b:7e80::/64), Caddy a
fixed address in it beside its IPv4 one, both trusted as the proxy; the
CI runner's unit denies fd00::/8 and its sandbox check tries Caddy's IPv6
address too. A commit of its own: it needs Docker 27 or later with
ip6tables on the server and the network made anew (compose down, never
-v, then up), checked by hand first (NEXT.md).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Whoever knew the password could guess codes at 10 an hour forever,
unnoticed, each guess checked against every authenticator (the second
reading's M4). Every code an account's second factor refuses after a
right password (sign-in, a phrase's reset, a change) or from a signed-in
device (removing an authenticator, new recovery codes, the code from one
confirmed before) now counts, per account, from every network; after
every tenth in a row no code is taken (codesWait, saying until when), an
hour, then twice as long each time, a day at the most (longestCodeWait,
second_factor_waits.dart): at a day, ten guesses a day still have about a
1 % chance a year against one authenticator, so the first wait mails the
owner that someone knows their password. The instance keeps no address,
so sign-in and the phrase's reset now send the address the device typed
(and its language), used only for that mail after its hash is checked
against the account's. A code accepted clears it all.
Migration (module, Learn's and Todo's servers, Learn's device schema):
CREATE TABLE "second_factor_failures" ("id" bigserial PRIMARY KEY,
"authUserId" uuid NOT NULL, "failures" bigint NOT NULL, "waits"
bigint NOT NULL, "waitUntil" timestamp, "updatedAt" timestamp NOT
NULL) with a unique index on "authUserId": a new table, nothing
touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Anyone who knew an address could spend its whole hour: 10 sign-ins, 5
phrase proofs and 3 codes, each new code voiding the last (the second
reading's M3). Now:
- sign-ins begun count per address and network (10 an hour), phrase
proofs likewise (5), and networks as before (30);
- failed proofs count per address from every network: past 30 an hour
(wrong passwords, phrases' signatures by another key, an address nobody
has alike) no sign-in or proof begins for it anywhere, so guessing from
many networks makes at most 30 guesses an hour, while holding an owner
out takes failing without end from at least three; longer windows
would outlive the hourly pruning of the limits' notes;
- a signed-in change of password begins 10 times an hour per account;
- codes by mail count per address and network (3 an hour), per address
from every network (10, no mailbox flooded) and per network (10), and
up to 3 are valid at once, a fourth voiding the oldest, so codes others
ask for don't void the one its owner is typing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
A key proof was a signature by the account's signing key, which every
signed-in device keeps in its keyring: whoever could read one, with a
code from its authenticator, set a new password without the password or
the phrase (the second reading's H1). The phrase now proves itself with
an Ed25519 key derived from its bits alone (halkyon_keys'
recoveryProofKeysOf, KDF context hlkrprf1, a sibling of the recovery
key), whose public half every setup, new phrase and account reset sends
and the register keeps (account_password.recoveryProofKey); the device
derives the secret half only while the phrase is typed. An account
without one (none exist outside development) proves no phrase until it
sets a new one; decoys check against a decoy proof key alike.
Migration (module, Learn's and Todo's servers, Learn's device schema):
ALTER TABLE "account_password" ADD COLUMN "recoveryProofKey" bytea;
nullable, so rows made before keep working and no data is touched.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017's step-3 section: codes by mail sign nobody in, the ways in
for accounts without a working password, changes while signed in,
sessions ended in every service, the apps' screens (the QR package and
the alternatives considered), the keys in the keyring, anonymous
people's keys waiting for step 4, and libsodium on devices (results
copied before they leave an isolate; a C compiler and make for every
build). The module's and halkyon_app's READMEs and the AGENTS files say
the same; the snap's build installs make for sodium's build hook.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017, step 3 (D4: nobody signs in with email codes yet, so the
password replaces them outright). `emailCode.verifyCode`,
`emailSignIn.signIn` and the switch token go, with the register's
`verifySignInCode` and `completeEmailSwitch` and their operations
between services. `emailCode.sendCode` stays, now without a session
(signing up and getting back in have none), as what proves an address
for a password's setup, a sign-up, a setup never finished and the
resets; `accountRecovery.sendCode`, which did that for the resets,
goes. The `switchChallengeId` column stays: Serverpod's challenges
still link a completion token, and older servers read it during a
deploy.
The apps: halkyon_app's email sign-in (its bloc, sheet, gateway calls
and the start screen's Continue with email) goes; Learn's profile and
Todo's account sheet show `AccountSecuritySection` (a password set up
by someone anonymous or signing in with GitHub, whose end it names;
the authenticators; changing the password; new recovery codes and
phrase), which reads anew after every change the repository makes.
BREAKING CHANGE: Learn's `minimumAppBuild` is 22: builds up to 21 sign
in with the endpoints that went. Todo was never released and stays at
build 1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017, step 3: halkyon_app's side, which Learn and Todo take.
- The account's cryptography on the device (`SodiumAccountCrypto`):
halkyon_keys and SRP's client half, each step on an isolate of its
own. sodium hands out results as views of native memory its
finalizers free once the isolate that made them ends, so everything
crossing back is copied first: a view read freed memory and crashed
the process.
- `PasswordGateway` on the module's endpoints (steps without a session
through a client holding none; an authenticator set up with a session
granted but not yet the device's), and `PasswordRepository` running
the flows: signing in (M2 checked, the keys opened before anything is
trusted), signing up, a password for someone signed in, an account
never finished, the authenticators, recovery codes, changing the
password, a new recovery phrase, and getting back in.
- The unlocked keys in the system's keyring per instance and person
(`SecureAccountKeyStore`), gone once another person signs in or
nobody, and with the instance's removal.
- The screens: the start screen's Sign in and Create an account (and
when GitHub sign-in ends), the sign-in sheet (codes, recovery codes,
an account never finished, a lost authenticator), the account's setup
(the recovery phrase shown once, three random words typed back before
anything reaches the instance; the authenticator's QR code, drawn
with pub.dev's qr; the recovery codes), getting back in, and the
account section (`AccountSecuritySection`).
- `FileSaver` moves from Learn into halkyon_app, for the phrase and the
codes in Todo too; `AppDependencies` names it and `passwords`.
- halkyon_keys rebuilds account keys from the secret keys a device kept;
the key proof's message is in halkyon_base_client, checked against
the register's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017, step 3. `password.beginChange` and `password.change` take the
old password's SRP proof and a TOTP or recovery code, and the same keys
wrapped under the new password; a session alone changes nothing, and an
account without a confirmed authenticator is told to set one up first.
`password.setRecoveryKeys` replaces the keys' wrapping under the
recovery phrase for a TOTP code, so a device holding the keys can make
a new phrase.
A change of password ends every other session of the account in every
service of the instance; resets of the password and of the account end
them all. The register ends its own and keeps a notice per member
(`sessions_ended_notice`, a migration that only creates it), told at
once and hourly until it confirms, as deletions are told: it names a
moment, so sessions made since stay, and the session that asked for a
change. A member answering one ends its sessions made before that
moment; a member signing someone in after a reset ends its own first
and makes the new session only once its clock is past the moment.
A reset not due says when it takes effect (`takesEffectAt` on
`PasswordRefusedException`), across members too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The hackathon's judging is over, so learn.halkyon.eu follows main once
more. Pushed together with build 21's release, as released apps up to
build 20 can't sign in against the new server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Every per-network limit counted by the connection's address, which behind
Caddy is Caddy's for everyone: new anonymous accounts, codes by mail and
password sign-ins were each limited for the whole instance.
`networkOf` (now `common/network.dart`) takes the last address of
X-Forwarded-For, the one the proxy saw itself, for a connection from a
proxy the operator trusts (`TRUSTED_PROXIES`, which deploy/compose.yaml
sets to Caddy's fixed address); otherwise, or where that is no address,
the connection's. Never the Forwarded header, which Caddy passes on as
sent, so never Serverpod's `remoteInfo`. A value that isn't addresses
stops the server as it starts.
Serverpod's own limit on anonymous accounts counts by the connection's
address too and can't be told otherwise, so it is off; the anonymous
endpoint counts them itself, 30 an hour per network as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017 records the decisions taken in building (SRP's identity, the
decoys, the 72-hour account reset proposed for confirmation, GitHub's
deprecation) and what step 3 must do; the module's README and the
AGENTS files say how password sign-in works.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Their migrations add halkyon_base's five tables for passwords,
authenticators, recovery codes, resets and sign-ins begun (nothing
else), and their pruning forgets sign-ins never finished and
authenticators never confirmed, hourly, and every GitHub ID once
GitHub sign-in has ended, daily.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
ADR 0017's step 1. Generic over group and hash (SrpGroup); production
takes RFC 5054's 3072-bit group with SHA-256. k = H(N | PAD(g)),
u = H(PAD(A) | PAD(B)), K = H(PAD(S)),
M1 = H(H(N) xor H(g) | H(I) | s | PAD(A) | PAD(B) | K),
M2 = H(PAD(A) | M1 | K). A and B must lie in 1..N-1 and u must not be
0; a and b are 256 bits from Random.secure(); proofs are compared in
constant time. x comes from the caller (halkyon_keys makes it); RFC
5054's own x is there for the vectors only.
- The register's half in halkyon_base_server (base.dart: srpChallenge,
srpVerify), the device's written by hand in halkyon_base_client
(srp.dart: SrpClient, srpVerifier). Serverpod's generator cleans and
writes only a client's lib/src/protocol/ (serverpod_cli 4.0.4's
_getDirectoriesRequiringCleaning; a generate leaves srp/ untouched).
- Tests: RFC 5054 Appendix B on both halves; the client against the
register, succeeding and every refusal.
- The module's tests depend on its client, and pub resolves every
member's dev dependencies, so Learn's server image takes the module's
client into its workspace too.
- The format check covers the hand-written half.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
What Learn and Todo each held now comes from halkyon_app:
- AccountTiles: whose account on which instance, switching, signing
out (asking first for an anonymous account, as it deletes it) and
deleting it (asking first), the app's own words passed in.
- showAppAbout: the app's name, version, icon and legalese as it gives
them, the source of what the instance runs (AppDependencies gains
sourceCodeUrl), the app's own parts, the licence list.
- resolveAppLocale: the first preferred language the app speaks, never
by country alone, British English otherwise.
- AssetCountryNames, the default of InstanceGate and InstanceScope:
Unicode CLDR 48.2's territory names in English and German
(tool/country_names.py, 7.6 KB), Hong Kong, Macao, Myanmar and
Palestine by their short names as Learn's world map has them, so
Todo says "Germany (EU)" where it said "DE (EU)".
Learn's texts read as before in every language; its About bloc and
gateway, its country names from the world map and the keys only the
moved code used are gone, as are Todo's copies.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
halkyon_base runs as the instance's register or as one of its members.
The register keeps the ways in, handles and profiles; members ask it
over HTTP with the instance's shared secret (instanceSecret), and open
sessions of their own for its accounts, under the same account ID.
- AccountsMode from the environment: INSTANCE_MEMBERS on the register,
ACCOUNT_REGISTER_URL on a member. Learn always holds the register
(Explore joins the profiles); Todo is a member where it is set.
- AccountRegister: LocalRegister, RemoteRegister, an in-memory one in
tests. Calls between services on the web server, refused without the
secret (constant-time compare); a server needing it doesn't start
without it.
- Deleting an account deletes it everywhere: notices per member kept in
the deletion's transaction (account_deletion_notice), told at once and
hourly until confirmed; members prune daily whom the register no longer
knows, never while it knows none of them or can't be reached.
- Deploy: INSTANCE_SECRET and INSTANCE_MEMBERS in compose and .env.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
People's public profiles are the instance's register's (ADR 0016), so
the module now holds them: the public_profile table (unchanged, so
Learn's migration records versions only; devices get the table empty,
as they hold every module's tables), the profile and profiles
endpoints under `base`, the handle rules, the link checks and the page
at /@handle, whose foot names the app as the host's configuration says
(profilePageNote). Deleting a person deletes their profile in the
module's core.
Hosts read profiles through the module (profileOf, profilesOf,
personWithHandle, fixHandle; saveProfile for their tests), behind an
AccountRegister that is this server's own database for now. Learn's
feedback, discussions and publishing ask it; publishing fixes the
handle just before it writes the release, outside its transaction.
Explore still joins the table in its search, so Learn holds the
register wherever it runs. Learn's app calls the module's endpoints;
a profile not found is ProfileNotFoundException. Todo's database gets
the table too (it was never deployed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Todo's packages are licensed as Learn's (decided 2026-10-19): the server
under the SSPL, the client and the app under the AGPL. Its AGENTS.md
and READMEs describe the shell it is now; the root's README, lint note
and licence table follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Todo's app is halkyon_app's instance and session gates with Todo's
frame ("Todo" under "Halkyon", its icon, its own sentences) and an
instance check accepting only `todo`; TodoDependencies on an instance,
an empty TodoPerson per person, nothing kept or synced yet. A person
sees an empty home and their account: whose it is on which instance,
switching account, signing out and deleting it (each asking first where
it deletes), and About with the source of what the instance runs and
the licence list. Texts in British English and German. App ID
app.halkyon.todo on every platform, GitHub's callback scheme registered
on Android, no web build.
Its icon is Lucide's list-checks (lucide-static 1.50.0) in the brand's
blues on white: tool/app_icons.py draws its strokes with Pillow and
writes every platform's launcher icon; Lucide's ISC notice joins the
licence list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Todo's server includes halkyon_base (ADR 0014), configured for Todo:
its basics say it serves `todo`, sign-in mails name Halkyon Todo, the
source is this repository, minimumAppBuild 1. Authentication through
the module (anonymous, GitHub, email codes, server-side sessions) in
place of the scaffold's email-and-password provider; the module's
pruning scheduled once however often the server starts. The scaffold's
greeting, its Flutter web hosting, cloud storage and Docker files go;
session logs are never persisted. Todo was never deployed, so its
migrations start again with one initial migration. The root's lints
apply, with the four constructor rules off as on Learn's server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The start screen's sentence after a GitHub account nobody here signs in
with named Learn's Profile screen. The package now says only that no
account signs in with it; the app's frame adds the rest as a sentence
of its own (thenAddGitHub), since the place takes its language's
grammar ("im Profil"). Learn's reads as before, which a test now pins.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
tool/release.dart releases one app: `dart run tool/release.dart learn
[major | minor | patch] [--dry-run]`, each app configured in Dart beside
it (apps/learn/release.dart). Every step and check of tool/release.sh
is kept: the repository as pushed, resuming after a release commit and
tag, Flutter as CI pins it, the tools, the key and its certificate pin,
secrets asked for without echo and never written down, CI green on the
newest commit, the signed release commit and tag by a key deploys
accept, the APK built from a checkout of the tag and checked, the snap,
the signed checksums, pushing only after both builds passed, the
release on Forgejo, the F-Droid repository, the Snap Store once a
channel is set.
New: tags per app (learn-v0.19.0, Learn's v… counting as its
history); only commits touching the app decide its version (its folder,
the shared packages it uses by path, the root pubspec and lock, its
release files, where its packages were before the move); --dry-run
reads all of it, lists every step and the notes, and what a release
would refuse, changing nothing. The pure parts are tested in
test/release/. Learn releases build 21 with tool/release.sh, then
switches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The root AGENTS.md holds what applies to every app and package (how
work is done, Serverpod, checks with Melos and the tools, CI); Learn's
own is apps/learn/AGENTS.md, Todo's a note that it is still the
scaffold. The root README introduces the project, Learn's moves to its
folder; LICENSE.md says which licence covers which directory, Todo's
packages unlicensed until decided; one CONTRIBUTING.md. ADR 0015
records the decision; ADR 0014 and 0003 note what moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
- Melos 8.9 as the root's dev dependency, with scripts across the
workspace (analyze, test:dart, test:flutter, generate, check) and its
filters (--diff, --include-dependents); cli_util overridden to 0.5.2
for it, since Learn's flutter_launcher_icons asks for 0.4 and uses only
its logger, unchanged in 0.5.
- tool/check.dart in place of tool/check.sh: packages, formatting of our
own files, analysis, the tools' tests, then every package's tests
through Melos (--changed-since=<commit> for only what changed and what
depends on it). tool/pre-push is the hook: the signature check, then
the checks. CI generates every server's code and runs the same check.
- The images build from the root with a workspace narrowed to what they
need (tool/workspace_only.sh); compose, the deploy's migration test,
the snap, the release script, the editor's launch files and Learn's
asset tools (run from apps/learn) follow the new layout.
- The project's source is named as project-halkyon/halkyon (source link,
snap, F-Droid listing, release uploads); the AGPL packages carry the
licence's own text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
The root pubspec lists Learn's, Todo's and the shared packages, which
the apps now take by path instead of pinned commits of halkyon_shared:
one resolution, one lockfile. Learn's versions stay as they were; Todo
moves to Serverpod 4.0.4 and Dart 3.13 with the rest (its generated code
unchanged), drops google_fonts, which it never used, and a template
line Dart 3.13 refuses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Its server, client and app as `serverpod create` made them, with the
switch to server-side sessions that was waiting uncommitted, as files:
its one commit holds the scaffold alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
Its server, client, app and curator, and the tools that write its
assets (maps, Anki fixtures, icons), as they are; paths are fixed in
the commits that follow.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>