// Package gatewayprojection defines the gateway-facing integration snapshot // model that stays separate from source-of-truth session entities. package gatewayprojection import ( "crypto/ed25519" "encoding/base64" "errors" "fmt" "strings" "time" "galaxy/authsession/internal/domain/common" ) // Status identifies the coarse lifecycle state projected to the gateway. type Status string const ( // StatusActive reports that the projected session may authenticate // requests on the gateway hot path. StatusActive Status = "active" // StatusRevoked reports that the projected session must be rejected on the // gateway hot path. StatusRevoked Status = "revoked" ) // IsKnown reports whether Status is one of the projection states supported by // the current integration model. func (s Status) IsKnown() bool { switch s { case StatusActive, StatusRevoked: return true default: return false } } // Snapshot stores the gateway-facing session projection without exposing any // Redis-specific field naming or storage encoding. type Snapshot struct { // DeviceSessionID identifies the projected device session. DeviceSessionID common.DeviceSessionID // UserID identifies the projected user. UserID common.UserID // ClientPublicKey stores the standard base64-encoded raw 32-byte Ed25519 // public key string expected by the gateway. ClientPublicKey string // Status reports whether the projected session is active or revoked. Status Status // RevokedAt optionally reports when the revoke took effect. RevokedAt *time.Time // RevokeReasonCode optionally stores the machine-readable revoke reason. RevokeReasonCode common.RevokeReasonCode // RevokeActorType optionally stores the machine-readable revoke actor type. RevokeActorType common.RevokeActorType // RevokeActorID optionally stores a stable revoke actor identifier. RevokeActorID string } // Validate reports whether Snapshot satisfies the Stage-2 structural // invariants. func (s Snapshot) Validate() error { if err := s.DeviceSessionID.Validate(); err != nil { return fmt.Errorf("gateway projection device session id: %w", err) } if err := s.UserID.Validate(); err != nil { return fmt.Errorf("gateway projection user id: %w", err) } if err := validateClientPublicKey(s.ClientPublicKey); err != nil { return fmt.Errorf("gateway projection client public key: %w", err) } if !s.Status.IsKnown() { return fmt.Errorf("gateway projection status %q is unsupported", s.Status) } if s.Status == StatusActive { if s.RevokedAt != nil { return errors.New("active gateway projection must not contain revoked time") } if !s.RevokeReasonCode.IsZero() { return errors.New("active gateway projection must not contain revoke reason code") } if !s.RevokeActorType.IsZero() { return errors.New("active gateway projection must not contain revoke actor type") } if s.RevokeActorID != "" { return errors.New("active gateway projection must not contain revoke actor id") } return nil } if s.RevokedAt != nil && s.RevokedAt.IsZero() { return errors.New("gateway projection revoked time must not be zero") } if !s.RevokeReasonCode.IsZero() { if err := s.RevokeReasonCode.Validate(); err != nil { return fmt.Errorf("gateway projection revoke reason code: %w", err) } } if !s.RevokeActorType.IsZero() { if err := s.RevokeActorType.Validate(); err != nil { return fmt.Errorf("gateway projection revoke actor type: %w", err) } } if s.RevokeActorType.IsZero() && s.RevokeActorID != "" { return errors.New("gateway projection revoke actor id requires revoke actor type") } if strings.TrimSpace(s.RevokeActorID) != s.RevokeActorID { return errors.New("gateway projection revoke actor id must not contain surrounding whitespace") } return nil } func validateClientPublicKey(value string) error { switch { case strings.TrimSpace(value) == "": return errors.New("client public key must not be empty") case strings.TrimSpace(value) != value: return errors.New("client public key must not contain surrounding whitespace") } decoded, err := base64.StdEncoding.DecodeString(value) if err != nil { return fmt.Errorf("client public key must be valid base64: %w", err) } if len(decoded) != ed25519.PublicKeySize { return fmt.Errorf("client public key must contain exactly %d bytes", ed25519.PublicKeySize) } return nil }