62 lines
3.9 KiB
Markdown
62 lines
3.9 KiB
Markdown
|
|
# ADR 0011: Profile pages on the account domain, and links verified by rel="me"
|
||
|
|
|
||
|
|
- **Status:** accepted
|
||
|
|
- **Date:** 2026-10-15
|
||
|
|
|
||
|
|
## Context
|
||
|
|
|
||
|
|
A public profile (ADR 0005, rule 2) holds up to four links: a person's site, their
|
||
|
|
other accounts. Anyone can write any link, so a profile can claim a site that isn't the
|
||
|
|
person's. Mastodon settles this with `rel="me"`: a link is verified when the page it
|
||
|
|
points to links back to the profile with `rel="me"`. The same works the other way:
|
||
|
|
Mastodon verifies a link to a profile page that links to the Mastodon account with
|
||
|
|
`rel="me"`. Both need a profile to have an address on the web, which until now it had
|
||
|
|
not; profiles showed only inside the app.
|
||
|
|
|
||
|
|
The developer means to run more services than Learn, such as train.halkyon.eu, sharing
|
||
|
|
one identity. Handles already belong to no single service: they are on the account domain
|
||
|
|
(`raisondetredev@halkyon.eu`), not on `learn.halkyon.eu`.
|
||
|
|
|
||
|
|
## Decision
|
||
|
|
|
||
|
|
1. **A profile's page is at the root of the account domain**
|
||
|
|
(`https://halkyon.eu/@raisondetredev`) where the operator set one (`ACCOUNT_DOMAIN`),
|
||
|
|
else on the instance's web server, as it is reached (`profilePageUrl`). The address
|
||
|
|
belongs to the identity, not to Learn: a shared identity service can serve it later
|
||
|
|
without any link or verification breaking, and federation (ADR 0005, stage 6) can
|
||
|
|
build on it. Learn serves it for now: Caddy forwards `/@…` on both its own domain and
|
||
|
|
the account domain to the web server, and the account domain serves nothing else.
|
||
|
|
Asked for on another host, a page sends the visitor to its own address (308).
|
||
|
|
2. **The page shows the profile**, as the app's profile sheet does: avatar, name, address,
|
||
|
|
bio, links. Server-rendered, English or German by the browser's language, no
|
||
|
|
JavaScript, no cookies, nothing fetched but the avatar (its Content-Security-Policy
|
||
|
|
says so), and `noindex`: people choose to be found in the app, not by search engines.
|
||
|
|
Every link is marked `rel="me"`, so other sites can verify it in turn.
|
||
|
|
3. **A link is verified when its page links back** with an `<a>` or `<link>` whose `rel`
|
||
|
|
holds `me` and whose address is the profile page's, compared without case, a trailing
|
||
|
|
slash, a query or a fragment (`linksBackTo`, read with pub.dev's `html`, the Dart
|
||
|
|
team's HTML5 parser, BSD-3). What is verified is kept on the profile row
|
||
|
|
(`verifiedLinks`), which no device syncs.
|
||
|
|
4. **Checked when asked**: the app asks after a save that brings a link not yet checked,
|
||
|
|
and when the person taps Check links (`profile.checkLinks`), at most 10 times an hour,
|
||
|
|
since each check fetches every page linked. A check stays until the link changes; a
|
||
|
|
new handle moves the page, and drops every check.
|
||
|
|
5. **Fetching stays safe** (`SafeLinkFetcher`): HTTPS on port 443 only; the host's
|
||
|
|
addresses are looked up and every one must be public (not loopback, private,
|
||
|
|
link-local, shared, unique local, multicast, documentation or reserved, nor such an
|
||
|
|
IPv4 address inside an IPv6 one), and the connection goes to the address checked,
|
||
|
|
the certificate verified for the host, so no second lookup can send it into the
|
||
|
|
instance's own networks; at most 3 redirects, each checked anew; at most 1 MiB of HTML
|
||
|
|
within 10 seconds; no proxy, no cookies; a User-Agent naming the instance.
|
||
|
|
|
||
|
|
## Consequences
|
||
|
|
|
||
|
|
- learn.halkyon.eu needs `ACCOUNT_DOMAIN=halkyon.eu` in `deploy/.env`, DNS for
|
||
|
|
halkyon.eu pointing at its server, and the deploy that brings this; until then its
|
||
|
|
handles are on `learn.halkyon.eu`, the fallback. The account domain can still change
|
||
|
|
now, as nobody follows a handle yet; studysets copied before keep the old domain in
|
||
|
|
their `location`, which only names where they came from.
|
||
|
|
- Profiles are public on the web, not only in the app. The privacy policy says so.
|
||
|
|
- The sites people link see the instance fetch their page, from its address.
|
||
|
|
- Sharing the account itself with another service (signing in to train.halkyon.eu as the
|
||
|
|
same person) is a separate decision, not taken here.
|