Explorar el Código

feat: introduce webhook client configuration and enhance error handling in logger and database

lblt hace 2 semanas
padre
commit
803125018c

+ 48 - 0
examples/webhook/main.go

@@ -0,0 +1,48 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"time"
+
+	"git.linuxforward.com/byom/byom-golang-lib/pkg/webhook"
+)
+
+func main() {
+	// Create webhook configuration
+	cfg := webhook.Config{
+		BaseURL:      "https://api.example.com",
+		Domain:       "myapp.com",
+		SecretKey:    "your-secret-key",
+		Timeout:      5 * time.Second,
+		MaxRetries:   3,
+		RetryBackoff: time.Second,
+	}
+
+	// Create new webhook client
+	client, err := webhook.New(cfg)
+	if err != nil {
+		log.Fatalf("Failed to create webhook client: %v", err)
+	}
+	defer client.Close()
+
+	// Create context with timeout
+	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+	defer cancel()
+
+	// Send webhook
+	err = client.Send(ctx, "user@example.com", "user.created")
+	if err != nil {
+		log.Fatalf("Failed to send webhook: %v", err)
+	}
+
+	fmt.Println("Webhook sent successfully!")
+
+	// Example of validating a webhook signature on the receiver side
+	payload := []byte(`{"email":"user@example.com","domain":"myapp.com","action":"user.created"}`)
+	signature := "computed-signature-from-request-header"
+
+	isValid := webhook.ValidateSignature(payload, signature, cfg.SecretKey)
+	fmt.Printf("Webhook signature is valid: %v\n", isValid)
+}

+ 85 - 0
pkg/auth/errors.go

@@ -0,0 +1,85 @@
+// Package auth provides authentication functionality
+package auth
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Common auth error types
+var (
+	// ErrInvalidToken indicates the JWT token is invalid or malformed
+	ErrInvalidToken = errors.New("invalid token")
+
+	// ErrExpiredToken indicates the JWT token has expired
+	ErrExpiredToken = errors.New("token expired")
+
+	// ErrInvalidSigningMethod indicates an unsupported signing method was used
+	ErrInvalidSigningMethod = errors.New("invalid signing method")
+
+	// ErrInvalidClaims indicates the token claims are invalid or missing
+	ErrInvalidClaims = errors.New("invalid token claims")
+
+	// ErrEmptyToken indicates an empty token was provided
+	ErrEmptyToken = errors.New("empty token")
+
+	// ErrInvalidConfig indicates invalid JWT configuration
+	ErrInvalidConfig = errors.New("invalid JWT configuration")
+)
+
+// Error represents an authentication-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "generate", "validate", "parse")
+	Op string
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	msg := fmt.Sprintf("auth %s failed", e.Op)
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new auth error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// IsExpiredTokenError returns true if the error indicates an expired token
+func IsExpiredTokenError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrExpiredToken)
+}
+
+// IsInvalidTokenError returns true if the error indicates an invalid token
+func IsInvalidTokenError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && (errors.Is(e.Err, ErrInvalidToken) ||
+		errors.Is(e.Err, ErrInvalidSigningMethod) ||
+		errors.Is(e.Err, ErrInvalidClaims))
+}

+ 5 - 6
pkg/auth/jwt.go

@@ -3,7 +3,6 @@ package auth
 import (
 	"time"
 
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 	"github.com/golang-jwt/jwt/v5"
 )
 
@@ -27,7 +26,7 @@ func (s *JWTService) GenerateToken(userID string, duration time.Duration) (strin
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
 	signedToken, err := token.SignedString(s.secretKey)
 	if err != nil {
-		return "", errors.NewAuthError("generate", err)
+		return "", NewError("generate", err, "failed to sign token")
 	}
 
 	return signedToken, nil
@@ -35,23 +34,23 @@ func (s *JWTService) GenerateToken(userID string, duration time.Duration) (strin
 
 func (s *JWTService) ValidateToken(tokenString string) (jwt.MapClaims, error) {
 	if tokenString == "" {
-		return nil, errors.NewAuthError("validate", errors.ErrInvalidInput)
+		return nil, NewError("validate", ErrEmptyToken, "token string is empty")
 	}
 
 	token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
 		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
-			return nil, errors.NewAuthError("validate", errors.ErrInvalidInput)
+			return nil, NewError("validate", ErrInvalidSigningMethod, "unexpected signing method")
 		}
 		return s.secretKey, nil
 	})
 
 	if err != nil {
-		return nil, errors.NewAuthError("parse", err)
+		return nil, NewError("parse", ErrInvalidToken, err.Error())
 	}
 
 	if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
 		return claims, nil
 	}
 
-	return nil, errors.NewAuthError("validate", errors.ErrInvalidInput)
+	return nil, NewError("validate", ErrInvalidClaims, "token claims are invalid")
 }

+ 20 - 15
pkg/config/config.go

@@ -7,7 +7,6 @@ import (
 
 	"git.linuxforward.com/byom/byom-golang-lib/pkg/auth"
 	"git.linuxforward.com/byom/byom-golang-lib/pkg/database"
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 	"git.linuxforward.com/byom/byom-golang-lib/pkg/logger"
 	"git.linuxforward.com/byom/byom-golang-lib/pkg/server"
 	"git.linuxforward.com/byom/byom-golang-lib/pkg/storage"
@@ -32,18 +31,18 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
 		return err
 	}
 	if c.App == nil {
-		return errors.NewConfigError("app", errors.ErrInvalidInput)
+		return NewFieldError("validate", "root", "app", "", ErrMissingRequired, "app section is required")
 	}
 
 	// Conditional validation based on app name
 	switch c.App.Name {
 	case "design":
 		if c.CloudComputing == nil {
-			return errors.NewConfigError("cloud_computing", errors.ErrInvalidInput)
+			return NewFieldError("validate", "root", "cloud_computing", "", ErrMissingRequired, "cloud_computing section is required for design app")
 		}
 	case "trends":
 		if c.SocialNetworks == nil {
-			return errors.NewConfigError("social_networks", errors.ErrInvalidInput)
+			return NewFieldError("validate", "root", "social_networks", "", ErrMissingRequired, "social_networks section is required for trends app")
 		}
 	}
 	return nil
@@ -61,7 +60,7 @@ func (c *App) UnmarshalYAML(unmarshal func(interface{}) error) error {
 		return err
 	}
 	if c.Name == "" {
-		return errors.NewConfigError("app.name", errors.ErrInvalidInput)
+		return NewFieldError("validate", "app", "name", "", ErrMissingRequired, "app name is required")
 	}
 	if c.Environment == "" {
 		c.Environment = "development"
@@ -69,7 +68,7 @@ func (c *App) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	// Validate app name
 	validNames := map[string]bool{"core": true, "design": true, "trends": true}
 	if !validNames[c.Name] {
-		return errors.NewConfigError("app.name", errors.ErrInvalidInput)
+		return NewFieldError("validate", "app", "name", c.Name, ErrInvalidAppName, "app name must be one of: core, design, trends")
 	}
 	return nil
 }
