docs(design): record the identity field validation contract #75

Sammanfogat
supernaut sammanfogade 1 incheckning från docs/identity-validation-contract in i main 2026-08-02 21:28:49 +00:00
Ägare

ADR 0038 settles who owns each identity field. It does not say what a valid value is, and
that question has a different answer in each of the three systems a value reaches. The union existed
only in code, with nothing pointing at it from the decision records and nothing tying it to the
upgrade cadence.

Adds design/identity-field-validation.md: the intersection rule for username, email and display
name; which of the portal, Kanidm and Forgejo contributes each constraint and why it is
load-bearing, so a future reader can tell a rule that matters from one that is incidental; the
pinned versions each rule was read from; and the upgrade obligation.

The obligation is the point of the note. Nothing detects upstream drift on its own — the tests
assert our transcription of the upstream rules, not upstream itself, so they keep passing
against a version whose rules have moved. There is no runtime check either: a value that has become
invalid downstream produces the same silent first-sign-in failure the intersection exists to
prevent. A human re-reading the source at bump time is the whole mechanism, which is why the
versions are written down.

Linked from ADR 0038, and added to ADR 0028's upgrade decisions as item 8.

The issue's premise was out of date

The issue says the union "exists only as a regex in the sign-up endpoint whose comment misdescribes
it". That was true when it was filed and was fixed before it was worked: gitborg/gitborg-web#151 and
gitborg/gitborg-web#152 shipped src/lib/identity-rules.ts and removed the regex. The note
therefore documents shipped behaviour in the present tense rather than specifying work to be done.
The gap the issue identified is still real — the contract lived only in code comments, and nothing
tied it to the upgrade cadence.

Verified, not assumed

Every figure in the note was read from the source rather than paraphrased, and re-checked
independently before this was opened:

  • Pinned versions read from bitborg-infra, not from the issue: Forgejo 16.0.1-rootless
    (ansible/group_vars/all/vars.yml), Kanidm 1.10.4 (ansible/roles/kanidm/defaults/main.yml).
    Both agree with what the code cites.
  • The 40-character username cap is Forgejo's (MaxSize(40)) and binds over Kanidm's 64 — confirmed
    against identity-rules.ts, which states both.
  • 33 Forgejo reserved usernames and 5 reserved suffixes (.atom, .gpg, .keys, .rss, .png) —
    counted from the source, not estimated.
  • Display name turned out to live in a separate module, src/lib/profile.ts, not in
    identity-rules.ts, and to be far less constrained: non-empty after trimming, no C0/C1 control
    characters, at most 100 characters. Confirmed line by line, including the reason each rule exists.
    The note says so plainly rather than inventing symmetry with the other two fields.
  • The looser rule on the resend path is real: it calls isKanidmName alone, not the sign-up
    intersection, so an account created before the rules existed can still be recovered.
  • pnpm format:check and pnpm mdlint clean; both re-ran under lefthook at commit time.

Closes #71. Part of #66.

ADR 0038 settles who **owns** each identity field. It does not say what a valid value **is**, and that question has a different answer in each of the three systems a value reaches. The union existed only in code, with nothing pointing at it from the decision records and nothing tying it to the upgrade cadence. Adds `design/identity-field-validation.md`: the intersection rule for username, email and display name; which of the portal, Kanidm and Forgejo contributes each constraint and why it is load-bearing, so a future reader can tell a rule that matters from one that is incidental; the pinned versions each rule was read from; and the upgrade obligation. The obligation is the point of the note. Nothing detects upstream drift on its own — the tests assert our **transcription** of the upstream rules, not upstream itself, so they keep passing against a version whose rules have moved. There is no runtime check either: a value that has become invalid downstream produces the same silent first-sign-in failure the intersection exists to prevent. A human re-reading the source at bump time is the whole mechanism, which is why the versions are written down. Linked from ADR 0038, and added to ADR 0028's upgrade decisions as item 8. ### The issue's premise was out of date The issue says the union "exists only as a regex in the sign-up endpoint whose comment misdescribes it". That was true when it was filed and was fixed before it was worked: gitborg/gitborg-web#151 and gitborg/gitborg-web#152 shipped `src/lib/identity-rules.ts` and removed the regex. The note therefore documents shipped behaviour in the present tense rather than specifying work to be done. The gap the issue identified is still real — the contract lived only in code comments, and nothing tied it to the upgrade cadence. ### Verified, not assumed Every figure in the note was read from the source rather than paraphrased, and re-checked independently before this was opened: - Pinned versions read from `bitborg-infra`, not from the issue: Forgejo `16.0.1-rootless` (`ansible/group_vars/all/vars.yml`), Kanidm `1.10.4` (`ansible/roles/kanidm/defaults/main.yml`). Both agree with what the code cites. - The 40-character username cap is Forgejo's (`MaxSize(40)`) and binds over Kanidm's 64 — confirmed against `identity-rules.ts`, which states both. - 33 Forgejo reserved usernames and 5 reserved suffixes (`.atom`, `.gpg`, `.keys`, `.rss`, `.png`) — counted from the source, not estimated. - Display name turned out to live in a **separate** module, `src/lib/profile.ts`, not in `identity-rules.ts`, and to be far less constrained: non-empty after trimming, no C0/C1 control characters, at most 100 characters. Confirmed line by line, including the reason each rule exists. The note says so plainly rather than inventing symmetry with the other two fields. - The looser rule on the resend path is real: it calls `isKanidmName` alone, not the sign-up intersection, so an account created before the rules existed can still be recovered. - `pnpm format:check` and `pnpm mdlint` clean; both re-ran under `lefthook` at commit time. Closes #71. Part of #66.
supernaut lade till 1 incheckning 2026-08-02 21:24:58 +00:00
docs(design): record the identity field validation contract
Alla kontroller lyckades
ci / ci (pull_request) Successful in 13s
d1377785d5
ADR 0038 settles who owns each identity field but not what a valid value is. Three
systems each enforce their own rules, and the union existed only in code.

Adds design/identity-field-validation.md: the intersection rule for username, email
and display name; which of the portal, Kanidm and Forgejo contributes each constraint
and why it is load-bearing; the pinned versions the rules were read from (Forgejo
16.0.1, Kanidm 1.10.4, both verified against gitborg-infra); and the upgrade
obligation, since nothing detects upstream drift on its own.

Linked from ADR 0038 and added to ADR 0028's upgrade decisions as item 8.

Closes #71. Part of #66.
supernaut tvångsskickade docs/identity-validation-contract från d1377785d5
Alla kontroller lyckades
ci / ci (pull_request) Successful in 13s
till 69db6d5656
Alla kontroller lyckades
ci / ci (pull_request) Successful in 12s
2026-08-02 21:27:27 +00:00
Jämför
supernaut sammanfogade incheckning 32becf4537 till main 2026-08-02 21:28:49 +00:00
supernaut tog bort grenen docs/identity-validation-contract 2026-08-02 21:28:50 +00:00
Logga in för att delta i denna konversation.
Inga granskare
Ingen milstolpe
Inget projekt
Inga tilldelade
1 deltagare
Notiser
Förfallodatum
Förfallodatumet är ogiltigt eller utanför gränserna. Använd formatet "åååå-mm-dd".

Inget förfallodatum satt.

Beroenden

Inga beroenden satta

Referens
bitborg/bitborg-docs!75
Ingen beskrivning angiven.