package ports import ( "context" "fmt" "time" "galaxy/authsession/internal/domain/challenge" "galaxy/authsession/internal/domain/common" ) // SendEmailCodeAbuseProtector decides whether one public send-email-code // attempt may proceed immediately or must be throttled by the auth-side resend // cooldown. type SendEmailCodeAbuseProtector interface { // CheckAndReserve validates input, checks the current resend cooldown // decision for input.Email, and reserves a new cooldown window immediately // when the outcome is allowed. CheckAndReserve(ctx context.Context, input SendEmailCodeAbuseInput) (SendEmailCodeAbuseResult, error) } // SendEmailCodeAbuseInput describes one resend-throttle decision request for // a normalized public send-email-code attempt. type SendEmailCodeAbuseInput struct { // Email identifies the normalized e-mail address addressed by the public // request. Email common.Email // Now records when the send attempt is being evaluated. Now time.Time } // Validate reports whether SendEmailCodeAbuseInput contains a complete resend // cooldown decision request. func (i SendEmailCodeAbuseInput) Validate() error { if err := i.Email.Validate(); err != nil { return fmt.Errorf("send email code abuse input email: %w", err) } if i.Now.IsZero() { return fmt.Errorf("send email code abuse input now must not be zero") } return nil } // SendEmailCodeAbuseOutcome identifies the coarse resend-throttle decision for // one public send-email-code attempt. type SendEmailCodeAbuseOutcome string const ( // SendEmailCodeAbuseOutcomeAllowed reports that the attempt may proceed and // that the cooldown window has been reserved immediately. SendEmailCodeAbuseOutcomeAllowed SendEmailCodeAbuseOutcome = "allowed" // SendEmailCodeAbuseOutcomeThrottled reports that the cooldown window is // still active and that the caller must not extend it. SendEmailCodeAbuseOutcomeThrottled SendEmailCodeAbuseOutcome = "throttled" ) // IsKnown reports whether SendEmailCodeAbuseOutcome belongs to the stable // Stage-17 resend-throttle contract. func (o SendEmailCodeAbuseOutcome) IsKnown() bool { switch o { case SendEmailCodeAbuseOutcomeAllowed, SendEmailCodeAbuseOutcomeThrottled: return true default: return false } } // SendEmailCodeAbuseResult describes one resend-throttle decision returned by // SendEmailCodeAbuseProtector. type SendEmailCodeAbuseResult struct { // Outcome reports whether the current send attempt may proceed or must be // throttled. Outcome SendEmailCodeAbuseOutcome } // Validate reports whether SendEmailCodeAbuseResult satisfies the resend // cooldown contract. func (r SendEmailCodeAbuseResult) Validate() error { if !r.Outcome.IsKnown() { return fmt.Errorf("send email code abuse result outcome %q is unsupported", r.Outcome) } return nil } // SendEmailCodeThrottleStatusToChallengeStatus maps one resend-throttle // outcome to the challenge lifecycle state used by sendemailcode. func SendEmailCodeThrottleStatusToChallengeStatus(outcome SendEmailCodeAbuseOutcome) (challenge.Status, challenge.DeliveryState, error) { switch outcome { case SendEmailCodeAbuseOutcomeAllowed: return challenge.StatusPendingSend, challenge.DeliveryPending, nil case SendEmailCodeAbuseOutcomeThrottled: return challenge.StatusDeliveryThrottled, challenge.DeliveryThrottled, nil default: return "", "", fmt.Errorf("map send email code abuse outcome %q: unsupported outcome", outcome) } }