@@ -87,13 +86,13 @@ func (c *CloudComputing) UnmarshalYAML(unmarshal func(interface{}) error) error
 		return err
 	}
 	if c.Provider == "" {
-		return errors.NewConfigError("cloud_computing.provider", errors.ErrInvalidInput)
+		return NewFieldError("validate", "cloud_computing", "provider", "", ErrMissingRequired, "provider is required")
 	}
 	if c.Region == "" {
-		return errors.NewConfigError("cloud_computing.region", errors.ErrInvalidInput)
+		return NewFieldError("validate", "cloud_computing", "region", "", ErrMissingRequired, "region is required")
 	}
 	if c.MaxCapacity <= 0 {
-		return errors.NewConfigError("cloud_computing.max_capacity", errors.ErrInvalidInput)
+		return NewFieldError("validate", "cloud_computing", "max_capacity", fmt.Sprintf("%d", c.MaxCapacity), ErrInvalidValue, "max_capacity must be greater than 0")
 	}
 	return nil
 }
@@ -115,18 +114,18 @@ func (c *SocialNetworks) UnmarshalYAML(unmarshal func(interface{}) error) error
 		return err
 	}
 	if len(c.Networks) == 0 {
-		return errors.NewConfigError("social_networks", errors.ErrInvalidInput)
+		return NewFieldError("validate", "social_networks", "networks", "", ErrMissingRequired, "at least one social network is required")
 	}
 	for i, network := range c.Networks {
 		section := fmt.Sprintf("social_networks.networks[%d]", i)
 		if network.Name == "" {
-			return errors.NewConfigError(section+".name", errors.ErrInvalidInput)
+			return NewFieldError("validate", section, "name", "", ErrMissingRequired, "network name is required")
 		}
 		if network.APIKey == "" {
-			return errors.NewConfigError(section+".api_key", errors.ErrInvalidInput)
+			return NewFieldError("validate", section, "api_key", "", ErrMissingRequired, "API key is required")
 		}
 		if network.APIToken == "" {
-			return errors.NewConfigError(section+".api_token", errors.ErrInvalidInput)
+			return NewFieldError("validate", section, "api_token", "", ErrMissingRequired, "API token is required")
 		}
 	}
 	return nil
@@ -135,13 +134,19 @@ func (c *SocialNetworks) UnmarshalYAML(unmarshal func(interface{}) error) error
 func ReadConfig(configPath string) (*Config, error) {
 	data, err := os.ReadFile(configPath)
 	if err != nil {
-		return nil, errors.NewConfigError("read", err)
+		if os.IsNotExist(err) {
+			return nil, NewError("read", ErrFileNotFound, configPath)
+		}
+		if os.IsPermission(err) {
+			return nil, NewError("read", ErrFilePermission, configPath)
+		}
+		return nil, NewError("read", err, "failed to read config file")
 	}
 
 	config := &Config{}
 	err = yaml.Unmarshal(data, config)
 	if err != nil {
-		return nil, errors.NewConfigError("parse", err)
+		return nil, NewError("parse", ErrInvalidYAML, err.Error())
 	}
 
 	// Validate all config sections that implement Validator interface

+ 131 - 0
pkg/config/errors.go

@@ -0,0 +1,131 @@
+// Package config provides configuration management functionality
+package config
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Common config error types
+var (
+	// ErrFileNotFound indicates the config file does not exist
+	ErrFileNotFound = errors.New("config file not found")
+
+	// ErrFilePermission indicates permission issues with the config file
+	ErrFilePermission = errors.New("insufficient permissions for config file")
+
+	// ErrInvalidYAML indicates YAML parsing or syntax errors
+	ErrInvalidYAML = errors.New("invalid YAML format")
+
+	// ErrMissingRequired indicates a required field is missing
+	ErrMissingRequired = errors.New("missing required field")
+
+	// ErrInvalidValue indicates a field has an invalid value
+	ErrInvalidValue = errors.New("invalid field value")
+
+	// ErrInvalidSection indicates an invalid or missing config section
+	ErrInvalidSection = errors.New("invalid config section")
+
+	// ErrValidationFailed indicates config validation failure
+	ErrValidationFailed = errors.New("config validation failed")
+
+	// ErrInvalidAppName indicates an invalid application name
+	ErrInvalidAppName = errors.New("invalid application name")
+
+	// ErrInvalidEnvironment indicates an invalid environment
+	ErrInvalidEnvironment = errors.New("invalid environment")
+)
+
+// Error represents a configuration-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "read", "parse", "validate")
+	Op string
+
+	// Section is the config section involved (if applicable)
+	Section string
+
+	// Field is the specific field involved (if applicable)
+	Field string
+
+	// Value is the invalid value (if applicable)
+	Value string
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	msg := fmt.Sprintf("config %s failed", e.Op)
+	if e.Section != "" {
+		msg = fmt.Sprintf("%s in section %q", msg, e.Section)
+	}
+	if e.Field != "" {
+		msg = fmt.Sprintf("%s at field %q", msg, e.Field)
+	}
+	if e.Value != "" {
+		msg = fmt.Sprintf("%s with value %q", msg, e.Value)
+	}
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new config error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewFieldError creates a new config field error
+func NewFieldError(op string, section string, field string, value string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Section: section,
+		Field:   field,
+		Value:   value,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// IsValidationError returns true if the error indicates a validation problem
+func IsValidationError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && (errors.Is(e.Err, ErrValidationFailed) ||
+		errors.Is(e.Err, ErrMissingRequired) ||
+		errors.Is(e.Err, ErrInvalidValue))
+}
+
+// IsFileError returns true if the error is related to file operations
+func IsFileError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && (errors.Is(e.Err, ErrFileNotFound) ||
+		errors.Is(e.Err, ErrFilePermission))
+}
+
+// IsParseError returns true if the error is related to parsing
+func IsParseError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrInvalidYAML)
+}

+ 123 - 0
pkg/database/errors.go

@@ -0,0 +1,123 @@
+// Package database provides database functionality
+package database
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Common database error types
+var (
+	// ErrInvalidConfig indicates missing or invalid database configuration
+	ErrInvalidConfig = errors.New("invalid database configuration")
+
+	// ErrConnectionFailed indicates failure to connect to the database
+	ErrConnectionFailed = errors.New("failed to connect to database")
+
+	// ErrQueryFailed indicates a query execution failure
+	ErrQueryFailed = errors.New("query execution failed")
+
+	// ErrTransactionFailed indicates a transaction operation failure
+	ErrTransactionFailed = errors.New("transaction operation failed")
+
+	// ErrMigrationFailed indicates a database migration failure
+	ErrMigrationFailed = errors.New("database migration failed")
+
+	// ErrPragmaFailed indicates failure to set database pragma
+	ErrPragmaFailed = errors.New("failed to set database pragma")
+
+	// ErrInvalidPath indicates an invalid database file path
+	ErrInvalidPath = errors.New("invalid database path")
+
+	// ErrDatabaseLocked indicates the database is locked
+	ErrDatabaseLocked = errors.New("database is locked")
+
+	// ErrNoRows indicates no rows were found
+	ErrNoRows = errors.New("no rows found")
+
+	// ErrDuplicateKey indicates a unique constraint violation
+	ErrDuplicateKey = errors.New("duplicate key violation")
+)
+
+// Error represents a database-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "connect", "query", "migrate")
+	Op string
+
+	// Query is the SQL query that failed (if applicable)
+	Query string
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	msg := fmt.Sprintf("database %s failed", e.Op)
+	if e.Query != "" {
+		// Truncate long queries in the error message
+		const maxQueryLen = 100
+		query := e.Query
+		if len(query) > maxQueryLen {
+			query = query[:maxQueryLen] + "..."
+		}
+		msg = fmt.Sprintf("%s for query %q", msg, query)
+	}
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new database error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewQueryError creates a new database error with query context
+func NewQueryError(op string, query string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Query:   query,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// IsLockError returns true if the error indicates a database lock
+func IsLockError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrDatabaseLocked)
+}
+
+// IsNotFoundError returns true if the error indicates no rows were found
+func IsNotFoundError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrNoRows)
+}
+
+// IsDuplicateError returns true if the error indicates a duplicate key
+func IsDuplicateError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrDuplicateKey)
+}

