halkyon/packages/halkyon_app
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Tizian 「ティツィアーン」 5270ce6775
fix(app): only an otpauth URI is shown as a QR code or opened
The instance sends the URI an authenticator is set up from; one that
isn't otpauth: (a web page, an Android intent) was drawn as a QR code and
handed to the system to open. Now it is neither; the key stays to type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
2026-10-23 14:12:57 +02:00
..
assets refactor(halkyon_app): the account section, About, the language and countries' names, shared 2026-10-22 18:11:08 +02:00
lib fix(app): only an otpauth URI is shown as a QR code or opened 2026-10-23 14:12:57 +02:00
test fix(app): only an otpauth URI is shown as a QR code or opened 2026-10-23 14:12:57 +02:00
tool refactor(halkyon_app): the account section, About, the language and countries' names, shared 2026-10-22 18:11:08 +02:00
.gitignore halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
analysis_options.yaml halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
CHANGELOG.md halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
l10n.yaml halkyon_app: instances, sign-in and sync plumbing for Halkyon's apps 2026-10-19 15:31:40 +02:00
LICENSE build: Melos, a Dart check and every path for the single repository 2026-10-22 18:11:04 +02:00
pubspec.yaml fix(app): saved files are the own user's alone, and iOS's copy goes 2026-10-23 14:12:56 +02:00
README.md docs(auth): proofs bound to the base URL and to what they approve (ADR 0017) 2026-10-23 14:12:55 +02:00

halkyon_app

What every Halkyon app (Learn, Todo, Train) is built around, on an instance's halkyon_base module: the instances a device keeps an account on, signing in, each person's data opened and closed, and syncing it. Taken from Halkyon Learn, which it was written for, so it behaves as Learn's own did; the texts are in British English, American English and German, and where they name the app, they name yours.

Written as the apps are: Dart 3.13's primary constructors, Material from package:material_ui, Lucide icons, blocs (flutter_bloc), LeanCode's lints.

