// Package devicesession defines the source-of-truth domain model for one // authenticated device session. package devicesession import ( "errors" "fmt" "strings" "time" "galaxy/authsession/internal/domain/common" ) // Status identifies the coarse lifecycle state of one device session. type Status string const ( // StatusActive reports that the session may be used for authenticated // request verification. StatusActive Status = "active" // StatusRevoked reports that the session has been revoked and must no // longer authenticate requests. StatusRevoked Status = "revoked" ) // RevokeReasonDeviceLogout reports that one device logged itself out. const RevokeReasonDeviceLogout common.RevokeReasonCode = "device_logout" // RevokeReasonLogoutAll reports that the session was revoked by a // user-scoped logout-all action. const RevokeReasonLogoutAll common.RevokeReasonCode = "logout_all" // RevokeReasonAdminRevoke reports that the session was revoked // administratively. const RevokeReasonAdminRevoke common.RevokeReasonCode = "admin_revoke" // RevokeReasonUserBlocked reports that the session was revoked because future // auth flow for the user or e-mail was blocked. const RevokeReasonUserBlocked common.RevokeReasonCode = "user_blocked" // IsKnown reports whether Status is one of the device-session states // supported by the current domain model. func (s Status) IsKnown() bool { switch s { case StatusActive, StatusRevoked: return true default: return false } } // CanTransitionTo reports whether the current device-session Status may move // to next under the Stage-2 lifecycle rules. func (s Status) CanTransitionTo(next Status) bool { return s == StatusActive && next == StatusRevoked } // IsKnownRevokeReasonCode reports whether code is one of the built-in revoke // reasons fixed by the Stage-2 domain model. func IsKnownRevokeReasonCode(code common.RevokeReasonCode) bool { switch code { case RevokeReasonDeviceLogout, RevokeReasonLogoutAll, RevokeReasonAdminRevoke, RevokeReasonUserBlocked: return true default: return false } } // Revocation stores the audit metadata recorded when a session is revoked. type Revocation struct { // At reports when the revoke took effect. At time.Time // ReasonCode stores one machine-readable revoke reason code. ReasonCode common.RevokeReasonCode // ActorType stores one machine-readable initiator type. ActorType common.RevokeActorType // ActorID optionally stores a stable initiator identifier. ActorID string } // Validate reports whether Revocation contains all metadata required for a // revoked session. func (r Revocation) Validate() error { if r.At.IsZero() { return errors.New("session revocation time must not be zero") } if err := r.ReasonCode.Validate(); err != nil { return fmt.Errorf("session revocation reason code: %w", err) } if err := r.ActorType.Validate(); err != nil { return fmt.Errorf("session revocation actor type: %w", err) } if strings.TrimSpace(r.ActorID) != r.ActorID { return errors.New("session revocation actor id must not contain surrounding whitespace") } return nil } // Session is the minimal source-of-truth aggregate shape fixed by Stage 2. type Session struct { // ID identifies the device session. ID common.DeviceSessionID // UserID identifies the durable user linkage for the session. UserID common.UserID // ClientPublicKey stores the validated device public key in parsed form. ClientPublicKey common.ClientPublicKey // Status reports the coarse lifecycle state of the session. Status Status // CreatedAt reports when the session was created. CreatedAt time.Time // Revocation is present only when Status is StatusRevoked. Revocation *Revocation } // Validate reports whether Session satisfies the Stage-2 structural and // lifecycle invariants. func (s Session) Validate() error { if err := s.ID.Validate(); err != nil { return fmt.Errorf("session id: %w", err) } if err := s.UserID.Validate(); err != nil { return fmt.Errorf("session user id: %w", err) } if err := s.ClientPublicKey.Validate(); err != nil { return fmt.Errorf("session client public key: %w", err) } if !s.Status.IsKnown() { return fmt.Errorf("session status %q is unsupported", s.Status) } if s.CreatedAt.IsZero() { return errors.New("session creation time must not be zero") } switch s.Status { case StatusActive: if s.Revocation != nil { return errors.New("active session must not contain revocation metadata") } case StatusRevoked: if s.Revocation == nil { return errors.New("revoked session must contain revocation metadata") } if err := s.Revocation.Validate(); err != nil { return fmt.Errorf("session revocation: %w", err) } } return nil }