zit 0.3.23 answers an HTTP Basic challenge. A forge that gates a read answers a clone's first request with 401; the credential can now reach the wire on that first request, which is the one a private repository refuses.
No artifact, tag namespace, sync message, or on-disk repository format changed. One HTTP request header is new, sent only when a peer asks for it.
The round that could not carry the card
0.3.22 taught clone and pull to send a login card, and a private
repository on a read-gating forge was still unclonable. The two halves
were each correct alone. A clone cannot sign its first request, because the
card's shared secret is derived from the project code and the project code
is what the first reply teaches; fossil's client sends no card on that
round either. And the forge refuses the first request with an HTTP 401,
which the transport treated as fatal, so the round that would have carried
the card never came. The server's log showed one request per clone and no
credential offered, whether or not one was given.
Fossil's own server never gates a read at the HTTP layer, so the retry
0.3.22 added, which fires on a protocol-level refusal that teaches the
project code, was the right answer to fossil and no answer to a forge.
Fossil's client reaches a forge with --httpauth, an HTTP Basic header on
every request, and that is the mechanism a first request can use.
The transport answers the challenge
When a verb holds a plaintext password, from --password, ZIT_PASSWORD,
or the prompt, the transport keeps a Basic credential ready and sends
nothing until asked. A 401 whose WWW-Authenticate names the Basic scheme
is answered by sending the same round again with the Authorization
header, and every later round carries it, as fossil's does. After the first
reply teaches the project code the login card rides the later rounds as
before, so a peer that admits either is served either way.
A 401 is no longer a bare status. With a challenge and no password to answer it, such as a pull on a stored login, the message says the peer wants a login and names the flags. With a credential sent and still refused, it says the peer refused the login. A 401 without a challenge says so.
A read-only account on a forge whose card secrets exist only for writers can now read with Basic, which is the case the card never covered.
Verification
- Transport unit tests: a Basic challenge is recognized case-insensitively
and a Bearer or absent challenge is not; the header value is the one
fossil's
--httpauthsends. - An end-to-end test stands an authenticating gate in front of
zit serve: every request withoutAuthorizationis answered 401 with a Basic challenge, every request with it is relayed. An anonymous clone is refused with the message naming the flags; a clone with--userand--passwordlands, and the gate's log shows its first request bare and every request after it carrying the header.
Compatibility
- No on-disk repository or wire format changed; Fossil RFC revision 00 remains the target.
- The header is sent only after a peer challenges for it, and only when a plaintext password was given to the run. A stored login never produces one. A peer that never challenges sees exactly the requests it saw before.
Qualification
zig fmt --check, zig build test, e2e, smoke, muscle,
git-import-e2e, git-export-e2e, dst, and cross, plus the
three-runner CI battery.