What it holds

  • Instances (lib/src/instance/): Instance (an address as people type it), InstanceChoice (those the device keeps an account on, one in use; FileInstanceStore), InstanceStorage (each instance's people's folders and session key), InstanceChecker (does an address answer, where does it keep people's data; ServerpodInstanceChecker asks its instance.basics()), InstanceScope. The screens: FirstStartScreen (the build's instance and another, as tiles, InstanceTile), InstanceStep (an address, checked), showAccountSwitcher and AddAccountScreen, and where an instance keeps people's data, as a flag and words (InstanceLocationBuilder, InstanceFlag, instanceLocationWords): EU, a country or a subdivision (ISO 3166-2, US-CA) as its operator says, "Germany (EU)" for a member of the European Union (europeanUnionMembers) and "California, United States" for a subdivision, with the most specific flag there is (a few subdivisions have their own, such as Scotland; else the country's). Countries and subdivisions are named by Unicode CLDR's names in English and German, which come with the package (AssetCountryNames, AssetSubdivisionNames; the package's tool/country_names.py and tool/subdivision_names.py write them, from one release); tests give names of their own (CountryNames, SubdivisionNames).
  • Gates (lib/src/app/): InstanceGate opens the app's dependencies on the instance in use (AppDependencies, handed down by AppScope), anew on a switch, the previous ones closed first; SessionGate keeps a person signed in, signing in anonymously at a first start and asking how to start once signed out (StartScreen); PersonScope opens each person's own dependencies and closes them when the person goes. resolveAppLocale picks the app's language as every Halkyon app does: the first preferred language it speaks, never by country alone, British English otherwise.
  • About (showAppAbout): the app's name (its frame's), its version, icon and legalese where it gives them, where the source of what the instance runs is (InstanceSourceCode, from AppDependencies.sourceCodeUrl; both licences require offering it), the app's own parts (its credits), and the licence list.
  • The account (AccountTiles): whose account on which instance, switching to the account on another, signing out (which of an anonymous account deletes it, so asks first) and deleting it (after asking), for an app's settings, in the app's words where only it can say them.
  • Signing in (lib/src/auth/): AuthRepository on an AuthGateway; ServerpodAuthGateway signs in anonymously and with GitHub (the system browser and a loopback address on Linux and Windows, LoopbackWebAuth), through the instance's halkyon_base, and puts a session granted elsewhere in place (useSession); SessionBloc, GitHubSignInBloc and SignInWaysCubit; SignedOutMark, SignInAddresses, PersonFolders.
  • A password and TOTP (lib/src/password/, ADR 0017): PasswordRepository on a PasswordGateway (ServerpodPasswordGateway), an AccountCrypto (SodiumAccountCrypto: halkyon_keys and SRP's client half, each step on an isolate of its own, whose results are copied before they cross back: sodium's are views of memory freed with the isolate that made them) and an AccountKeyStore (SecureAccountKeyStore: the account's unlocked keys in the system's keyring, per instance and person, gone once another person signs in or nobody). Signing in (SRP's M2 checked and the keys opened before anything is trusted), signing up (the recovery phrase shown once and three of its words typed back before anything reaches the instance), a setup never finished, the authenticators (QR codes drawn with pub.dev's qr), recovery codes, changing the password, a new phrase, and getting back in. The screens: the start screen's Sign in and Create an account, showPasswordSignInSheet, showAccountSetupSheet, showAccountRecoverySheet, and AccountSecuritySection for an app's account part. Every change of how the account is secured asks for the password with the code, one answer per sheet (OwnerAnswer, the owner's proof: a session alone gets no code checked), each answer bound to the instance's base URL and to the change it approves (its purpose and the digest of what it sets, proof_digests.dart), so nobody between the device and the instance swaps anything in; deleting by a code by mail follows secondFactorWorks, as the instance tells it, and asks for the address only where the device doesn't know it. Anonymous people get no keys until something is encrypted (step 4). Every build of an app needs a C compiler and make: sodium's build hook compiles libsodium.
  • Files (FileSaver, PlatformFileSaver): keeping a file the app made where the person chooses (a recovery phrase, recovery codes, an app's exports).
  • Sync (lib/src/sync/): SyncService (once at start, shortly after local changes, on resume and every few minutes, one sync at a time, counting the changes not on the server yet, PendingChangesStore), AppCompatibility and compatibleSync (the instance's minimum app build, the engine's table check), SyncStatusBloc, syncAndWait, and the widgets: SyncStatusCard, AppMismatchBanner, SyncTroubleBadge.
  • A client of halkyon_base alone (BaseClient): to ask an instance about itself, and to sign in as another person than the app's client holds.
  • Testing (package:halkyon_app/testing.dart): fakes of every boundary (FakeAuthGateway, FakePasswordGateway, FakeAccountCrypto, FakeInstanceChecker, the stores in memory), a frame (testFrame), testApp, and TestAppDependencies.

Using it in an app

  1. The server includes halkyon_base with the nickname base (its README), so the app's generated client has client.modules.base.

  2. Depend on it by path, within the repository's workspace, as on halkyon_ui:

    dependencies:
      halkyon_app:
        path: ../../../packages/halkyon_app
    
  3. Texts: add HalkyonAppLocalizations.delegate to every MaterialApp's delegates, beside HalkyonUiLocalizations.delegate and material_ui's GlobalMaterialLocalizations.delegates.

  4. The frame (AppFrame): the app's name and wordmark word, its icon, an example address, its theme, delegates, languages (resolveAppLocale as its resolution) and launch screen, and the three sentences only it can say: what the first start's instance keeps of the person's, what a switch to another account leaves behind, and where GitHub is added later.

  5. The dependencies: the app's composition root implements AppDependencies<P> (serverName, auth, signedOut, passwords, fileSaver, sourceCodeUrl, openPerson, dispose), its person's dependencies Disposable. Its passwords is a PasswordRepository on ServerpodPasswordGateway(client, base: client.modules.base), SodiumAccountCrypto(), SecureAccountKeyStore(storage.keysOf(instance)), the auth gateway and the app's SignInAddresses. Its auth is an AuthRepository on ServerpodAuthGateway(client, base: client.modules.base, callbackScheme: …, appName: …); the scheme is the app's own (app.halkyon.learn), registered with each platform and in the instance's GitHub OAuth app, whose loopback address on Linux and Windows is http://127.0.0.1/github-callback (port 43823 unless given).

  6. The tree:

    InstanceGate(
      frame: frame,
      choice: choice,
      checker: ServerpodInstanceChecker(app: 'todo'),
      readLook: …,           // the device's theme mode and language
      open: Dependencies.create,
      app: SessionGate(
        look: (context) => …,
        app: (person) => PersonApp(person: person), // with PersonScope inside
      ),
    )
    

    A person's widgets find their dependencies with PersonScope.of<PersonDependencies>(context) and the app's with AppScope.of<Dependencies>(context).

  7. Licences: call registerHalkyonAppLicenses() once at startup (the flags' and Unicode CLDR's notices), beside halkyon_ui's registerFontLicenses().

Licence

GNU Affero General Public License 3.0 or later (LICENSE). The flags are lipis' flag-icons, drawn by the flag package, under the MIT licence (assets/flags/LICENSE-flag-icons.txt). The subdivisions' names are Unicode CLDR's (release 48.2), under the Unicode License v3 (assets/subdivisions/LICENSE-CLDR.txt, assets/subdivisions/ATTRIBUTION.md).