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>
136 lines
7.3 KiB
Markdown
136 lines
7.3 KiB
Markdown
# halkyon_base
|
|
|
|
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, 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.modules.base.emailCode`):
|
|
|
|
| Endpoint | Methods | Who may call |
|
|
|---|---|---|
|
|
| `anonymousIdp` | `login` (Serverpod's anonymous sign-in, at most 30 new accounts per IP address and hour) | anyone |
|
|
| `gitHubUpgrade` | `upgrade`: links a GitHub account to the caller, who becomes `identified` | signed in |
|
|
| `gitHubSignIn` | `signIn`: a session for whoever has that GitHub account | without a session |
|
|
| `emailCode` | `sendCode`, `verifyCode`: a code by mail; a new address becomes the caller's, another person's gives a switch token | signed in |
|
|
| `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.
|
|
- 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`, 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
|
|
|
|
1. Depend on both packages by path, within the repository's workspace (an app's
|
|
packages sit in `apps/<app>/`):
|
|
|
|
```yaml
|
|
# the server's pubspec.yaml
|
|
halkyon_base_server:
|
|
path: ../../../packages/halkyon_base_server
|
|
# the client's pubspec.yaml: the same with halkyon_base_client
|
|
```
|
|
|
|
2. Name it in the server's `config/generator.yaml`:
|
|
|
|
```yaml
|
|
modules:
|
|
halkyon_base:
|
|
nickname: base
|
|
```
|
|
|
|
3. Configure it once, before the pod starts, and run its authentication and pruning:
|
|
|
|
```dart
|
|
final pod = Serverpod(args); // the host's generated Serverpod
|
|
HalkyonBase.configure(
|
|
pod,
|
|
HalkyonBase(
|
|
app: 'todo', // its apps accept only instances naming it; never change it
|
|
appName: 'Halkyon Todo', // in sign-in mails
|
|
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(
|
|
tokenManagerBuilders: tokenManagerBuilders,
|
|
identityProviderBuilders: identityProviderBuilders(pod),
|
|
);
|
|
```
|
|
|
|
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`.
|
|
|
|
4. A host's deletion (`PersonDataDeletion`) runs inside `deletePerson`'s transaction,
|
|
after the person's sessions ended and before their identities and the person go, in
|
|
the order given; it deletes the person's rows in the host's tables, and returns what
|
|
has to wait until the deletion is committed (files outside the database), or null.
|
|
Pruning deletes through the same function.
|
|
|
|
5. Create the host's migration. A host taking the tables over from its own models (as
|
|
Halkyon Learn does) finds no change once its copies are gone: Serverpod matches tables
|
|
by name, and these are identical. `create-migration --empty` then records the module's
|
|
version, with no SQL besides.
|
|
|
|
The passwords it reads: `serverSideSessionKeyHashPepper`; for GitHub `githubClientId` and
|
|
`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:
|
|
`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.
|
|
|
|
## Developing it
|
|
|
|
`serverpod --no-analytics generate` here after changing a model or an endpoint, and
|
|
`serverpod --no-analytics create-migration` after changing a table. Tests need no Docker:
|
|
`dart test` here starts Serverpod's embedded PostgreSQL. They read `config/passwords.yaml`
|
|
(git-ignored) with throwaway values under `test:` for `database`, `serviceSecret`,
|
|
`serverSideSessionKeyHashPepper` and `emailSignInPepper`.
|
|
|
|
## Licences
|
|
|
|
The server package is under the Server Side Public License 1.0, as the Halkyon servers
|
|
are; the client package under the GNU Affero General Public License 3.0 or later, as the
|
|
Halkyon apps are. So each combines with its counterpart in a host exactly as the host's
|
|
own do.
|