Repository navigation
Description
Follow-up to #283 and PR #359. Replaces the mechanism #359 shipped; the user-visible outcome is unchanged.
What #359 does today
GET /api/v1/auth/provisioning — anonymous, returns {provisioned: bool} — polled by the SPA on every login-page mount, so the login screen can say that no account exists yet.
Proposal
Delete that endpoint. Instead, when a login attempt fails and the account count is zero, have the login response carry a distinct error code (e.g. Auth.NoAccountsProvisioned), and let the SPA map that code to the existing copy in the error area it already renders.
Why it's better
The single biggest finding on #359 — raised independently by codex (P1) and a review agent — was that an anonymous, unauthenticated endpoint reaches the database on every request throughout the pre-provisioning window, with no global limiter behind it. #359 mitigated that with a dedicated auth-provisioning rate-limit policy. This removes the surface instead of rationing it.
Login is already rate-limited (10 per 15 min per IP, #143) and already performs a PBKDF2 hash, so one cheap EXISTS on the failure path is negligible next to what that request already costs.
It also narrows disclosure. The polled endpoint answers anyone who asks, without them attempting a sign-in. The proposed version only speaks to someone who actually tried, and only when the count is zero — a state in which no credential exists to protect, and which reveals nothing about any individual account. Once one account exists, behaviour is byte-identical to today's non-enumerating 401.
Deletes
GET /api/v1/auth/provisioningandProvisioningStatusResponse- the
auth-provisioningrate-limit policy (RateLimitingOptions.ProvisioningPolicyName, itsFixedWindow, its registration and validation) getProvisioningStatusinweb/src/api/client.tsand the login-page mount effecttests/Cluckwork.Api.IntegrationTests/ProvisioningStatusTests.cs
Keeps
FirstRunStatusServiceandFirstRunProvisioningLatch— same query, called from the login path instead of an endpoint. The latch still matters: it keeps the check off the failure path once an Owner exists.- The notice copy (
auth:noAdminYet/auth:noAdminYetHint), now rendered from the login failure rather than on mount. It stays free of any command or deployment detail (see feat(auth): tell the operator when an instance has no admin yet #359). - README, GLOSSARY and Help page wording — all already phrased in terms of what the screen tells you, not the mechanism.
Tradeoff, accepted
The operator no longer learns before attempting. They land on an ordinary form, try to sign in, fail, and are then told. One extra step, and arguably the right audience: only someone actually trying to sign in needs to know.
Tests worth having
- zero accounts + failed login → the distinct code; one account + failed login → the ordinary non-enumerating 401, byte-identical (both sides of the boundary — a one-sided test would pass against a build that always emits the code)
- a successful login never emits it
- the check does not run once the latch is set
- the SPA renders the notice copy for that code and the ordinary invalid-credentials copy otherwise
Activity
mforce commentedon Aug 2, 2026
Closing — filed in error. This is not separate work: it's the same #283 follow-up as PR #359, just a better mechanism for it. Tracking stays on #283, and the implementation moves to a replacement PR; #359 will be closed as superseded rather than merged.
mforce commentedon Aug 2, 2026
Reopening — this is the tracking issue for #363, which implements it. Disregard my earlier close comment; the work is real and is in review now, it just belongs to one issue rather than a second one alongside #283.