feat: dev and staging bypass

This commit is contained in:
Nathnael
2026-08-15 08:07:06 +00:00
parent 6c4969f3fd
commit 69805315d9
8 changed files with 142 additions and 30 deletions

View File

@@ -8,6 +8,7 @@ import { OtpRepository } from "./otp.repository";
import { NotificationsService } from "../notifications/notifications.service";
import { EmailClientService } from "../notifications/email-client.service";
import { isBypassEnv, DEV_BYPASS_OTP } from "../../common/dev-bypass.util";
/**
* Where a code goes. At least one of phone/email must be set — enforced by the
@@ -146,6 +147,21 @@ export class OtpService {
`otp.issue channels=${channels.join("+")} target=${label} action=${rotated ? "rotate" : "create"}`,
);
// Dev/staging only: the row above still exists (so a real code would
// still verify), but skip the real SMS/email send — no carrier cost, no
// dependency on RabbitMQ/the mail relay being up. Verify with the fixed
// DEV_BYPASS_OTP code instead of whatever landed in the row.
if (isBypassEnv()) {
this.logger.warn(
`otp.dispatch.bypassed target=${label} — dev/staging, no real SMS/email sent (verify with ${DEV_BYPASS_OTP})`,
);
return {
success: true,
delivered: true,
message: "OTP sent successfully",
};
}
// NOTE: do NOT reset the brute-force attempt counter on send. Clearing it
// here let an attacker wipe the per-target guess budget just by calling
// /otp/send between guesses. The counter is cleared only when the code is
@@ -394,7 +410,10 @@ export class OtpService {
// invalid otp — per-target attempt cap so a 6-digit code can't be
// brute-forced within its TTL; the code is burned once the budget is spent.
if (otpData.otp !== otp) {
// Dev/staging only: a fixed code verifies any pending OTP row without
// knowing the real one — the row still has to exist (sendOtp still runs).
const bypassed = isBypassEnv() && otp === DEV_BYPASS_OTP;
if (otpData.otp !== otp && !bypassed) {
const attempts = (this.actionAttempts.get(key) ?? 0) + 1;
if (attempts >= this.MAX_ACTION_ATTEMPTS) {
await this.otpRepository.deleteOtp(otpData);
@@ -482,7 +501,10 @@ export class OtpService {
);
}
if (otpData.otp !== otp) {
// Dev/staging only: a fixed code verifies any pending OTP row without
// knowing the real one — the row still has to exist (sendOtp still runs).
const bypassed = isBypassEnv() && otp === DEV_BYPASS_OTP;
if (otpData.otp !== otp && !bypassed) {
const attempts = (this.actionAttempts.get(key) ?? 0) + 1;
if (attempts >= this.MAX_ACTION_ATTEMPTS) {