A send that the mail provider did not accept finished its background
transaction as ok, so rejected reset emails looked delivered in tracing.
It now finishes as an error with a fixed message that names no recipient.
The channel used DropWrite, under which TryWrite reports success and
discards the email, so a full queue still counted the request and never
sent the code. Wait makes TryWrite return false when the queue is full,
without blocking, so the request is released and the user can ask again.
A token carries the user's roles and account, so a demoted admin kept admin
claims until the token expired. The team member update and the admin user
edit now rotate the security stamp and evict the cached value when the role,
account or user name changes. Permission overrides are read per request and
are not in the token.
Tokens now carry a keyed hash of the account's security stamp, and every
authenticated request compares it with the stored stamp (cached for 60 s,
evicted in-process on change). A password reset or change, a deactivation
and a deletion all rotate or remove the stamp, so tokens issued before them
get 401. Tokens without the claim get 401 too.
An admin revoke overturns a human decision, so admins may revoke only
admin-approved uplifts. The work-order revoke path let an admin revoke an
auto-approved uplift they had requested themselves. Both revoke endpoints
now refuse it; dispatchers keep revoking their own auto-approved uplifts.
A work order whose uplifts were all cancelled, withdrawn, expired or revoked no
longer matches the advanced-search Has uplift filter, and the board Uplift
column no longer reports it as having an uplift or shows the dead one as its
primary status. One shared live-status list backs both queries. Explicit
cancelled/revoked sub-filter values still find those work orders.
SendCodeAsync validates the invite, then starts the code with a conditional
update that also requires the invite to still be open. When an admin revoked
the link (or it was used) between those two reads, the refusal was reported as
resend_too_soon with a Retry-After, although the link was already dead.
On a refused start the invite is now read again: a closed invite gets the same
generic invalid_invite response as any other dead link, and a cooldown or send
limit refusal is computed from the fresh row.
A standalone severity patch on an Overdue WO still stores the value,
because the board patches severity before type when correcting Overdue
to Emergency/Reactive and that write must not be dropped or rejected.
Project the severity as null for Overdue in the board row mapping, which
also feeds the PATCH response, search results and the detail view, so a
stored value never surfaces on a type that carries no severity.
- Forgot Password is limited to 3 codes an hour and 10 a day per email, and
an account gets 10 failed code checks a day across every code it is sent,
so new client addresses and new codes no longer buy more guesses. Refused
requests answer exactly like accepted ones.
- The reset email is queued to a background sender, and unregistered
addresses store a row no code can match, so both paths do the same work
and return without waiting on the mail provider. Each request also clears
expired codes.
- Code hashes are HMAC-SHA256 under a key derived with HKDF from the JWT
signing secret; rows in the previous unkeyed format stop matching.
- Email and code are read only from the JSON body.