package shared import ( "crypto/ed25519" "encoding/base64" "fmt" "strings" "time" "galaxy/authsession/internal/domain/common" "galaxy/authsession/internal/domain/devicesession" ) // NormalizeString trims surrounding Unicode whitespace from value. func NormalizeString(value string) string { return strings.TrimSpace(value) } // ParseEmail trims value and validates it against the frozen public e-mail // contract. func ParseEmail(value string) (common.Email, error) { email := common.Email(NormalizeString(value)) if err := email.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return email, nil } // ParseChallengeID trims value and validates it as one challenge identifier. func ParseChallengeID(value string) (common.ChallengeID, error) { challengeID := common.ChallengeID(NormalizeString(value)) if err := challengeID.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return challengeID, nil } // ParseDeviceSessionID trims value and validates it as one device-session // identifier. func ParseDeviceSessionID(value string) (common.DeviceSessionID, error) { deviceSessionID := common.DeviceSessionID(NormalizeString(value)) if err := deviceSessionID.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return deviceSessionID, nil } // ParseUserID trims value and validates it as one user identifier. func ParseUserID(value string) (common.UserID, error) { userID := common.UserID(NormalizeString(value)) if err := userID.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return userID, nil } // ParseRequiredCode trims value and validates it as a required non-empty // confirmation code. func ParseRequiredCode(value string) (string, error) { code := NormalizeString(value) if code == "" { return "", InvalidRequest("code must not be empty") } return code, nil } // ParseClientPublicKey trims value and validates it as the standard // base64-encoded raw 32-byte Ed25519 public key expected by the public auth // contract. func ParseClientPublicKey(value string) (common.ClientPublicKey, error) { normalized := NormalizeString(value) if normalized == "" { return common.ClientPublicKey{}, InvalidClientPublicKey() } decoded, err := base64.StdEncoding.Strict().DecodeString(normalized) if err != nil || len(decoded) != ed25519.PublicKeySize { return common.ClientPublicKey{}, InvalidClientPublicKey() } key, err := common.NewClientPublicKey(ed25519.PublicKey(decoded)) if err != nil { return common.ClientPublicKey{}, InvalidClientPublicKey() } return key, nil } // ParseTimeZone trims value and validates it as an IANA time zone name. func ParseTimeZone(value string) (string, error) { timeZone := NormalizeString(value) if timeZone == "" { return "", InvalidRequest("time_zone must not be empty") } if _, err := time.LoadLocation(timeZone); err != nil { return "", InvalidRequest("time_zone must be a valid IANA time zone name") } return timeZone, nil } // ParseRevokeReasonCode trims value and validates it as one machine-readable // revoke reason code. func ParseRevokeReasonCode(value string) (common.RevokeReasonCode, error) { code := common.RevokeReasonCode(NormalizeString(value)) if err := code.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return code, nil } // ParseRevokeActorType trims value and validates it as one machine-readable // revoke actor type. func ParseRevokeActorType(value string) (common.RevokeActorType, error) { actorType := common.RevokeActorType(NormalizeString(value)) if err := actorType.Validate(); err != nil { return "", InvalidRequest(err.Error()) } return actorType, nil } // ParseOptionalActorID trims value and validates it as one optional stable // actor identifier. func ParseOptionalActorID(value string) (string, error) { actorID := NormalizeString(value) if actorID != value { return "", InvalidRequest("actor_id must not contain surrounding whitespace") } return actorID, nil } // BuildRevocation validates one revoke request payload and returns the domain // revocation metadata applied to a session mutation. func BuildRevocation(reasonCode string, actorType string, actorID string, at time.Time) (devicesession.Revocation, error) { if at.IsZero() { return devicesession.Revocation{}, InternalError(fmt.Errorf("revocation time must not be zero")) } parsedReasonCode, err := ParseRevokeReasonCode(reasonCode) if err != nil { return devicesession.Revocation{}, err } parsedActorType, err := ParseRevokeActorType(actorType) if err != nil { return devicesession.Revocation{}, err } parsedActorID, err := ParseOptionalActorID(actorID) if err != nil { return devicesession.Revocation{}, err } revocation := devicesession.Revocation{ At: at.UTC(), ReasonCode: parsedReasonCode, ActorType: parsedActorType, ActorID: parsedActorID, } if err := revocation.Validate(); err != nil { return devicesession.Revocation{}, InternalError(fmt.Errorf("build revocation: %w", err)) } return revocation, nil }