v0.7.1
Someone can change your phone number and switch off your MFA
Changing a phone number is more destructive than it looks. nauth-toolkit clears the verified flag, deletes every SMS MFA device bound to the old number, and — if SMS was the last factor — switches MFA off entirely.
Until this release, none of that reached the account owner. It was recorded in the audit table and nowhere else.
Phone-change alerts
The new phoneChanged notification tells the owner what happened, including how many SMS devices went with the old number and whether MFA is now off.
emailNotifications: {
enabled: true,
suppress: {
phoneChanged: false, // opt in, like every optional notification
},
}
The alert is addressed to the account email, never to either phone number. A phone change is routine — people change carriers and jobs — so the mailbox is the stable channel. Notifying the old number would also fail exactly when it matters, since an attacker who changed the number no longer controls it.
For your own delivery, implement IPhoneChangedHook, or on NestJS use the @PhoneChangedHook() decorator:
@Injectable()
@PhoneChangedHook()
export class PhoneChangedAlert implements IPhoneChangedHook {
async execute(metadata: PhoneChangedMetadata): Promise<void> {
await this.alerts.send(metadata.user.email, {
newPhone: metadata.newPhone,
removedDevices: metadata.deactivatedMFADevices,
mfaNowOff: metadata.mfaDisabled,
});
}
}
See Notifications and IPhoneChangedHook.
MFA device removal now notifies
Related, and worth enabling even if you skip the above: removing an MFA device as a side effect of a contact change now fires mfaDeviceRemoved. Previously that hook only fired when a user removed a device themselves, so a phone or email change stripped a second factor silently.
suppress: {
mfaDeviceRemoved: false,
}
Password reset on a disabled account
Requesting a reset for an account an admin had disabled sent a code, and confirming it reported success — while login stayed blocked. Nothing told the user why they still could not get in.
The request is now skipped, without revealing that the account exists, and confirmation answers ACCOUNT_LOCKED. That check runs only after the reset code is verified, so the lock is never disclosed to someone who does not hold the code.
Documentation
A new Environment Cheat Sheet collects the cookie, CORS, CSRF, passkey and social-callback values that actually work for four deployment shapes — everything on localhost, a dev-server proxy or same-site alias against a remote API, same-origin behind a reverse proxy, and frontend and API on sibling subdomains. It ends with a symptom-to-cause table for when cookies silently fail to stick.
Two corrections worth checking against your own code:
disableUsernever setsisActive: false. The API reference showed that in the response example, but the call only touchesisLocked.isActiveis enforced at login yet nothing in the toolkit ever writes it. If you branch onisActivein that response, it has always beentrue. See Admin Operations.- The NestJS hook decorators are exported. Seven decorator pages claimed otherwise and told you to register hooks by hand through
HookRegistryService.@EmailChangedHook(),@PasswordChangedHook()and the rest all import from@nauth-toolkit/nestjsand work as their own examples show.
Removed: code that never ran
The account-lockout notification is gone. The hook behind it was never invoked, so no lockout email was ever sent — removing it changes nothing you could observe.
If your config sets this key, delete the line:
emailNotifications: {
suppress: {
accountLockout: true, // no longer a valid key
},
}
The failed-login lockout is unaffected: it is IP-based and answers RATE_LIMIT_LOGIN.
Removed alongside it, all equally unreachable: IAccountLockedHook, AccountLockedMetadata, registerAccountLocked() / executeAccountLocked(), @AccountLockedHook(), sendLockoutEmail() and sendAccountLockedEmail(), and TemplateType.ACCOUNT_LOCKOUT.
Four audit event types go with them: ACCOUNT_ACTIVATED, ACCOUNT_DEACTIVATED, ACCOUNT_LOCKED and ACCOUNT_UNLOCKED. None was ever written. If you alert on account lock or unlock, use ACCOUNT_DISABLED and ACCOUNT_ENABLED — what the toolkit has always recorded. See Audit Logs.