refactor(profile): handles and profiles move into halkyon_base

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>
This commit is contained in:
Tizian 「ティツィアーン」 2026-10-19 22:20:27 +02:00
commit 4107cc3087
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64

View file

@ -2,12 +2,14 @@
What every Halkyon server has, as a Serverpod module (`halkyon_base_server`, with its
generated client `halkyon_base_client`): signing in, the account's ways in and its
deletion, the pruning of what nobody can return to, and what an instance tells its apps
before anyone signs in. It comes from Halkyon Learn's server, whose rules it keeps.
deletion, the pruning of what nobody can return to, what an instance tells its apps
before anyone signs in, and people's public profiles with their pages on the web, which
the instance's register keeps (ADR 0016). It comes from Halkyon Learn's server, whose
rules it keeps.
## What it provides
Endpoints, under the nickname `base` in a host's client (`client.base.emailCode`):
Endpoints, under the nickname `base` in a host's client (`client.modules.base.emailCode`):
| Endpoint | Methods | Who may call |
|---|---|---|
@ -18,18 +20,33 @@ Endpoints, under the nickname `base` in a host's client (`client.base.emailCode`
| `emailSignIn` | `signIn`: a session for the person a switch token names | without a session |
| `account` | `delete`, `signInWays` | signed in |
| `instance` | `basics`: the app it serves, GitHub's client ID if any, whether email sign-in is on, the source link, the oldest app build, where data and backups are | anyone |
| `profile` | `mine`, `save`, `checkHandle`, `checkLinks` (at most 10 an hour), `setAvatar`, `removeAvatar`, `remove`: the caller's own public profile | `identified` |
| `profiles` | `byHandle`: anyone's public profile, whatever the handle's case | signed in |
- GitHub keeps only the numeric GitHub ID, in Serverpod's own table; email keeps only
HMAC-SHA256 of the address, keyed with `emailSignInPepper` (`email_identity`), and its
codes as Argon2id hashes (`email_code_request`). Codes are eight Crockford characters,
valid 15 minutes, 3 tries; at most 3 an hour per address and 10 per network.
- Refusals are `GitHubSignInRefusedException` and `EmailSignInRefusedException`, each with a
reason.
- Profiles: a handle as Mastodon's (1 to 30 letters, digits or underscores, unique
whatever their case, a few reserved for the instance), fixed once the host says so
(`fixHandle`; Halkyon Learn at the first publish); a display name, a bio, up to 4
links verified as Mastodon verifies them (`rel="me"` back to the profile's page,
fetched over HTTPS from public addresses only), and an avatar, a PNG of at most 1 MB
in the public storage at `avatars/<SHA-256>.png`. The page is at
`https://<account domain>/@handle` (`ACCOUNT_DOMAIN`, else the server's public host),
served by `addProfilePageRoute`, in English or German, its foot what the host's
`profilePageNote` says.
- Refusals are `GitHubSignInRefusedException`, `EmailSignInRefusedException` and
`ProfileRefusedException`, each with a reason; `ProfileNotFoundException` where there is
no profile.
- For the host's own code, `package:halkyon_base_server/base.dart`: the configuration,
`tokenManagerBuilders` and `identityProviderBuilders`, the `identified` scope and
`grantIdentified`, `deletePerson`, the pruning (`pruneAbandonedAnonymousPeople`,
`forgetOldSignUpAttempts`, `forgetExpiredEmailCodes`), short codes, `sourceCodeUrl`,
`dataLocation`, `backupLocation`, and the models.
`dataLocation`, `backupLocation`, people's profiles as the host's code reads them
(`profileOf`, `profilesOf`, `personWithHandle`, `fixHandle`; `saveProfile` for its
tests), `accountDomain`, `profilePageUrl`, `addProfilePageRoute`, the register
(`AccountRegister`), and the models. A host never reads the profiles' table itself.
## Taking it into a host
@ -63,6 +80,11 @@ Endpoints, under the nickname `base` in a host's client (`client.base.emailCode`
upstreamSourceCodeUrl: 'https://code.halkyon.app/project-halkyon/halkyon_todo',
minimumAppBuild: 1, // raise it in the commit that breaks older apps
deletePersonData: [deleteTodosOf], // the host's own data of a person
profilePageNote: ( // what profile pages say of the app at their foot
english: 'On Halkyon Todo, a to-do list:',
german: 'Auf Halkyon Todo, einer Aufgabenliste:',
link: Uri.parse('https://halkyon.app/todo'),
),
),
);
pod.initializeAuthServices(
@ -71,6 +93,9 @@ Endpoints, under the nickname `base` in a host's client (`client.base.emailCode`
);
```
Serve the profile pages on its web server: `addProfilePageRoute(pod)`, after any
literal routes of its own.
Then schedule the pruning in a future call of the host's own: daily
`pruneAbandonedAnonymousPeople`, hourly `forgetOldSignUpAttempts` and
`forgetExpiredEmailCodes`.
@ -90,7 +115,8 @@ The passwords it reads: `serverSideSessionKeyHashPepper`; for GitHub `githubClie
`githubClientSecret`; for email `emailSignInPepper` and `emailSmtpHost`,
`emailSmtpPort`, `emailSmtpUser`, `emailSmtpPassword`, `emailFrom`. Never change either
pepper once people sign in: every session or address hash would be lost. The environment:
`SOURCE_URL`, `DATA_LOCATION`, `BACKUP_LOCATION`: where the instance keeps people's data
`ACCOUNT_DOMAIN`, the domain handles are on, which can never change once anyone follows
one; `SOURCE_URL`, `DATA_LOCATION`, `BACKUP_LOCATION`: where the instance keeps people's data
and its backups, each `EU`, a country's code (`DE`) or a subdivision's (ISO 3166-2,
`US-CA`, `DE-BY`), the backups also `none`; where the disks are, not which law governs.