// Package acceptauthdelivery implements synchronous durable acceptance of auth // login-code deliveries. package acceptauthdelivery import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "fmt" "log/slog" "strings" "time" "galaxy/mail/internal/domain/attempt" "galaxy/mail/internal/domain/common" deliverydomain "galaxy/mail/internal/domain/delivery" "galaxy/mail/internal/domain/idempotency" "galaxy/mail/internal/logging" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" oteltrace "go.opentelemetry.io/otel/trace" ) var ( // ErrConflict reports that the idempotency scope already belongs to a // different normalized auth request. ErrConflict = errors.New("accept auth delivery conflict") // ErrServiceUnavailable reports that durable acceptance could not be // completed or recovered safely. ErrServiceUnavailable = errors.New("accept auth delivery service unavailable") ) const ( // AuthTemplateID is the dedicated template family used for auth login-code // deliveries. AuthTemplateID common.TemplateID = "auth.login_code" maxCreateRetries = 3 tracerName = "galaxy/mail/acceptauthdelivery" ) // Outcome identifies the stable auth-delivery acceptance outcome. type Outcome string const ( // OutcomeSent reports that the delivery was accepted into the durable // internal pipeline. OutcomeSent Outcome = "sent" // OutcomeSuppressed reports that outward delivery was intentionally skipped // while the auth flow remained success-shaped. OutcomeSuppressed Outcome = "suppressed" ) // IsKnown reports whether outcome belongs to the stable auth-delivery surface. func (outcome Outcome) IsKnown() bool { switch outcome { case OutcomeSent, OutcomeSuppressed: return true default: return false } } // Result stores the coarse auth-delivery acceptance outcome. type Result struct { // Outcome stores the stable auth-delivery result. Outcome Outcome } // Validate reports whether result contains a supported auth-delivery outcome. func (result Result) Validate() error { if !result.Outcome.IsKnown() { return fmt.Errorf("accept auth delivery outcome %q is unsupported", result.Outcome) } return nil } // Input stores one normalized auth-delivery acceptance command. type Input struct { // IdempotencyKey stores the caller-owned stable deduplication key. IdempotencyKey common.IdempotencyKey // Email stores the normalized recipient mailbox. Email common.Email // Code stores the exact login code. Code string // Locale stores the canonical BCP 47 language tag selected upstream. Locale common.Locale } // Validate reports whether input contains one valid auth-delivery command. func (input Input) Validate() error { if err := input.IdempotencyKey.Validate(); err != nil { return fmt.Errorf("idempotency key: %w", err) } if err := input.Email.Validate(); err != nil { return fmt.Errorf("email: %w", err) } if strings.TrimSpace(input.Code) == "" { return errors.New("code must not be empty") } if strings.TrimSpace(input.Code) != input.Code { return errors.New("code must not contain surrounding whitespace") } if err := input.Locale.Validate(); err != nil { return fmt.Errorf("locale: %w", err) } return nil } // Fingerprint returns the stable idempotency fingerprint of input. func (input Input) Fingerprint() (string, error) { if err := input.Validate(); err != nil { return "", err } normalized := struct { IdempotencyKey string `json:"idempotency_key"` Email string `json:"email"` Code string `json:"code"` Locale string `json:"locale"` }{ IdempotencyKey: input.IdempotencyKey.String(), Email: input.Email.String(), Code: input.Code, Locale: input.Locale.String(), } payload, err := json.Marshal(normalized) if err != nil { return "", fmt.Errorf("marshal auth-delivery fingerprint: %w", err) } sum := sha256.Sum256(payload) return "sha256:" + hex.EncodeToString(sum[:]), nil } // CreateAcceptanceInput stores the durable write set required for one // auth-delivery acceptance attempt. type CreateAcceptanceInput struct { // Delivery stores the accepted delivery record. Delivery deliverydomain.Delivery // FirstAttempt stores the optional first scheduled attempt. FirstAttempt *attempt.Attempt // Idempotency stores the idempotency reservation bound to Delivery. Idempotency idempotency.Record } // Validate reports whether input contains a consistent durable write set. func (input CreateAcceptanceInput) Validate() error { if err := input.Delivery.Validate(); err != nil { return fmt.Errorf("delivery: %w", err) } if err := input.Idempotency.Validate(); err != nil { return fmt.Errorf("idempotency: %w", err) } if input.Idempotency.DeliveryID != input.Delivery.DeliveryID { return errors.New("idempotency delivery id must match delivery id") } if input.Idempotency.Source != input.Delivery.Source { return errors.New("idempotency source must match delivery source") } if input.Idempotency.IdempotencyKey != input.Delivery.IdempotencyKey { return errors.New("idempotency key must match delivery idempotency key") } switch { case input.FirstAttempt == nil: if input.Delivery.Status != deliverydomain.StatusSuppressed { return errors.New("first attempt must not be nil unless delivery is suppressed") } case input.Delivery.Status == deliverydomain.StatusSuppressed: return errors.New("suppressed delivery must not create first attempt") default: if err := input.FirstAttempt.Validate(); err != nil { return fmt.Errorf("first attempt: %w", err) } if input.FirstAttempt.DeliveryID != input.Delivery.DeliveryID { return errors.New("first attempt delivery id must match delivery id") } if input.FirstAttempt.Status != attempt.StatusScheduled { return fmt.Errorf("first attempt status must be %q", attempt.StatusScheduled) } } return nil } // Store describes the durable storage required by the auth-delivery use case. type Store interface { // CreateAcceptance stores the complete durable write set for one auth // acceptance attempt. Implementations must wrap ErrConflict when the write // set races with an already accepted idempotency scope. CreateAcceptance(context.Context, CreateAcceptanceInput) error // GetIdempotency loads the idempotency reservation for one auth-delivery // scope. GetIdempotency(context.Context, deliverydomain.Source, common.IdempotencyKey) (idempotency.Record, bool, error) // GetDelivery loads one accepted delivery by its internal identifier. GetDelivery(context.Context, common.DeliveryID) (deliverydomain.Delivery, bool, error) } // DeliveryIDGenerator describes the source of new internal delivery // identifiers. type DeliveryIDGenerator interface { // NewDeliveryID returns one new internal delivery identifier. NewDeliveryID() (common.DeliveryID, error) } // Clock provides the current wall-clock time. type Clock interface { // Now returns the current time. Now() time.Time } // Telemetry records low-cardinality auth-delivery outcomes. type Telemetry interface { // RecordAuthDeliveryOutcome records one coarse auth-delivery outcome. RecordAuthDeliveryOutcome(context.Context, string) // RecordAcceptedAuthDelivery records one newly accepted auth delivery. RecordAcceptedAuthDelivery(context.Context) // RecordDeliveryStatusTransition records one durable delivery status // transition. RecordDeliveryStatusTransition(context.Context, string, string) } // Config stores the dependencies and policy switches used by Service. type Config struct { // Store owns the durable accepted state. Store Store // DeliveryIDGenerator builds internal delivery identifiers. DeliveryIDGenerator DeliveryIDGenerator // Clock provides wall-clock timestamps. Clock Clock // Telemetry records low-cardinality acceptance outcomes. Telemetry Telemetry // TracerProvider constructs the application span recorder used by the auth // acceptance flow. TracerProvider oteltrace.TracerProvider // Logger writes structured auth acceptance logs. Logger *slog.Logger // IdempotencyTTL stores how long accepted idempotency scopes remain valid. IdempotencyTTL time.Duration // SuppressOutbound reports whether new auth-deliveries should be accepted // directly as suppressed. SuppressOutbound bool } // Service accepts auth login-code deliveries synchronously and durably. type Service struct { store Store deliveryIDGenerator DeliveryIDGenerator clock Clock telemetry Telemetry tracerProvider oteltrace.TracerProvider logger *slog.Logger idempotencyTTL time.Duration suppressOutbound bool } // New constructs Service from cfg. func New(cfg Config) (*Service, error) { switch { case cfg.Store == nil: return nil, errors.New("new accept auth delivery service: nil store") case cfg.DeliveryIDGenerator == nil: return nil, errors.New("new accept auth delivery service: nil delivery id generator") case cfg.Clock == nil: return nil, errors.New("new accept auth delivery service: nil clock") case cfg.IdempotencyTTL <= 0: return nil, errors.New("new accept auth delivery service: non-positive idempotency ttl") default: tracerProvider := cfg.TracerProvider if tracerProvider == nil { tracerProvider = otel.GetTracerProvider() } logger := cfg.Logger if logger == nil { logger = slog.Default() } return &Service{ store: cfg.Store, deliveryIDGenerator: cfg.DeliveryIDGenerator, clock: cfg.Clock, telemetry: cfg.Telemetry, tracerProvider: tracerProvider, logger: logger.With("component", "accept_auth_delivery"), idempotencyTTL: cfg.IdempotencyTTL, suppressOutbound: cfg.SuppressOutbound, }, nil } } // Execute accepts one auth login-code delivery command. func (service *Service) Execute(ctx context.Context, input Input) (Result, error) { if ctx == nil { return Result{}, errors.New("accept auth delivery: nil context") } if service == nil { return Result{}, errors.New("accept auth delivery: nil service") } if err := input.Validate(); err != nil { return Result{}, fmt.Errorf("accept auth delivery: %w", err) } ctx, span := service.tracerProvider.Tracer(tracerName).Start(ctx, "mail.accept_auth_delivery") defer span.End() span.SetAttributes( attribute.String("mail.locale", input.Locale.String()), ) fingerprint, err := input.Fingerprint() if err != nil { return Result{}, fmt.Errorf("accept auth delivery: %w", err) } if result, handled, err := service.resolveReplay(ctx, input.IdempotencyKey, fingerprint); handled { if err != nil { service.recordOutcome(ctx, replayOutcomeForError(err)) return Result{}, err } service.recordOutcome(ctx, "duplicate") return result, nil } for range maxCreateRetries { createInput, result, err := service.buildCreateInput(input, fingerprint) if err != nil { return Result{}, fmt.Errorf("accept auth delivery: %w", err) } if err := service.store.CreateAcceptance(ctx, createInput); err != nil { if !errors.Is(err, ErrConflict) { service.recordOutcome(ctx, "service_unavailable") return Result{}, fmt.Errorf("%w: create acceptance: %v", ErrServiceUnavailable, err) } if replayResult, handled, replayErr := service.resolveReplay(ctx, input.IdempotencyKey, fingerprint); handled { if replayErr != nil { service.recordOutcome(ctx, replayOutcomeForError(replayErr)) return Result{}, replayErr } service.recordOutcome(ctx, "duplicate") return replayResult, nil } continue } service.recordOutcome(ctx, string(result.Outcome)) service.recordAcceptedDelivery(ctx) service.recordStatusTransition(ctx, createInput.Delivery) span.SetAttributes( attribute.String("mail.delivery_id", createInput.Delivery.DeliveryID.String()), attribute.String("mail.source", string(createInput.Delivery.Source)), attribute.String("mail.status", string(createInput.Delivery.Status)), ) logArgs := logging.DeliveryAttrs(createInput.Delivery) logArgs = append(logArgs, "status", string(createInput.Delivery.Status), "outcome", string(result.Outcome), "locale", input.Locale.String(), ) logArgs = append(logArgs, logging.TraceAttrsFromContext(ctx)...) service.logger.Info("auth delivery accepted", logArgs...) return result, nil } service.recordOutcome(ctx, "service_unavailable") return Result{}, fmt.Errorf("%w: delivery id conflict retry limit exceeded", ErrServiceUnavailable) } func (service *Service) buildCreateInput(input Input, fingerprint string) (CreateAcceptanceInput, Result, error) { now := service.clock.Now().UTC().Truncate(time.Millisecond) deliveryID, err := service.deliveryIDGenerator.NewDeliveryID() if err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("%w: generate delivery id: %v", ErrServiceUnavailable, err) } deliveryRecord := deliverydomain.Delivery{ DeliveryID: deliveryID, Source: deliverydomain.SourceAuthSession, PayloadMode: deliverydomain.PayloadModeTemplate, TemplateID: AuthTemplateID, Envelope: deliverydomain.Envelope{To: []common.Email{input.Email}}, Locale: input.Locale, TemplateVariables: map[string]any{ "code": input.Code, }, IdempotencyKey: input.IdempotencyKey, CreatedAt: now, UpdatedAt: now, } result := Result{} var firstAttempt *attempt.Attempt if service.suppressOutbound { deliveryRecord.Status = deliverydomain.StatusSuppressed deliveryRecord.SuppressedAt = ptrTime(now) result.Outcome = OutcomeSuppressed } else { deliveryRecord.Status = deliverydomain.StatusQueued deliveryRecord.AttemptCount = 1 scheduledAttempt := attempt.Attempt{ DeliveryID: deliveryID, AttemptNo: 1, ScheduledFor: now, Status: attempt.StatusScheduled, } firstAttempt = &scheduledAttempt result.Outcome = OutcomeSent } if err := deliveryRecord.Validate(); err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build auth delivery record: %w", err) } if err := result.Validate(); err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build auth delivery result: %w", err) } createInput := CreateAcceptanceInput{ Delivery: deliveryRecord, FirstAttempt: firstAttempt, Idempotency: idempotency.Record{ Source: deliverydomain.SourceAuthSession, IdempotencyKey: input.IdempotencyKey, DeliveryID: deliveryID, RequestFingerprint: fingerprint, CreatedAt: now, ExpiresAt: now.Add(service.idempotencyTTL), }, } if err := createInput.Validate(); err != nil { return CreateAcceptanceInput{}, Result{}, fmt.Errorf("build auth create input: %w", err) } return createInput, result, nil } func (service *Service) recordAcceptedDelivery(ctx context.Context) { if service == nil || service.telemetry == nil { return } service.telemetry.RecordAcceptedAuthDelivery(ctx) } func (service *Service) recordStatusTransition(ctx context.Context, record deliverydomain.Delivery) { if service == nil || service.telemetry == nil { return } service.telemetry.RecordDeliveryStatusTransition(ctx, string(record.Status), string(record.Source)) } func (service *Service) resolveReplay(ctx context.Context, key common.IdempotencyKey, fingerprint string) (Result, bool, error) { record, found, err := service.store.GetIdempotency(ctx, deliverydomain.SourceAuthSession, key) if err != nil { return Result{}, true, fmt.Errorf("%w: load idempotency: %v", ErrServiceUnavailable, err) } if !found { return Result{}, false, nil } if record.RequestFingerprint != fingerprint { return Result{}, true, fmt.Errorf("%w: request conflicts with current state", ErrConflict) } deliveryRecord, found, err := service.store.GetDelivery(ctx, record.DeliveryID) if err != nil { return Result{}, true, fmt.Errorf("%w: load delivery: %v", ErrServiceUnavailable, err) } if !found { return Result{}, true, fmt.Errorf("%w: delivery %q is missing for idempotency scope", ErrServiceUnavailable, record.DeliveryID) } return deriveReplayResult(deliveryRecord) } func deriveReplayResult(record deliverydomain.Delivery) (Result, bool, error) { switch record.Status { case deliverydomain.StatusSuppressed: return Result{Outcome: OutcomeSuppressed}, true, nil case deliverydomain.StatusAccepted, deliverydomain.StatusQueued, deliverydomain.StatusRendered, deliverydomain.StatusSending, deliverydomain.StatusSent, deliverydomain.StatusFailed, deliverydomain.StatusDeadLetter: return Result{Outcome: OutcomeSent}, true, nil default: return Result{}, true, fmt.Errorf("%w: unsupported replay delivery status %q", ErrServiceUnavailable, record.Status) } } func (service *Service) recordOutcome(ctx context.Context, outcome string) { if service == nil || service.telemetry == nil || strings.TrimSpace(outcome) == "" { return } service.telemetry.RecordAuthDeliveryOutcome(ctx, outcome) } func replayOutcomeForError(err error) string { switch { case errors.Is(err, ErrConflict): return "conflict" case errors.Is(err, ErrServiceUnavailable): return "service_unavailable" default: return "" } } func ptrTime(value time.Time) *time.Time { return &value }