+ 6 - 4
pkg/database/sqlite.go

@@ -4,19 +4,21 @@ import (
 	"database/sql"
 	"time"
 
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 	_ "github.com/mattn/go-sqlite3"
 )
 
 func NewSQLiteDB(config *Config) (*sql.DB, error) {
 	db, err := sql.Open("sqlite3", config.Path)
 	if err != nil {
-		return nil, errors.NewDatabaseError("open", err)
+		if config.Path == "" {
+			return nil, NewError("open", ErrInvalidPath, "database path is required")
+		}
+		return nil, NewError("open", ErrConnectionFailed, err.Error())
 	}
 
 	// Test connection
 	if err := db.Ping(); err != nil {
-		return nil, errors.NewDatabaseError("ping", err)
+		return nil, NewError("ping", ErrConnectionFailed, "failed to verify database connection")
 	}
 
 	// Set connection pool settings
@@ -29,7 +31,7 @@ func NewSQLiteDB(config *Config) (*sql.DB, error) {
 		for _, pragma := range config.Pragmas {
 			_, err := db.Exec(pragma)
 			if err != nil {
-				return nil, errors.NewDatabaseError("pragma", err)
+				return nil, NewQueryError("pragma", pragma, ErrPragmaFailed, err.Error())
 			}
 		}
 	}

+ 0 - 139
pkg/errors/errors.go

@@ -1,139 +0,0 @@
-package errors
-
-import (
-	stderrors "errors"
-	"fmt"
-)
-
-// Is reports whether any error in err's chain matches target.
-var Is = stderrors.Is
-
-// As finds the first error in err's chain that matches target, and if so, sets
-// target to that error value and returns true.
-var As = stderrors.As
-
-// Unwrap returns the result of calling the Unwrap method on err, if err's
-// type contains an Unwrap method returning error.
-// Otherwise, Unwrap returns nil.
-var Unwrap = stderrors.Unwrap
-
-// Standard errors that can be used across packages
-var (
-	ErrNotFound          = stderrors.New("resource not found")
-	ErrInvalidInput      = stderrors.New("invalid input")
-	ErrUnauthorized      = stderrors.New("unauthorized")
-	ErrInternalServer    = stderrors.New("internal server error")
-	ErrDatabaseOperation = stderrors.New("database operation failed")
-)
-
-// DatabaseError represents a database-related error
-type DatabaseError struct {
-	Operation string
-	Err       error
-}
-
-func (e *DatabaseError) Error() string {
-	return fmt.Sprintf("database error during %s: %v", e.Operation, e.Err)
-}
-
-func (e *DatabaseError) Unwrap() error {
-	return e.Err
-}
-
-// NewDatabaseError creates a new DatabaseError
-func NewDatabaseError(operation string, err error) *DatabaseError {
-	return &DatabaseError{
-		Operation: operation,
-		Err:       err,
-	}
-}
-
-// AuthError represents an authentication-related error
-type AuthError struct {
-	Action string
-	Err    error
-}
-
-func (e *AuthError) Error() string {
-	return fmt.Sprintf("authentication error during %s: %v", e.Action, e.Err)
-}
-
-func (e *AuthError) Unwrap() error {
-	return e.Err
-}
-
-// NewAuthError creates a new AuthError
-func NewAuthError(action string, err error) *AuthError {
-	return &AuthError{
-		Action: action,
-		Err:    err,
-	}
-}
-
-// StorageError represents a storage-related error
-type StorageError struct {
-	Operation string
-	Bucket    string
-	Err       error
-}
-
-func (e *StorageError) Error() string {
-	return fmt.Sprintf("storage error during %s on bucket %s: %v", e.Operation, e.Bucket, e.Err)
-}
-
-func (e *StorageError) Unwrap() error {
-	return e.Err
-}
-
-// NewStorageError creates a new StorageError
-func NewStorageError(operation, bucket string, err error) *StorageError {
-	return &StorageError{
-		Operation: operation,
-		Bucket:    bucket,
-		Err:       err,
-	}
-}
-
-// ConfigError represents a configuration-related error
-type ConfigError struct {
-	Section string
-	Err     error
-}
-
-func (e *ConfigError) Error() string {
-	return fmt.Sprintf("configuration error in %s: %v", e.Section, e.Err)
-}
-
-func (e *ConfigError) Unwrap() error {
-	return e.Err
-}
-
-// NewConfigError creates a new ConfigError
-func NewConfigError(section string, err error) *ConfigError {
-	return &ConfigError{
-		Section: section,
-		Err:     err,
-	}
-}
-
-// SMTPError represents an email-related error
-type SMTPError struct {
-	Operation string
-	Err       error
-}
-
-func (e *SMTPError) Error() string {
-	return fmt.Sprintf("smtp error during %s: %v", e.Operation, e.Err)
-}
-
-func (e *SMTPError) Unwrap() error {
-	return e.Err
-}
-
-// NewSMTPError creates a new SMTPError
-func NewSMTPError(operation string, err error) *SMTPError {
-	return &SMTPError{
-		Operation: operation,
-		Err:       err,
-	}
-}

+ 105 - 0
pkg/logger/errors.go

@@ -0,0 +1,105 @@
+// Package logger provides logging functionality
+package logger
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Common logger error types
+var (
+	// ErrInvalidFormat indicates an invalid log format was specified
+	ErrInvalidFormat = errors.New("invalid log format")
+
+	// ErrInvalidLevel indicates an invalid log level was specified
+	ErrInvalidLevel = errors.New("invalid log level")
+
+	// ErrOutputFailed indicates a failure to set or write to the log output
+	ErrOutputFailed = errors.New("failed to set or write to log output")
+
+	// ErrFormatterFailed indicates a failure in the log formatter
+	ErrFormatterFailed = errors.New("log formatter failed")
+
+	// ErrInvalidConfig indicates missing or invalid logger configuration
+	ErrInvalidConfig = errors.New("invalid logger configuration")
+)
+
+// Error represents a logger-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "format", "level", "output")
+	Op string
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+
+	// Setting indicates the configuration setting involved (if applicable)
+	Setting string
+
+	// Value indicates the invalid value (if applicable)
+	Value string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	msg := fmt.Sprintf("logger %s failed", e.Op)
+	if e.Setting != "" {
+		msg = fmt.Sprintf("%s for setting %q", msg, e.Setting)
+	}
+	if e.Value != "" {
+		msg = fmt.Sprintf("%s with value %q", msg, e.Value)
+	}
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new logger error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewConfigError creates a new logger configuration error
+func NewConfigError(op string, setting string, value string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Setting: setting,
+		Value:   value,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// IsConfigError returns true if the error indicates a configuration problem
+func IsConfigError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && (errors.Is(e.Err, ErrInvalidFormat) ||
+		errors.Is(e.Err, ErrInvalidLevel) ||
+		errors.Is(e.Err, ErrInvalidConfig))
+}
+
+// IsOutputError returns true if the error indicates an output problem
+func IsOutputError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrOutputFailed)
+}

