Release v1.38.20 - #958
Merged
Merged
Release v1.38.20#958
Conversation
`POST /api/auth/check-user` answered, for any typed identifier and with no credential at all, whether an account exists and whether it carries a passkey or a password. Its header called that the intended contract with the native client; it is not one any more. The native client never calls it, and no web surface in this repository does either. Removed without a replacement: the route, its two test files, the OpenAPI operation and its two schemas, the surface's mention in the rate-limiter doc comment, the bucket expectation in the auth-surface rate-limit test, and the login route's comment pointing at it. No stub, no deprecation shim, no 410 arm — there is no caller to keep.
Sign-in returned the moment it found no account, or an account carrying no password hash, before it reached the verifier. An account that did have a password paid a full Argon2id verification at 19 MiB and t=2. The three refusals answered with the identical 401 body and three very different durations, so the clock told an anonymous caller whether an address is registered and whether it carries a password. `verifyPasswordOrDummy` verifies against a stand-in hash minted once per process at the same cost parameters real hashes use, and returns false whenever there is nothing to verify, so the passkey-only account takes the same path as one with a password. The dummy arm discards its own verdict and swallows a throw, so no outcome there can be read as a success or answer with a different status.
Changing the profile address asks whether another account already holds it, and the answer is 409 versus 200 — an existence check over every address on the instance, open to any signed-in caller. Neither route that reaches it had a ceiling of any kind and the API wrapper supplies no default, so the only bound on a sweep was the network. Both routes now share one per-account bucket, ten an hour, charged before the question is asked so a refused caller learns nothing. Only a request whose address actually differs from the one on file is counted, so re-saving a settings form that carries the unchanged address stays free. The refusal is the standard 429 with Retry-After and the X-RateLimit triple, carries `profile.update.emailRateLimited`, and is published on both operations.
With the discovery endpoint gone, this is the remaining anonymous path that answers whether an address is registered. The 409 already gives that away, so the work asymmetry ahead of it costs nothing today. It would become the whole answer the moment somebody unified the status code without moving the check after the hash, so the ordering now says it is load-bearing.
The dummy hash was awaited as an argument to the verifier, so a rejected mint threw before the verifier was reached and before the catch on it could apply. The promise was created once at import, and a rejected promise stays rejected, so one failed mint under memory pressure would have made every unknown identifier answer 500 while a real account answered 401 — a cleaner oracle than the timing gap this closes, and passkey-only accounts locked out of the password form until restart. The mint is now awaited inside the guarded region, is lazy rather than an import-time cost in every process that touches this module, and is dropped on failure so the next refusal mints again instead of inheriting a cached rejection. A failure leaves a log line; the answer is false either way. The tests cover a rejecting mint, not only a rejecting verify.
The path stood in the published contract, so a caller outside this repository can hold a reference to it whether or not we can name one. A bare 404 reads as a broken deploy or a misrouted proxy, which is exactly the reading that kept a client retrying a removed path for twelve days once before. Registering the retirement makes the edge answer 410 with the removing version and no replacement, and republishes the path as gone rather than erasing it from the document.
A refused address change returned before the update was built, so every other field in the same request went with it. The settings form posts the whole profile at once, so somebody who had spent the budget could not save a timezone or a height for an hour and was told it was about email addresses. The address is now dropped and the rest of the save lands, reported through the existing rejectedFields key with the code `rate_limited`; the 429 is kept for a request that asked for nothing else, where a partial success would be a lie. The comment claiming an audit row beside each question was inverted: both refusals returned before the row is written, so only the answers that told the caller nothing were being recorded. The two answers worth detecting write their own rows now. The stored address is normalised before the "has it changed" comparison. Registration writes it as typed, so an account created with capitals did not match its own address and paid for its first ordinary save.
v1.38.19 was cut before this change and still answers on the path, so naming it as the version that removed the route would be wrong in the published contract from the day it appears.
…nt here Bump the version, the service-worker fallback and the OpenAPI document, and add the CHANGELOG entry for v1.38.20.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v1.38.20
Signing in no longer tells anybody which addresses have an account here.
Security
Removed
POST /api/auth/check-useris gone. It said, to anybody and without signing in, whether an account existed and whether it had a passkey or a password. It was built to decide which field a sign-in screen shows first, which is not worth telling the world who has an account here. Neither the native client nor this application's own screens ever called it. The path answers 410 and says so in the API reference rather than disappearing, so anything still calling it gets a clear answer instead of a 404 that looks like a broken server.Full entry in
CHANGELOG.md.