package ports import ( "context" "errors" "fmt" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/domain/devicesession" ) // SessionStore provides source-of-truth persistence for device sessions // without exposing storage-specific encoding or transaction primitives. type SessionStore interface { // Get returns the stored session for deviceSessionID. Implementations must // wrap ErrNotFound when deviceSessionID does not exist. Get(ctx context.Context, deviceSessionID common.DeviceSessionID) (devicesession.Session, error) // ListByUserID returns every stored session for userID in newest-first // order. Implementations must return an empty slice, not ErrNotFound, when // userID has no stored sessions. ListByUserID(ctx context.Context, userID common.UserID) ([]devicesession.Session, error) // CountActiveByUserID returns the number of active sessions currently stored // for userID. CountActiveByUserID(ctx context.Context, userID common.UserID) (int, error) // Create persists record as a new device session. Implementations must wrap // ErrConflict when record.ID already exists. Create(ctx context.Context, record devicesession.Session) error // Revoke stores a revoked view of one target session. Implementations must // wrap ErrNotFound when input.DeviceSessionID does not exist. Revoke(ctx context.Context, input RevokeSessionInput) (RevokeSessionResult, error) // RevokeAllByUserID stores revoked views for all currently active sessions // owned by input.UserID. RevokeAllByUserID(ctx context.Context, input RevokeUserSessionsInput) (RevokeUserSessionsResult, error) } // RevokeSessionInput describes one single-session revoke mutation requested // from SessionStore. type RevokeSessionInput struct { // DeviceSessionID identifies the session that should be revoked. DeviceSessionID common.DeviceSessionID // Revocation stores the audit metadata that must be attached to the revoked // session. Revocation devicesession.Revocation } // Validate reports whether RevokeSessionInput contains a complete revoke // request. func (i RevokeSessionInput) Validate() error { if err := i.DeviceSessionID.Validate(); err != nil { return fmt.Errorf("revoke session input device session id: %w", err) } if err := i.Revocation.Validate(); err != nil { return fmt.Errorf("revoke session input revocation: %w", err) } return nil } // RevokeSessionOutcome identifies the coarse outcome of revoking one device // session. type RevokeSessionOutcome string const ( // RevokeSessionOutcomeRevoked reports that an active session was moved to // the revoked state by the current mutation. RevokeSessionOutcomeRevoked RevokeSessionOutcome = "revoked" // RevokeSessionOutcomeAlreadyRevoked reports that the requested session had // already been revoked before the current mutation. RevokeSessionOutcomeAlreadyRevoked RevokeSessionOutcome = "already_revoked" ) // IsKnown reports whether RevokeSessionOutcome is supported by the current // session-store contract. func (o RevokeSessionOutcome) IsKnown() bool { switch o { case RevokeSessionOutcomeRevoked, RevokeSessionOutcomeAlreadyRevoked: return true default: return false } } // RevokeSessionResult describes the stable outcome returned by SessionStore // after a single-session revoke attempt. type RevokeSessionResult struct { // Outcome reports whether the session was revoked just now or had already // been revoked. Outcome RevokeSessionOutcome // Session stores the current source-of-truth session state after the revoke // attempt. Session devicesession.Session } // Validate reports whether RevokeSessionResult satisfies the session-store // contract invariants. func (r RevokeSessionResult) Validate() error { if !r.Outcome.IsKnown() { return fmt.Errorf("revoke session result outcome %q is unsupported", r.Outcome) } if err := r.Session.Validate(); err != nil { return fmt.Errorf("revoke session result session: %w", err) } if r.Session.Status != devicesession.StatusRevoked { return errors.New("revoke session result session must be revoked") } return nil } // RevokeUserSessionsInput describes one bulk user-session revoke mutation // requested from SessionStore. type RevokeUserSessionsInput struct { // UserID identifies the owner whose active sessions should be revoked. UserID common.UserID // Revocation stores the audit metadata that must be attached to every // revoked session. Revocation devicesession.Revocation } // Validate reports whether RevokeUserSessionsInput contains a complete bulk // revoke request. func (i RevokeUserSessionsInput) Validate() error { if err := i.UserID.Validate(); err != nil { return fmt.Errorf("revoke user sessions input user id: %w", err) } if err := i.Revocation.Validate(); err != nil { return fmt.Errorf("revoke user sessions input revocation: %w", err) } return nil } // RevokeUserSessionsOutcome identifies the coarse outcome of revoking all // active sessions of one user. type RevokeUserSessionsOutcome string const ( // RevokeUserSessionsOutcomeRevoked reports that one or more active sessions // were revoked by the current mutation. RevokeUserSessionsOutcomeRevoked RevokeUserSessionsOutcome = "revoked" // RevokeUserSessionsOutcomeNoActiveSessions reports that the target user did // not currently own any active sessions. RevokeUserSessionsOutcomeNoActiveSessions RevokeUserSessionsOutcome = "no_active_sessions" ) // IsKnown reports whether RevokeUserSessionsOutcome is supported by the // current session-store contract. func (o RevokeUserSessionsOutcome) IsKnown() bool { switch o { case RevokeUserSessionsOutcomeRevoked, RevokeUserSessionsOutcomeNoActiveSessions: return true default: return false } } // RevokeUserSessionsResult describes the stable outcome returned by // SessionStore after one bulk revoke attempt. type RevokeUserSessionsResult struct { // Outcome reports whether at least one active session was revoked. Outcome RevokeUserSessionsOutcome // UserID identifies the owner whose sessions were evaluated. UserID common.UserID // Sessions stores the current source-of-truth session states for every // session affected by the bulk revoke operation. Sessions []devicesession.Session } // Validate reports whether RevokeUserSessionsResult satisfies the bulk // session-store contract invariants. func (r RevokeUserSessionsResult) Validate() error { if !r.Outcome.IsKnown() { return fmt.Errorf("revoke user sessions result outcome %q is unsupported", r.Outcome) } if err := r.UserID.Validate(); err != nil { return fmt.Errorf("revoke user sessions result user id: %w", err) } for index, session := range r.Sessions { if err := session.Validate(); err != nil { return fmt.Errorf("revoke user sessions result session %d: %w", index, err) } if session.Status != devicesession.StatusRevoked { return fmt.Errorf("revoke user sessions result session %d must be revoked", index) } if session.UserID != r.UserID { return fmt.Errorf("revoke user sessions result session %d belongs to %q, want %q", index, session.UserID, r.UserID) } } switch r.Outcome { case RevokeUserSessionsOutcomeRevoked: if len(r.Sessions) == 0 { return errors.New("revoke user sessions result must include sessions when outcome is revoked") } case RevokeUserSessionsOutcomeNoActiveSessions: if len(r.Sessions) != 0 { return errors.New("revoke user sessions result must not include sessions when outcome is no_active_sessions") } } return nil }