+ 14 - 3
pkg/logger/logrus.go

@@ -3,7 +3,6 @@ package logger
 import (
 	"os"
 
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 	"github.com/sirupsen/logrus"
 )
 
@@ -27,7 +26,13 @@ func NewLogger(config *Config) (*logrus.Logger, error) {
 			FullTimestamp: true,
 		})
 	default:
-		return nil, errors.NewConfigError("logger.formatter", errors.ErrInvalidInput)
+		return nil, NewConfigError(
+			"configure",
+			"format",
+			config.Format,
+			ErrInvalidFormat,
+			"supported formats are 'json' and 'text'",
+		)
 	}
 
 	// Validate and set level
@@ -37,7 +42,13 @@ func NewLogger(config *Config) (*logrus.Logger, error) {
 	} else {
 		level, err := logrus.ParseLevel(config.Level)
 		if err != nil {
-			return nil, errors.NewConfigError("logger.level", err)
+			return nil, NewConfigError(
+				"configure",
+				"level",
+				config.Level,
+				ErrInvalidLevel,
+				err.Error(),
+			)
 		}
 		logger.SetLevel(level)
 	}

+ 182 - 0
pkg/server/errors.go

@@ -0,0 +1,182 @@
+// Package server provides HTTP server functionality
+package server
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+// Common server error types
+var (
+	// ErrInvalidConfig indicates missing or invalid server configuration
+	ErrInvalidConfig = errors.New("invalid server configuration")
+
+	// ErrServerStartFailed indicates failure to start the server
+	ErrServerStartFailed = errors.New("failed to start server")
+
+	// ErrUnauthorizedOrigin indicates a request from an unauthorized origin
+	ErrUnauthorizedOrigin = errors.New("unauthorized origin")
+
+	// ErrRouteSetupFailed indicates failure to set up routes
+	ErrRouteSetupFailed = errors.New("failed to set up routes")
+
+	// ErrHandlerNotFound indicates a missing handler
+	ErrHandlerNotFound = errors.New("handler not found")
+
+	// ErrPanicRecovered indicates a recovered panic
+	ErrPanicRecovered = errors.New("panic recovered")
+
+	// ErrMiddlewareError indicates a middleware error
+	ErrMiddlewareError = errors.New("middleware error")
+
+	// HTTP error types
+	ErrBadRequest         = errors.New("bad request")
+	ErrUnauthorized       = errors.New("unauthorized")
+	ErrForbidden          = errors.New("forbidden")
+	ErrNotFound           = errors.New("not found")
+	ErrMethodNotAllowed   = errors.New("method not allowed")
+	ErrInternalServer     = errors.New("internal server error")
+	ErrServiceUnavailable = errors.New("service unavailable")
+)
+
+// Error represents a server-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "start", "route", "middleware")
+	Op string
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+
+	// StatusCode is the HTTP status code (if applicable)
+	StatusCode int
+
+	// Path is the request path (if applicable)
+	Path string
+
+	// Method is the HTTP method (if applicable)
+	Method string
+
+	// Origin is the request origin (if applicable)
+	Origin string
+
+	// RequestID is the request ID (if applicable)
+	RequestID string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	msg := fmt.Sprintf("server %s failed", e.Op)
+	if e.StatusCode > 0 {
+		msg = fmt.Sprintf("%s with status %d", msg, e.StatusCode)
+	}
+	if e.Method != "" && e.Path != "" {
+		msg = fmt.Sprintf("%s for %s %s", msg, e.Method, e.Path)
+	}
+	if e.Origin != "" {
+		msg = fmt.Sprintf("%s from origin %s", msg, e.Origin)
+	}
+	if e.RequestID != "" {
+		msg = fmt.Sprintf("%s (request ID: %s)", msg, e.RequestID)
+	}
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new server error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewRequestError creates a new server error with request context
+func NewRequestError(op string, method string, path string, statusCode int, err error, details string) *Error {
+	return &Error{
+		Op:         op,
+		Method:     method,
+		Path:       path,
+		StatusCode: statusCode,
+		Err:        err,
+		Details:    details,
+	}
+}
+
+// NewMiddlewareError creates a new middleware error
+func NewMiddlewareError(op string, requestID string, err error, details string) *Error {
+	return &Error{
+		Op:        op,
+		RequestID: requestID,
+		Err:       err,
+		Details:   details,
+	}
+}
+
+// NewCORSError creates a new CORS error
+func NewCORSError(origin string, err error, details string) *Error {
+	return &Error{
+		Op:      "cors",
+		Origin:  origin,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// StatusCode returns the appropriate HTTP status code for the error
+func StatusCode(err error) int {
+	var e *Error
+	if errors.As(err, &e) && e.StatusCode > 0 {
+		return e.StatusCode
+	}
+
+	switch {
+	case errors.Is(err, ErrBadRequest):
+		return http.StatusBadRequest
+	case errors.Is(err, ErrUnauthorized):
+		return http.StatusUnauthorized
+	case errors.Is(err, ErrForbidden):
+		return http.StatusForbidden
+	case errors.Is(err, ErrNotFound):
+		return http.StatusNotFound
+	case errors.Is(err, ErrMethodNotAllowed):
+		return http.StatusMethodNotAllowed
+	case errors.Is(err, ErrUnauthorizedOrigin):
+		return http.StatusForbidden
+	case errors.Is(err, ErrServiceUnavailable):
+		return http.StatusServiceUnavailable
+	default:
+		return http.StatusInternalServerError
+	}
+}
+
+// IsClientError returns true if the error represents a client error (4xx)
+func IsClientError(err error) bool {
+	code := StatusCode(err)
+	return code >= 400 && code < 500
+}
+
+// IsServerError returns true if the error represents a server error (5xx)
+func IsServerError(err error) bool {
+	code := StatusCode(err)
+	return code >= 500
+}

+ 36 - 5
pkg/server/gin.go

@@ -3,7 +3,6 @@ package server
 import (
 	"time"
 
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 	"github.com/gin-gonic/gin"
 	"github.com/sirupsen/logrus"
 )
@@ -13,15 +12,21 @@ type ServerConfig struct {
 	RequestTimeout time.Duration
 }
 
+type ServiceHandler interface {
+	SetupRoutes(r *gin.Engine)
+	Close() error
+}
+
 type Server struct {
-	router *gin.Engine
-	logger *logrus.Logger
-	config ServerConfig
+	router   *gin.Engine
+	logger   *logrus.Logger
+	config   ServerConfig
+	handlers []*ServiceHandler
 }
 
 func NewGinServer(logger *logrus.Logger, config *Config) (*Server, error) {
 	if logger == nil {
-		return nil, errors.NewConfigError("server", errors.ErrInvalidInput)
+		return nil, NewError("initialize", ErrInvalidConfig, "logger is required")
 	}
 
 	// Set Gin mode to release
@@ -50,6 +55,32 @@ func (s *Server) Router() *gin.Engine {
 	return s.router
 }
 
+func (s *Server) Run() error {
+	if err := s.router.Run(); err != nil {
+		return NewError("run", ErrServerStartFailed, err.Error())
+	}
+	return nil
+}
+
+func (s *Server) Close() error {
+	return nil
+}
+
+func (s *Server) SetupRoutes() {
+	if len(s.handlers) == 0 {
+		s.logger.Warn("no handlers registered")
+		return
+	}
+
+	for _, h := range s.handlers {
+		if h == nil {
+			s.logger.Error("nil handler found")
+			continue
+		}
+		(*h).SetupRoutes(s.router)
+	}
+}
+
 // AddHealthCheck adds a basic health check endpoint
 func (s *Server) AddHealthCheck() {
 	s.router.GET("/health", func(c *gin.Context) {

+ 16 - 20
pkg/server/middleware.go

@@ -5,7 +5,6 @@ import (
 	"runtime/debug"
 	"time"
 
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 	"github.com/gin-gonic/gin"
 	"github.com/google/uuid"
 )
@@ -28,6 +27,8 @@ func (s *Server) corsMiddleware() gin.HandlerFunc {
 
 		if !allowed && origin != "" {
 			s.logger.WithField("origin", origin).Warn("blocked request from unauthorized origin")
+			c.AbortWithError(http.StatusForbidden, NewCORSError(origin, ErrUnauthorizedOrigin, "origin not allowed"))
+			return
 		}
 
 		c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
@@ -90,7 +91,7 @@ func (s *Server) recoveryMiddleware() gin.HandlerFunc {
 				}).Error("panic recovered")
 
 				c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
-					"error":      errors.ErrInternalServer.Error(),
+					"error":      ErrInternalServer.Error(),
 					"request_id": requestID,
 				})
 			}
@@ -113,27 +114,22 @@ func (s *Server) errorHandlerMiddleware() gin.HandlerFunc {
 				"request_id": requestID,
 			}).Error("request error")
 
-			// Determine status code based on error type
-			var statusCode int
-			var response gin.H
-
-			switch {
-			case errors.Is(err, errors.ErrNotFound):
-				statusCode = http.StatusNotFound
-			case errors.Is(err, errors.ErrUnauthorized):
-				statusCode = http.StatusUnauthorized
-			case errors.Is(err, errors.ErrInvalidInput):
-				statusCode = http.StatusBadRequest
-			default:
-				statusCode = http.StatusInternalServerError
-			}
-
-			response = gin.H{
-				"error":      err.Error(),
+			// Create a server error with request context
+			serverErr := NewRequestError(
+				"handle_request",
+				c.Request.Method,
+				c.Request.URL.Path,
+				StatusCode(err),
+				err,
+				"request processing failed",
+			)
+
+			response := gin.H{
+				"error":      serverErr.Error(),
 				"request_id": requestID,
 			}
 
-			c.JSON(statusCode, response)
+			c.JSON(serverErr.StatusCode, response)
 		}
 	}
 }

