v0.7.4
Social users could not set a password on NestJS
POST /auth/social/set-password answered 500 AuthService is not available on every NestJS deployment, for as long as the route has existed. Express and Fastify were fine.
The NestJS module built SocialAuthService with a null AuthService to break a circular dependency, and setting a password delegates to exactly that service. The cycle turned out to be phantom — AuthService declares a socialAuthService parameter it never reads — so the dependency now runs one way and the real instance is injected. No configuration change on your side.
If your app offers "add a password to your Google account", it starts working on upgrade.
Adaptive MFA: a risk factor that fed itself
suspicious_activity could never clear once triggered.
The assessment records its verdict as an audit event with status SUSPICIOUS. The next assessment then read that event back as evidence of suspicious activity — so a single genuinely risky sign-in pinned the account indefinitely, each new login re-deriving the factor from its own previous output and refreshing the one-hour window.
In practice: a user who reset their password on an unrecognised device tripped the threshold once, and then saw require_mfa on every subsequent sign-in with suspicious_activity as the only remaining factor, forever.
The assessment's own event type is now excluded from the evidence lookup.
The fix stops new loops but does not rewrite history. Accounts already carrying SUSPICIOUS assessment rows keep matching until those rows age out of the window — an hour by default, counted from the last sign-in attempt, so the user has to stop trying for the window to pass.
Adaptive MFA: new_ip never fired
The LOGIN_ATTEMPT audit row for the current request is written before risk detection runs, and the IP lookup searched every event type — so it always found the row it had just written and concluded the address was familiar. The factor has been inert in every deployment since it shipped.
If you rely on new_ip, expect it to start contributing to scores after upgrading. Review your thresholds: sign-ins that previously scored 0 may now reach require_mfa.
Adaptive MFA: partial riskWeights disabled everything else
riskWeights: { new_device: 10 } // used to silently zero every other factor
The config replaced the default weights rather than merging over them, so any factor the config did not name resolved to undefined and contributed nothing. Someone lowering new_device turned off impossible_travel detection without any indication. Weights now merge over the defaults, matching how riskLevels has always behaved.
Check any config that sets riskWeights — factors you did not list were scoring zero and will now score again.
Adaptive MFA now says why
suspicious_activity records the reason it fired — a prior suspicious audit event, named by id, or a count of failed logins — and logs it at warn. Previously it was a bare boolean, leaving no way to tell a real security event from three mistyped passwords.
Related: a sign-in with no resolved geolocation now warns that new_country and impossible_travel could not be evaluated. Every location factor is gated on a resolved country, so a failing MaxMind database quietly reduced adaptive MFA to device-only signals with nothing indicating it.
RiskScoringService.getRiskLevel() also honours mfa.adaptive.riskLevels instead of hardcoded bands, so it no longer reports a level that disagrees with the action taken.