// Package revokedevicesession implements the trusted internal single-session // revoke use case. package revokedevicesession import ( "context" "errors" "fmt" "galaxy/authsession/internal/ports" "galaxy/authsession/internal/service/shared" "galaxy/authsession/internal/telemetry" "go.uber.org/zap" ) // Input describes one trusted internal revoke-device-session request. type Input struct { // DeviceSessionID identifies the session that should be revoked. DeviceSessionID string // ReasonCode stores the machine-readable revoke reason code. ReasonCode string // ActorType stores the machine-readable revoke actor type. ActorType string // ActorID stores the optional stable revoke actor identifier. ActorID string } // Result describes the frozen internal revoke-device-session acknowledgement. type Result struct { // Outcome reports whether the current call revoked the session or found it // already revoked. Outcome string // DeviceSessionID identifies the session addressed by the operation. DeviceSessionID string // AffectedSessionCount reports how many sessions changed state during the // current call. AffectedSessionCount int64 } // Service executes the trusted internal revoke-device-session use case. type Service struct { sessionStore ports.SessionStore publisher ports.GatewaySessionProjectionPublisher clock ports.Clock logger *zap.Logger telemetry *telemetry.Runtime } // New returns a revoke-device-session service wired to the required ports. func New(sessionStore ports.SessionStore, publisher ports.GatewaySessionProjectionPublisher, clock ports.Clock) (*Service, error) { return NewWithObservability(sessionStore, publisher, clock, nil, nil) } // NewWithObservability returns a revoke-device-session service wired to the // required ports plus optional structured logging and telemetry dependencies. func NewWithObservability( sessionStore ports.SessionStore, publisher ports.GatewaySessionProjectionPublisher, clock ports.Clock, logger *zap.Logger, telemetryRuntime *telemetry.Runtime, ) (*Service, error) { switch { case sessionStore == nil: return nil, fmt.Errorf("revokedevicesession: session store must not be nil") case publisher == nil: return nil, fmt.Errorf("revokedevicesession: projection publisher must not be nil") case clock == nil: return nil, fmt.Errorf("revokedevicesession: clock must not be nil") default: return &Service{ sessionStore: sessionStore, publisher: publisher, clock: clock, logger: namedLogger(logger, "revoke_device_session"), telemetry: telemetryRuntime, }, nil } } // Execute revokes one device session and republishes the current gateway // projection for the resulting source-of-truth session state. func (s *Service) Execute(ctx context.Context, input Input) (result Result, err error) { logFields := []zap.Field{ zap.String("component", "service"), zap.String("use_case", "revoke_device_session"), } defer func() { shared.LogServiceOutcome(s.logger, ctx, "revoke device session completed", err, logFields...) }() deviceSessionID, err := shared.ParseDeviceSessionID(input.DeviceSessionID) if err != nil { return Result{}, err } logFields = append(logFields, zap.String("device_session_id", deviceSessionID.String())) revocation, err := shared.BuildRevocation(input.ReasonCode, input.ActorType, input.ActorID, s.clock.Now()) if err != nil { return Result{}, err } logFields = append(logFields, zap.String("reason_code", revocation.ReasonCode.String())) storeResult, err := s.sessionStore.Revoke(ctx, ports.RevokeSessionInput{ DeviceSessionID: deviceSessionID, Revocation: revocation, }) if err != nil { switch { case errors.Is(err, ports.ErrNotFound): return Result{}, shared.SessionNotFound() default: return Result{}, shared.ServiceUnavailable(err) } } if err := storeResult.Validate(); err != nil { return Result{}, shared.InternalError(err) } logFields = append(logFields, zap.String("outcome", string(storeResult.Outcome))) if err := shared.PublishSessionProjectionWithTelemetry(ctx, s.publisher, storeResult.Session, s.telemetry, "revoke_device_session"); err != nil { return Result{}, err } affectedSessionCount := int64(0) if storeResult.Outcome == ports.RevokeSessionOutcomeRevoked { affectedSessionCount = 1 s.telemetry.RecordSessionRevocations(ctx, "revoke_device_session", revocation.ReasonCode.String(), affectedSessionCount) } logFields = append(logFields, zap.Int64("affected_session_count", affectedSessionCount)) return Result{ Outcome: string(storeResult.Outcome), DeviceSessionID: storeResult.Session.ID.String(), AffectedSessionCount: affectedSessionCount, }, nil } func namedLogger(logger *zap.Logger, name string) *zap.Logger { if logger == nil { logger = zap.NewNop() } return logger.Named(name) }