+ 126 - 0
pkg/smtp/errors.go

@@ -0,0 +1,126 @@
+// Package smtp provides email sending functionality
+package smtp
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Common SMTP error types
+var (
+	// ErrInvalidConfig indicates missing or invalid SMTP configuration
+	ErrInvalidConfig = errors.New("invalid SMTP configuration")
+
+	// ErrInvalidRecipient indicates an invalid or empty recipient email
+	ErrInvalidRecipient = errors.New("invalid recipient email")
+
+	// ErrTemplateParsingFailed indicates a failure to parse an email template
+	ErrTemplateParsingFailed = errors.New("failed to parse email template")
+
+	// ErrTemplateExecutionFailed indicates a failure to execute an email template
+	ErrTemplateExecutionFailed = errors.New("failed to execute email template")
+
+	// ErrMessageCreationFailed indicates a failure to create an email message
+	ErrMessageCreationFailed = errors.New("failed to create email message")
+
+	// ErrSendFailed indicates a failure to send an email
+	ErrSendFailed = errors.New("failed to send email")
+
+	// ErrConnectionFailed indicates a failure to connect to the SMTP server
+	ErrConnectionFailed = errors.New("failed to connect to SMTP server")
+
+	// ErrInvalidTemplate indicates missing or invalid template data
+	ErrInvalidTemplate = errors.New("invalid template data")
+
+	// ErrClientClosed indicates the SMTP client is already closed
+	ErrClientClosed = errors.New("SMTP client is closed")
+)
+
+// Error represents an SMTP-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "send", "connect", "template")
+	Op string
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+
+	// Template indicates which email template was involved (if applicable)
+	Template string
+
+	// Recipient indicates which email address was involved (if applicable)
+	Recipient string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	msg := fmt.Sprintf("smtp %s failed", e.Op)
+	if e.Template != "" {
+		msg = fmt.Sprintf("%s for template %s", msg, e.Template)
+	}
+	if e.Recipient != "" {
+		msg = fmt.Sprintf("%s to %s", msg, e.Recipient)
+	}
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new SMTP error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewTemplateError creates a new SMTP template error
+func NewTemplateError(op string, templateName string, err error, details string) *Error {
+	return &Error{
+		Op:       op,
+		Template: templateName,
+		Err:      err,
+		Details:  details,
+	}
+}
+
+// NewSendError creates a new SMTP send error
+func NewSendError(op string, recipient string, err error, details string) *Error {
+	return &Error{
+		Op:        op,
+		Recipient: recipient,
+		Err:       err,
+		Details:   details,
+	}
+}
+
+// IsConnectionError returns true if the error indicates a connection problem
+func IsConnectionError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrConnectionFailed)
+}
+
+// IsTemplateError returns true if the error is related to template processing
+func IsTemplateError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && (errors.Is(e.Err, ErrTemplateParsingFailed) ||
+		errors.Is(e.Err, ErrTemplateExecutionFailed) ||
+		errors.Is(e.Err, ErrInvalidTemplate))
+}

+ 22 - 24
pkg/smtp/service.go

@@ -7,8 +7,6 @@ import (
 
 	"github.com/sirupsen/logrus"
 	"github.com/wneessen/go-mail"
-
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 )
 
 // SMTPConfig holds the configuration for the SMTP service
