Step 1 of #201 (Gitea organizations and accounts beside GitHub): the engine can list the organizations and repositories of a Gitea server. Library only. Accounts, the command line and the desktop app do not use it yet, so nothing a user sees changes.
ForgeKind; ForgeAccessException / ForgeAccessKind as the failure type of every forge. GitHubAccessException derives from it and keeps its own Kind.
GiteaServerUrl: what may be the address of a server (HTTPS, no credentials in it, nothing after the path).
GiteaRepositoryLister, GiteaOrganizationLister behind the existing IRepositoryLister / IOrganizationLister, on HttpClient and source-generated JSON. No new package.
A contract test suite (ListerContractTests) that the listers of both forges pass.
ADR 0010 and threat model T16.
Why
Closes#202. The address of a Gitea server is typed by a user, and it decides where the account's token is sent. The rules, each pinned by a test:
the token goes to the configured address, in a header, and to no other host: redirects are not followed, the address of a next page is never taken from an answer, and clone addresses are built from the configured address (the server's clone_url is not read);
HTTPS only, certificates validated by the platform, and no setting that changes either;
answers bounded in size, lists in pages, requests in time; a server's own words reach a message only as the reason of a refusal.
How it was tested
GiteaListerTests (a fake server behind HttpClient): paging by what the server says, the account fallback, the owner filter, what is and is not taken from answers, every failure kind, cancellation, timeouts, answers and lists without end.
GiteaServerUrlTests, ForgeAccessExceptionTests, and the contract suite run for GitHub and Gitea.
Gates, started from PowerShell: numbers in the comment below.
Not merged until
1.1.1 is tagged (#200): main is the release candidate. This stays open, and the next steps of #201 are stacked on it.
To confirm against a live server before step 2: Gitea accepts the token as the git password with a fixed user name (the form the git client uses for GitHub).
Checklist
Commits are signed and signed off (git commit -S -s)
Zero build warnings; format and lint are clean
Tests added or updated; the 100% coverage gate still passes
Docs updated for user-visible changes; threat model updated if the attack surface changed (ADR 0010, T16; no user-visible change yet)
No secrets, credentials, or personal data
New dependencies are justified below, or none were added (none)
Substantial AI assistance, if any, is noted here: written with Claude Code.
## What
Step 1 of #201 (Gitea organizations and accounts beside GitHub): the engine can list the organizations and repositories of a Gitea server. Library only. Accounts, the command line and the desktop app do not use it yet, so nothing a user sees changes.
- `ForgeKind`; `ForgeAccessException` / `ForgeAccessKind` as the failure type of every forge. `GitHubAccessException` derives from it and keeps its own `Kind`.
- `GiteaServerUrl`: what may be the address of a server (HTTPS, no credentials in it, nothing after the path).
- `GiteaRepositoryLister`, `GiteaOrganizationLister` behind the existing `IRepositoryLister` / `IOrganizationLister`, on `HttpClient` and source-generated JSON. No new package.
- A contract test suite (`ListerContractTests`) that the listers of both forges pass.
- ADR 0010 and threat model T16.
## Why
Closes #202. The address of a Gitea server is typed by a user, and it decides where the account's token is sent. The rules, each pinned by a test:
- the token goes to the configured address, in a header, and to no other host: redirects are not followed, the address of a next page is never taken from an answer, and clone addresses are built from the configured address (the server's `clone_url` is not read);
- HTTPS only, certificates validated by the platform, and no setting that changes either;
- answers bounded in size, lists in pages, requests in time; a server's own words reach a message only as the reason of a refusal.
## How it was tested
- `GiteaListerTests` (a fake server behind `HttpClient`): paging by what the server says, the account fallback, the owner filter, what is and is not taken from answers, every failure kind, cancellation, timeouts, answers and lists without end.
- `GiteaServerUrlTests`, `ForgeAccessExceptionTests`, and the contract suite run for GitHub and Gitea.
- Gates, started from PowerShell: numbers in the comment below.
## Not merged until
1.1.1 is tagged (#200): `main` is the release candidate. This stays open, and the next steps of #201 are stacked on it.
To confirm against a live server before step 2: Gitea accepts the token as the git password with a fixed user name (the form the git client uses for GitHub).
## Checklist
- [x] Commits are signed and signed off (`git commit -S -s`)
- [x] Zero build warnings; format and lint are clean
- [x] Tests added or updated; the 100% coverage gate still passes
- [x] Docs updated for user-visible changes; threat model updated if the attack surface changed (ADR 0010, T16; no user-visible change yet)
- [x] No secrets, credentials, or personal data
- [x] New dependencies are justified below, or none were added (none)
- [x] Substantial AI assistance, if any, is noted here: written with Claude Code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The engine can list the organizations and repositories of a Gitea server (or
one that speaks its API) beside GitHub. Library only: accounts, the CLI and
the app do not use it yet (#201).
- ForgeKind, and ForgeAccessException with ForgeAccessKind: what a failed
request to any forge means to a caller. GitHubAccessException derives from
it and keeps its own Kind, so code written against it still compiles and
still catches.
- GiteaServerUrl: the address of a server is HTTPS, without credentials, and
without anything after its path. There is no setting for http.
- GiteaRepositoryLister and GiteaOrganizationLister, on HttpClient and
source-generated JSON, with no new package. The token goes to the server
the lister was made for and nowhere else: redirects are not followed, the
address of a next page is never taken from an answer, and clone addresses
are built from the configured address instead of being read from the
server. Certificates are validated by the platform; nothing switches that
off. Answers, lists and requests are bounded.
- Paging follows what the server says about a next page: a Gitea server caps
the page size and leaves out what the token may not see, so a short or
empty page does not end a list.
- A lister contract test suite that the listers of both forges pass.
- ADR 0010; threat model T16.
Closes#202
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Gates on 0d76ea6, started from PowerShell: build with warnings as errors 0, engine 1,276, CLI 212, line coverage 100% on the three gated assemblies (the Gitea client included, nothing excluded), format clean, UI suite 13 of 13.
Gates on 0d76ea6, started from PowerShell: build with warnings as errors 0, engine 1,276, CLI 212, line coverage 100% on the three gated assemblies (the Gitea client included, nothing excluded), format clean, UI suite 13 of 13.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
An independent review found no way to send a token to another host on an
address that was typed right, and these gaps.
- An address that is refused is no longer repeated in the message: one that
was refused for the password or the token in it put that into the text.
- 'https:/host' with one slash was taken for a host named 'https'. Text
without a scheme is a host with at most a port; anything else is refused.
A host with characters that have no place in a name is refused instead of
throwing.
- The client all accounts share keeps no cookies: what one account's server
set travelled with another account's token.
- A Link header that is not about paging (one a proxy added) ended a list
after its first page. Only paging relations say that there is no next page.
- A connection lost while an answer is read is 'unavailable', not an
unexpected error. A page with more items than were asked for is refused, so
a list has an upper bound. A token with a space or a line break in it is
refused before anything is sent, and is not repeated.
- A token that may not read organizations is refused at an organization's
list whatever the name is: the account path is tried then too, so it still
lists its own account.
- A redirect says that the organization or account may have been renamed;
408 is 'retry later'; other statuses no longer claim there is no API.
- Tests for each, for the escaping of names in requests and clone addresses,
for the time limit covering the reading of an answer, and that the client
of production is the one built on the handler whose settings are asserted.
Refs #202
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Signed-off-by: James Maes <james@kof22.com>
Independent review: merge after fixes. It found no way to send a token to another host on an address that was typed right. What it found, all fixed in bce029d:
Finding
Fix
The message for a refused address repeated the address, and with it a password or token that was in it
The address is not repeated
https:/host (one slash) was taken for a host named https
Text without a scheme is a host with at most a port; a host with characters that have no place in a name is refused instead of throwing
The client all accounts share kept cookies: what one account's server set travelled with another account's token
No cookies, asserted on the production handler
A Link header that is not about paging ended a list after its first page
Only paging relations say there is no next page
A connection lost while an answer is read surfaced as an unexpected error
It is "unavailable"
A page could hold any number of items
A page with more than was asked for is refused: a list has an upper bound
A token without the scope to read organizations could not list its own account (from the reviewer's reading of Gitea; to confirm on a live server)
The account path is tried after a refusal of the organization's list too
A redirect for a renamed organization read as "check the server address"
The message says to use the new name
Test gaps: escaping of names, the time limit over reading an answer, that production uses the asserted handler
Tests added
Left as it is, and written into ADR 0010: reflection that looks Kind up by name on GitHubAccessException now finds two properties.
Gates on bce029d, started from PowerShell: build with warnings as errors 0, engine 1,313, CLI 212, line coverage 100% on the three gated assemblies, format clean, UI suite 13 of 13.
Independent review: *merge after fixes*. It found no way to send a token to another host on an address that was typed right. What it found, all fixed in bce029d:
| Finding | Fix |
|---|---|
| The message for a refused address repeated the address, and with it a password or token that was in it | The address is not repeated |
| `https:/host` (one slash) was taken for a host named `https` | Text without a scheme is a host with at most a port; a host with characters that have no place in a name is refused instead of throwing |
| The client all accounts share kept cookies: what one account's server set travelled with another account's token | No cookies, asserted on the production handler |
| A `Link` header that is not about paging ended a list after its first page | Only paging relations say there is no next page |
| A connection lost while an answer is read surfaced as an unexpected error | It is "unavailable" |
| A page could hold any number of items | A page with more than was asked for is refused: a list has an upper bound |
| A token without the scope to read organizations could not list its own account (from the reviewer's reading of Gitea; to confirm on a live server) | The account path is tried after a refusal of the organization's list too |
| A redirect for a renamed organization read as "check the server address" | The message says to use the new name |
| Test gaps: escaping of names, the time limit over reading an answer, that production uses the asserted handler | Tests added |
Left as it is, and written into ADR 0010: reflection that looks `Kind` up by name on `GitHubAccessException` now finds two properties.
Gates on bce029d, started from PowerShell: build with warnings as errors 0, engine 1,313, CLI 212, line coverage 100% on the three gated assemblies, format clean, UI suite 13 of 13.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Step 1 of #201 (Gitea organizations and accounts beside GitHub): the engine can list the organizations and repositories of a Gitea server. Library only. Accounts, the command line and the desktop app do not use it yet, so nothing a user sees changes.
ForgeKind;ForgeAccessException/ForgeAccessKindas the failure type of every forge.GitHubAccessExceptionderives from it and keeps its ownKind.GiteaServerUrl: what may be the address of a server (HTTPS, no credentials in it, nothing after the path).GiteaRepositoryLister,GiteaOrganizationListerbehind the existingIRepositoryLister/IOrganizationLister, onHttpClientand source-generated JSON. No new package.ListerContractTests) that the listers of both forges pass.Why
Closes #202. The address of a Gitea server is typed by a user, and it decides where the account's token is sent. The rules, each pinned by a test:
clone_urlis not read);How it was tested
GiteaListerTests(a fake server behindHttpClient): paging by what the server says, the account fallback, the owner filter, what is and is not taken from answers, every failure kind, cancellation, timeouts, answers and lists without end.GiteaServerUrlTests,ForgeAccessExceptionTests, and the contract suite run for GitHub and Gitea.Not merged until
1.1.1 is tagged (#200):
mainis the release candidate. This stays open, and the next steps of #201 are stacked on it.To confirm against a live server before step 2: Gitea accepts the token as the git password with a fixed user name (the form the git client uses for GitHub).
Checklist
git commit -S -s)🤖 Generated with Claude Code
Gates on
0d76ea6, started from PowerShell: build with warnings as errors 0, engine 1,276, CLI 212, line coverage 100% on the three gated assemblies (the Gitea client included, nothing excluded), format clean, UI suite 13 of 13.🤖 Generated with Claude Code
Independent review: merge after fixes. It found no way to send a token to another host on an address that was typed right. What it found, all fixed in
bce029d:https:/host(one slash) was taken for a host namedhttpsLinkheader that is not about paging ended a list after its first pageLeft as it is, and written into ADR 0010: reflection that looks
Kindup by name onGitHubAccessExceptionnow finds two properties.Gates on
bce029d, started from PowerShell: build with warnings as errors 0, engine 1,313, CLI 212, line coverage 100% on the three gated assemblies, format clean, UI suite 13 of 13.🤖 Generated with Claude Code