@@ -48,12 +46,12 @@ type SMTPService struct {
 // NewSMTPService creates a new SMTP service instance
 func NewSMTPService(config SMTPConfig, logger *logrus.Logger) (*SMTPService, error) {
 	if logger == nil {
-		return nil, errors.NewSMTPError("initialization", errors.ErrInvalidInput)
+		return nil, NewError("initialization", ErrInvalidConfig, "logger is required")
 	}
 
 	// Validate config
 	if config.Host == "" || config.Port == 0 || config.Username == "" || config.Password == "" || config.From == "" {
-		return nil, errors.NewSMTPError("initialization", errors.ErrInvalidInput)
+		return nil, NewError("initialization", ErrInvalidConfig, "missing required configuration")
 	}
 
 	// Create mail client with proper configuration
@@ -66,7 +64,7 @@ func NewSMTPService(config SMTPConfig, logger *logrus.Logger) (*SMTPService, err
 	)
 	if err != nil {
 		logger.WithError(err).Error("Failed to create SMTP client")
-		return nil, errors.NewSMTPError("client creation", err)
+		return nil, NewError("initialization", ErrConnectionFailed, err.Error())
 	}
 
 	return &SMTPService{
@@ -79,29 +77,29 @@ func NewSMTPService(config SMTPConfig, logger *logrus.Logger) (*SMTPService, err
 // SendEmail sends a basic email using the default template
 func (s *SMTPService) SendEmail(to string, data EmailData) error {
 	if strings.TrimSpace(to) == "" {
-		return errors.NewSMTPError("send", errors.ErrInvalidInput)
+		return NewSendError("send", to, ErrInvalidRecipient, "empty recipient email")
 	}
 
 	// Parse and execute template
 	tmpl, err := template.New("email").Parse(defaultTemplate)
 	if err != nil {
 		s.logger.WithError(err).Error("Failed to parse email template")
-		return errors.NewSMTPError("template parsing", err)
+		return NewTemplateError("send", "default", ErrTemplateParsingFailed, err.Error())
 	}
 
 	var body bytes.Buffer
 	if err := tmpl.Execute(&body, data); err != nil {
 		s.logger.WithError(err).Error("Failed to execute email template")
-		return errors.NewSMTPError("template execution", err)
+		return NewTemplateError("send", "default", ErrTemplateExecutionFailed, err.Error())
 	}
 
 	// Create and configure message
 	msg := mail.NewMsg()
 	if err := msg.From(s.from); err != nil {
-		return errors.NewSMTPError("message creation", err)
+		return NewError("send", ErrMessageCreationFailed, "failed to set from address")
 	}
 	if err := msg.To(to); err != nil {
-		return errors.NewSMTPError("message creation", err)
+		return NewSendError("send", to, ErrMessageCreationFailed, "failed to set recipient")
 	}
 	msg.Subject(data.Subject)
 	msg.SetBodyString(mail.TypeTextHTML, body.String())
@@ -109,7 +107,7 @@ func (s *SMTPService) SendEmail(to string, data EmailData) error {
 	// Send email
 	if err := s.client.DialAndSend(msg); err != nil {
 		s.logger.WithError(err).Error("Failed to send email")
-		return errors.NewSMTPError("sending", err)
+		return NewSendError("send", to, ErrSendFailed, err.Error())
 	}
 
 	s.logger.WithFields(logrus.Fields{
@@ -123,29 +121,29 @@ func (s *SMTPService) SendEmail(to string, data EmailData) error {
 // SendVerificationEmail sends an email verification message
 func (s *SMTPService) SendVerificationEmail(to string, data VerificationData) error {
 	if strings.TrimSpace(to) == "" || strings.TrimSpace(data.VerificationURL) == "" {
-		return errors.NewSMTPError("send verification", errors.ErrInvalidInput)
+		return NewError("send_verification", ErrInvalidTemplate, "empty recipient or verification URL")
 	}
 
 	// Parse and execute template
 	tmpl, err := template.New("verification").Parse(verificationTemplate)
 	if err != nil {
 		s.logger.WithError(err).Error("Failed to parse verification template")
-		return errors.NewSMTPError("template parsing", err)
+		return NewTemplateError("send_verification", "verification", ErrTemplateParsingFailed, err.Error())
 	}
 
 	var body bytes.Buffer
 	if err := tmpl.Execute(&body, data); err != nil {
 		s.logger.WithError(err).Error("Failed to execute verification template")
-		return errors.NewSMTPError("template execution", err)
+		return NewTemplateError("send_verification", "verification", ErrTemplateExecutionFailed, err.Error())
 	}
 
 	// Create and configure message
 	msg := mail.NewMsg()
 	if err := msg.From(s.from); err != nil {
-		return errors.NewSMTPError("message creation", err)
+		return NewError("send_verification", ErrMessageCreationFailed, "failed to set from address")
 	}
 	if err := msg.To(to); err != nil {
-		return errors.NewSMTPError("message creation", err)
+		return NewSendError("send_verification", to, ErrMessageCreationFailed, "failed to set recipient")
 	}
 	msg.Subject("Verify Your Email Address")
 	msg.SetBodyString(mail.TypeTextHTML, body.String())
@@ -153,7 +151,7 @@ func (s *SMTPService) SendVerificationEmail(to string, data VerificationData) er
 	// Send email
 	if err := s.client.DialAndSend(msg); err != nil {
 		s.logger.WithError(err).Error("Failed to send verification email")
-		return errors.NewSMTPError("sending", err)
+		return NewSendError("send_verification", to, ErrSendFailed, err.Error())
 	}
 
 	s.logger.WithFields(logrus.Fields{
@@ -166,29 +164,29 @@ func (s *SMTPService) SendVerificationEmail(to string, data VerificationData) er
 // SendWelcomeEmail sends a welcome email with credentials
 func (s *SMTPService) SendWelcomeEmail(to string, data WelcomeData) error {
 	if strings.TrimSpace(to) == "" || strings.TrimSpace(data.Username) == "" {
-		return errors.NewSMTPError("send welcome", errors.ErrInvalidInput)
+		return NewError("send_welcome", ErrInvalidTemplate, "empty recipient or username")
 	}
 
 	// Parse and execute template
 	tmpl, err := template.New("welcome").Parse(welcomeTemplate)
 	if err != nil {
 		s.logger.WithError(err).Error("Failed to parse welcome template")
-		return errors.NewSMTPError("template parsing", err)
+		return NewTemplateError("send_welcome", "welcome", ErrTemplateParsingFailed, err.Error())
 	}
 
 	var body bytes.Buffer
 	if err := tmpl.Execute(&body, data); err != nil {
 		s.logger.WithError(err).Error("Failed to execute welcome template")
-		return errors.NewSMTPError("template execution", err)
+		return NewTemplateError("send_welcome", "welcome", ErrTemplateExecutionFailed, err.Error())
 	}
 
 	// Create and configure message
 	msg := mail.NewMsg()
 	if err := msg.From(s.from); err != nil {
-		return errors.NewSMTPError("message creation", err)
+		return NewError("send_welcome", ErrMessageCreationFailed, "failed to set from address")
 	}
 	if err := msg.To(to); err != nil {
-		return errors.NewSMTPError("message creation", err)
+		return NewSendError("send_welcome", to, ErrMessageCreationFailed, "failed to set recipient")
 	}
 	msg.Subject("Welcome to Our Platform!")
 	msg.SetBodyString(mail.TypeTextHTML, body.String())
@@ -196,7 +194,7 @@ func (s *SMTPService) SendWelcomeEmail(to string, data WelcomeData) error {
 	// Send email
 	if err := s.client.DialAndSend(msg); err != nil {
 		s.logger.WithError(err).Error("Failed to send welcome email")
-		return errors.NewSMTPError("sending", err)
+		return NewSendError("send_welcome", to, ErrSendFailed, err.Error())
 	}
 
 	s.logger.WithFields(logrus.Fields{
@@ -212,7 +210,7 @@ func (s *SMTPService) Close() error {
 	if s.client != nil {
 		if err := s.client.Close(); err != nil {
 			s.logger.WithError(err).Error("Failed to close SMTP client")
-			return errors.NewSMTPError("close", err)
+			return NewError("close", ErrClientClosed, err.Error())
 		}
 		s.logger.Info("SMTP client closed successfully")
 	}

+ 129 - 0
pkg/storage/errors.go

@@ -0,0 +1,129 @@
+// Package storage provides object storage functionality
+package storage
+
+import (
+	"errors"
+	"fmt"
+)
+
+// Common storage error types
+var (
+	// ErrInvalidConfig indicates missing or invalid storage configuration
+	ErrInvalidConfig = errors.New("invalid storage configuration")
+
+	// ErrClientInitFailed indicates failure to initialize storage client
+	ErrClientInitFailed = errors.New("failed to initialize storage client")
+
+	// ErrBucketNotFound indicates the specified bucket does not exist
+	ErrBucketNotFound = errors.New("bucket not found")
+
+	// ErrBucketCreationFailed indicates failure to create a new bucket
+	ErrBucketCreationFailed = errors.New("failed to create bucket")
+
+	// ErrObjectNotFound indicates the requested object does not exist
+	ErrObjectNotFound = errors.New("object not found")
+
+	// ErrUploadFailed indicates failure to upload an object
+	ErrUploadFailed = errors.New("failed to upload object")
+
+	// ErrDownloadFailed indicates failure to download an object
+	ErrDownloadFailed = errors.New("failed to download object")
+
+	// ErrInvalidObjectName indicates an invalid object name was provided
+	ErrInvalidObjectName = errors.New("invalid object name")
+
+	// ErrInvalidObjectSize indicates an invalid object size was provided
+	ErrInvalidObjectSize = errors.New("invalid object size")
+
+	// ErrAuthenticationFailed indicates storage authentication failure
+	ErrAuthenticationFailed = errors.New("storage authentication failed")
+)
+
+// Error represents a storage-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "upload", "download", "init")
+	Op string
+
+	// Bucket is the bucket involved in the operation (if applicable)
+	Bucket string
+
+	// Object is the object involved in the operation (if applicable)
+	Object string
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	msg := fmt.Sprintf("storage %s failed", e.Op)
+	if e.Bucket != "" {
+		msg = fmt.Sprintf("%s for bucket %s", msg, e.Bucket)
+	}
+	if e.Object != "" {
+		msg = fmt.Sprintf("%s at object %s", msg, e.Object)
+	}
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new storage error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewBucketError creates a new storage error with bucket context
+func NewBucketError(op string, bucket string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Bucket:  bucket,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewObjectError creates a new storage error with object context
+func NewObjectError(op string, bucket string, object string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Bucket:  bucket,
+		Object:  object,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// IsNotFoundError returns true if the error indicates a not found condition
+func IsNotFoundError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && (errors.Is(e.Err, ErrBucketNotFound) ||
+		errors.Is(e.Err, ErrObjectNotFound))
+}
+
+// IsAuthError returns true if the error indicates an authentication problem
+func IsAuthError(err error) bool {
+	var e *Error
+	return errors.As(err, &e) && errors.Is(e.Err, ErrAuthenticationFailed)
+}

+ 33 - 8
pkg/storage/minio.go

@@ -4,7 +4,6 @@ import (
 	"context"
 	"io"
 
-	"git.linuxforward.com/byom/byom-golang-lib/pkg/errors"
 	"github.com/minio/minio-go/v7"
 	"github.com/minio/minio-go/v7/pkg/credentials"
 )
@@ -16,7 +15,7 @@ type MinioClient struct {
 
 func NewMinioClient(config *Config) (*MinioClient, error) {
 	if config.Endpoint == "" {
-		return nil, errors.NewStorageError("initialize", "", errors.ErrInvalidInput)
+		return nil, NewError("initialize", ErrInvalidConfig, "endpoint is required")
 	}
 
 	client, err := minio.New(config.Endpoint, &minio.Options{
@@ -24,20 +23,20 @@ func NewMinioClient(config *Config) (*MinioClient, error) {
 		Secure: config.UseSSL,
 	})
 	if err != nil {
-		return nil, errors.NewStorageError("create_client", "", err)
+		return nil, NewError("create_client", ErrClientInitFailed, err.Error())
 	}
 
 	// Ensure bucket exists
 	ctx := context.Background()
 	exists, err := client.BucketExists(ctx, config.BucketName)
 	if err != nil {
-		return nil, errors.NewStorageError("check_bucket", config.BucketName, err)
+		return nil, NewBucketError("check_bucket", config.BucketName, err, "failed to check bucket existence")
 	}
 
 	if !exists {
 		err = client.MakeBucket(ctx, config.BucketName, minio.MakeBucketOptions{})
 		if err != nil {
-			return nil, errors.NewStorageError("create_bucket", config.BucketName, err)
+			return nil, NewBucketError("create_bucket", config.BucketName, ErrBucketCreationFailed, err.Error())
 		}
 	}
 
@@ -58,11 +57,37 @@ func (m *MinioClient) BucketName() string {
 }
 
 // PutObject is a helper method to upload an object to the bucket
-func (m *MinioClient) PutObject(ctx context.Context, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (info minio.UploadInfo, err error) {
-	return m.client.PutObject(ctx, m.bucketName, objectName, reader, objectSize, opts)
+func (m *MinioClient) PutObject(ctx context.Context, objectName string, reader io.Reader, objectSize int64, opts minio.PutObjectOptions) (minio.UploadInfo, error) {
+	if objectName == "" {
+		return minio.UploadInfo{}, NewError("put_object", ErrInvalidObjectName, "object name is required")
+	}
+	if objectSize < 0 {
+		return minio.UploadInfo{}, NewError("put_object", ErrInvalidObjectSize, "object size must be non-negative")
+	}
+
+	info, err := m.client.PutObject(ctx, m.bucketName, objectName, reader, objectSize, opts)
+	if err != nil {
+		return minio.UploadInfo{}, NewObjectError("put_object", m.bucketName, objectName, ErrUploadFailed, err.Error())
+	}
+	return info, nil
 }
 
 // GetObject is a helper method to download an object from the bucket
 func (m *MinioClient) GetObject(ctx context.Context, objectName string, opts minio.GetObjectOptions) (*minio.Object, error) {
-	return m.client.GetObject(ctx, m.bucketName, objectName, opts)
+	if objectName == "" {
+		return nil, NewError("get_object", ErrInvalidObjectName, "object name is required")
+	}
+
+	obj, err := m.client.GetObject(ctx, m.bucketName, objectName, opts)
+	if err != nil {
+		return nil, NewObjectError("get_object", m.bucketName, objectName, ErrDownloadFailed, err.Error())
+	}
+
+	// Check if object exists by trying to get its stats
+	_, err = obj.Stat()
+	if err != nil {
+		return nil, NewObjectError("get_object", m.bucketName, objectName, ErrObjectNotFound, "object does not exist")
+	}
+
+	return obj, nil
 }

+ 33 - 0
pkg/webhook/config.go

@@ -0,0 +1,33 @@
+package webhook
+
+import "time"
+
+// Config represents the configuration for a webhook client
+type Config struct {
+	// BaseURL is the base URL for the webhook endpoint
+	BaseURL string `json:"base_url" validate:"required,url"`
+
+	// Domain identifies the source domain sending webhooks
+	Domain string `json:"domain" validate:"required"`
+
+	// SecretKey is used to sign webhook payloads
+	SecretKey string `json:"secret_key" validate:"required"`
+
+	// Timeout is the HTTP client timeout (default: 10s)
+	Timeout time.Duration `json:"timeout"`
+
+	// MaxRetries is the maximum number of retry attempts (default: 3)
+	MaxRetries int `json:"max_retries"`
+
+	// RetryBackoff is the duration to wait between retries (default: 1s)
+	RetryBackoff time.Duration `json:"retry_backoff"`
+}
+
+// DefaultConfig returns a Config with default values
+func DefaultConfig() Config {
+	return Config{
+		Timeout:      10 * time.Second,
+		MaxRetries:   3,
+		RetryBackoff: time.Second,
+	}
+}

+ 107 - 0
pkg/webhook/errors.go

@@ -0,0 +1,107 @@
+// Package webhook provides functionality for sending and validating webhooks
+package webhook
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+)
+
+// Common webhook error types
+var (
+	// ErrInvalidConfig indicates missing or invalid configuration values
+	ErrInvalidConfig = errors.New("invalid webhook configuration")
+
+	// ErrPayloadMarshall indicates a failure to marshal the webhook payload
+	ErrPayloadMarshall = errors.New("failed to marshal webhook payload")
+
+	// ErrRequestCreation indicates a failure to create the HTTP request
+	ErrRequestCreation = errors.New("failed to create webhook request")
+
+	// ErrRequestFailed indicates a permanent failure in the webhook request
+	ErrRequestFailed = errors.New("webhook request failed")
+
+	// ErrTooManyRequests indicates rate limiting by the webhook endpoint
+	ErrTooManyRequests = errors.New("rate limit exceeded")
+
+	// ErrMaxRetriesReached indicates all retry attempts have been exhausted
+	ErrMaxRetriesReached = errors.New("maximum retries reached")
+
+	// ErrInvalidSignature indicates webhook signature validation failed
+	ErrInvalidSignature = errors.New("invalid webhook signature")
+)
+
+// Error represents a webhook-specific error with detailed context
+type Error struct {
+	// Op is the operation that failed (e.g., "send", "validate")
+	Op string
+
+	// Code is the HTTP status code (if applicable)
+	Code int
+
+	// Err is the underlying error
+	Err error
+
+	// Details contains additional error context
+	Details string
+}
+
+// Error returns a string representation of the error
+func (e *Error) Error() string {
+	var msg string
+	if e.Code > 0 {
+		msg = fmt.Sprintf("webhook %s failed with status %d", e.Op, e.Code)
+	} else {
+		msg = fmt.Sprintf("webhook %s failed", e.Op)
+	}
+
+	if e.Details != "" {
+		msg = fmt.Sprintf("%s: %s", msg, e.Details)
+	}
+
+	if e.Err != nil {
+		msg = fmt.Sprintf("%s: %v", msg, e.Err)
+	}
+
+	return msg
+}
+
+// Unwrap returns the underlying error
+func (e *Error) Unwrap() error {
+	return e.Err
+}
+
+// Is reports whether target matches this error
+func (e *Error) Is(target error) bool {
+	return errors.Is(e.Err, target)
+}
+
+// NewError creates a new webhook error
+func NewError(op string, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// NewHTTPError creates a new webhook error with HTTP status code
+func NewHTTPError(op string, code int, err error, details string) *Error {
+	return &Error{
+		Op:      op,
+		Code:    code,
+		Err:     err,
+		Details: details,
+	}
+}
+
+// IsTemporaryError returns true if the error is temporary and the operation can be retried
+func IsTemporaryError(err error) bool {
+	var e *Error
+	if errors.As(err, &e) {
+		return e.Code == http.StatusTooManyRequests ||
+			(e.Code >= 500 && e.Code <= 599) ||
+			e.Is(ErrTooManyRequests)
+	}
+	return false
+}

+ 154 - 0
pkg/webhook/webhook.go

@@ -0,0 +1,154 @@
+package webhook
+
+import (
+	"bytes"
+	"context"
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"time"
+)
+
+// Client handles webhook operations
+type Client struct {
+	config     Config
+	httpClient *http.Client
+}
+
+// Payload represents the webhook payload
+type Payload struct {
+	Email     string    `json:"email"`
+	Domain    string    `json:"domain"`
+	Action    string    `json:"action"`
+	Timestamp time.Time `json:"timestamp"`
+}
+
+// New creates a new webhook client with the provided configuration
+func New(cfg Config) (*Client, error) {
+	if cfg.BaseURL == "" || cfg.Domain == "" || cfg.SecretKey == "" {
+		return nil, ErrInvalidConfig
+	}
+
+	// Apply defaults if not set
+	if cfg.Timeout == 0 {
+		cfg.Timeout = DefaultConfig().Timeout
+	}
+	if cfg.MaxRetries == 0 {
+		cfg.MaxRetries = DefaultConfig().MaxRetries
+	}
+	if cfg.RetryBackoff == 0 {
+		cfg.RetryBackoff = DefaultConfig().RetryBackoff
+	}
+
+	return &Client{
+		config: cfg,
+		httpClient: &http.Client{
+			Timeout: cfg.Timeout,
+		},
+	}, nil
+}
+
+// Send sends a webhook with the provided email and action
+func (c *Client) Send(ctx context.Context, email, action string) error {
+	payload := Payload{
+		Email:     email,
+		Domain:    c.config.Domain,
+		Action:    action,
+		Timestamp: time.Now(),
+	}
+
+	payloadBytes, err := json.Marshal(payload)
+	if err != nil {
+		return NewError("marshal", ErrPayloadMarshall, err.Error())
+	}
+
+	// Calculate signature
+	signature := c.calculateHMAC(payloadBytes)
+
+	// Create request
+	req, err := http.NewRequestWithContext(
+		ctx,
+		http.MethodPost,
+		fmt.Sprintf("%s/webhook/user-sync", c.config.BaseURL),
+		bytes.NewBuffer(payloadBytes),
+	)
+	if err != nil {
+		return NewError("create_request", ErrRequestCreation, err.Error())
+	}
+
+	// Set headers
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("X-Webhook-Signature", signature)
+	req.Header.Set("X-Webhook-Domain", c.config.Domain)
+
+	// Send request with retries
+	var lastErr error
+	for retry := 0; retry < c.config.MaxRetries; retry++ {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+			resp, err := c.httpClient.Do(req)
+			if err != nil {
+				lastErr = err
+				time.Sleep(c.config.RetryBackoff * time.Duration(retry+1))
+				continue
+			}
+			defer resp.Body.Close()
+
+			if resp.StatusCode == http.StatusOK {
+				return nil
+			}
+
+			if resp.StatusCode == http.StatusTooManyRequests {
+				lastErr = ErrTooManyRequests
+				time.Sleep(c.config.RetryBackoff * time.Duration(retry+1))
+				continue
+			}
+
+			// Permanent failure
+			if resp.StatusCode >= 400 && resp.StatusCode != http.StatusTooManyRequests {
+				return NewHTTPError(
+					"send",
+					resp.StatusCode,
+					ErrRequestFailed,
+					"permanent failure",
+				)
+			}
+		}
+	}
+
+	if lastErr != nil {
+		return NewError("send", ErrMaxRetriesReached, lastErr.Error())
+	}
+	return NewError("send", ErrMaxRetriesReached, "all retry attempts failed")
+}
+
+// calculateHMAC calculates the HMAC signature for the payload
+func (c *Client) calculateHMAC(message []byte) string {
+	mac := hmac.New(sha256.New, []byte(c.config.SecretKey))
+	mac.Write(message)
+	return hex.EncodeToString(mac.Sum(nil))
+}
+
+// Close closes the webhook client and its resources
+func (c *Client) Close() error {
+	if c.httpClient != nil {
+		if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
+			transport.CloseIdleConnections()
+		}
+		c.httpClient = nil
+	}
+	return nil
+}
+
+// ValidateSignature validates the HMAC signature of a webhook payload
+func ValidateSignature(payload []byte, signature, secretKey string) bool {
+	mac := hmac.New(sha256.New, []byte(secretKey))
+	mac.Write(payload)
+	expectedMAC := hex.EncodeToString(mac.Sum(nil))
+	return hmac.Equal([]byte(signature), []byte(expectedMAC))
+}