You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
561 lines
13 KiB
561 lines
13 KiB
// Package errors provides domain-specific error types for the ORLY relay. |
|
// These typed errors enable structured error handling, machine-readable error codes, |
|
// and proper error categorization throughout the codebase. |
|
package errors |
|
|
|
import ( |
|
"fmt" |
|
) |
|
|
|
// DomainError is the base interface for all domain errors. |
|
// It extends the standard error interface with structured metadata. |
|
type DomainError interface { |
|
error |
|
Code() string // Machine-readable error code (e.g., "INVALID_ID") |
|
Category() string // Error category for grouping (e.g., "validation") |
|
IsRetryable() bool // Whether the operation can be retried |
|
} |
|
|
|
// Base provides common implementation for all domain errors. |
|
type Base struct { |
|
code string |
|
category string |
|
message string |
|
retryable bool |
|
cause error |
|
} |
|
|
|
func (e *Base) Error() string { |
|
if e.cause != nil { |
|
return fmt.Sprintf("%s: %v", e.message, e.cause) |
|
} |
|
return e.message |
|
} |
|
|
|
func (e *Base) Code() string { return e.code } |
|
func (e *Base) Category() string { return e.category } |
|
func (e *Base) IsRetryable() bool { return e.retryable } |
|
func (e *Base) Unwrap() error { return e.cause } |
|
|
|
// WithCause returns a copy of the error with the given cause. |
|
func (e *Base) WithCause(cause error) *Base { |
|
return &Base{ |
|
code: e.code, |
|
category: e.category, |
|
message: e.message, |
|
retryable: e.retryable, |
|
cause: cause, |
|
} |
|
} |
|
|
|
// WithMessage returns a copy of the error with the given message. |
|
func (e *Base) WithMessage(msg string) *Base { |
|
return &Base{ |
|
code: e.code, |
|
category: e.category, |
|
message: msg, |
|
retryable: e.retryable, |
|
cause: e.cause, |
|
} |
|
} |
|
|
|
// ============================================================================= |
|
// Validation Errors |
|
// ============================================================================= |
|
|
|
// ValidationError represents an error in event validation. |
|
type ValidationError struct { |
|
Base |
|
Field string // The field that failed validation |
|
} |
|
|
|
// NewValidationError creates a new validation error. |
|
func NewValidationError(code, field, message string) *ValidationError { |
|
return &ValidationError{ |
|
Base: Base{ |
|
code: code, |
|
category: "validation", |
|
message: message, |
|
}, |
|
Field: field, |
|
} |
|
} |
|
|
|
// WithField returns a copy with the specified field. |
|
func (e *ValidationError) WithField(field string) *ValidationError { |
|
return &ValidationError{ |
|
Base: e.Base, |
|
Field: field, |
|
} |
|
} |
|
|
|
// Validation error constants |
|
var ( |
|
ErrInvalidEventID = NewValidationError( |
|
"INVALID_ID", |
|
"id", |
|
"event ID does not match computed hash", |
|
) |
|
ErrInvalidSignature = NewValidationError( |
|
"INVALID_SIG", |
|
"sig", |
|
"signature verification failed", |
|
) |
|
ErrFutureTimestamp = NewValidationError( |
|
"FUTURE_TS", |
|
"created_at", |
|
"timestamp too far in future", |
|
) |
|
ErrPastTimestamp = NewValidationError( |
|
"PAST_TS", |
|
"created_at", |
|
"timestamp too far in past", |
|
) |
|
ErrUppercaseHex = NewValidationError( |
|
"UPPERCASE_HEX", |
|
"id/pubkey", |
|
"hex values must be lowercase", |
|
) |
|
ErrProtectedTagMismatch = NewValidationError( |
|
"PROTECTED_TAG", |
|
"tags", |
|
"protected event can only be modified by author", |
|
) |
|
ErrInvalidJSON = NewValidationError( |
|
"INVALID_JSON", |
|
"", |
|
"malformed JSON", |
|
) |
|
ErrEventTooLarge = NewValidationError( |
|
"EVENT_TOO_LARGE", |
|
"content", |
|
"event exceeds size limit", |
|
) |
|
ErrInvalidKind = NewValidationError( |
|
"INVALID_KIND", |
|
"kind", |
|
"event kind not allowed", |
|
) |
|
ErrMissingTag = NewValidationError( |
|
"MISSING_TAG", |
|
"tags", |
|
"required tag missing", |
|
) |
|
ErrInvalidTagValue = NewValidationError( |
|
"INVALID_TAG", |
|
"tags", |
|
"tag value validation failed", |
|
) |
|
) |
|
|
|
// ============================================================================= |
|
// Authorization Errors |
|
// ============================================================================= |
|
|
|
// AuthorizationError represents an authorization failure. |
|
type AuthorizationError struct { |
|
Base |
|
Pubkey []byte // The pubkey that was denied |
|
AccessLevel string // The access level that was required |
|
RequireAuth bool // Whether authentication might resolve this |
|
} |
|
|
|
// NeedsAuth returns true if authentication might resolve this error. |
|
func (e *AuthorizationError) NeedsAuth() bool { return e.RequireAuth } |
|
|
|
// NewAuthRequired creates an error indicating authentication is required. |
|
func NewAuthRequired(reason string) *AuthorizationError { |
|
return &AuthorizationError{ |
|
Base: Base{ |
|
code: "AUTH_REQUIRED", |
|
category: "authorization", |
|
message: reason, |
|
}, |
|
RequireAuth: true, |
|
} |
|
} |
|
|
|
// NewAccessDenied creates an error indicating access was denied. |
|
func NewAccessDenied(level, reason string) *AuthorizationError { |
|
return &AuthorizationError{ |
|
Base: Base{ |
|
code: "ACCESS_DENIED", |
|
category: "authorization", |
|
message: reason, |
|
}, |
|
AccessLevel: level, |
|
} |
|
} |
|
|
|
// WithPubkey returns a copy with the specified pubkey. |
|
func (e *AuthorizationError) WithPubkey(pubkey []byte) *AuthorizationError { |
|
return &AuthorizationError{ |
|
Base: e.Base, |
|
Pubkey: pubkey, |
|
AccessLevel: e.AccessLevel, |
|
RequireAuth: e.RequireAuth, |
|
} |
|
} |
|
|
|
// Authorization error constants |
|
var ( |
|
ErrAuthRequired = NewAuthRequired("authentication required") |
|
ErrBanned = &AuthorizationError{ |
|
Base: Base{ |
|
code: "BANNED", |
|
category: "authorization", |
|
message: "pubkey banned", |
|
}, |
|
} |
|
ErrIPBlocked = &AuthorizationError{ |
|
Base: Base{ |
|
code: "IP_BLOCKED", |
|
category: "authorization", |
|
message: "IP address blocked", |
|
}, |
|
} |
|
ErrNotFollowed = &AuthorizationError{ |
|
Base: Base{ |
|
code: "NOT_FOLLOWED", |
|
category: "authorization", |
|
message: "write access requires being followed by admin", |
|
}, |
|
} |
|
ErrNotMember = &AuthorizationError{ |
|
Base: Base{ |
|
code: "NOT_MEMBER", |
|
category: "authorization", |
|
message: "membership required", |
|
}, |
|
} |
|
ErrInsufficientAccess = &AuthorizationError{ |
|
Base: Base{ |
|
code: "INSUFFICIENT_ACCESS", |
|
category: "authorization", |
|
message: "insufficient access level", |
|
}, |
|
} |
|
) |
|
|
|
// ============================================================================= |
|
// Processing Errors |
|
// ============================================================================= |
|
|
|
// ProcessingError represents an error during event processing. |
|
type ProcessingError struct { |
|
Base |
|
EventID []byte // The event ID (if known) |
|
Kind uint16 // The event kind (if known) |
|
} |
|
|
|
// NewProcessingError creates a new processing error. |
|
func NewProcessingError(code string, message string, retryable bool) *ProcessingError { |
|
return &ProcessingError{ |
|
Base: Base{ |
|
code: code, |
|
category: "processing", |
|
message: message, |
|
retryable: retryable, |
|
}, |
|
} |
|
} |
|
|
|
// WithEventID returns a copy with the specified event ID. |
|
func (e *ProcessingError) WithEventID(id []byte) *ProcessingError { |
|
return &ProcessingError{ |
|
Base: e.Base, |
|
EventID: id, |
|
Kind: e.Kind, |
|
} |
|
} |
|
|
|
// WithKind returns a copy with the specified kind. |
|
func (e *ProcessingError) WithKind(kind uint16) *ProcessingError { |
|
return &ProcessingError{ |
|
Base: e.Base, |
|
EventID: e.EventID, |
|
Kind: kind, |
|
} |
|
} |
|
|
|
// Processing error constants |
|
var ( |
|
ErrDuplicate = NewProcessingError( |
|
"DUPLICATE", |
|
"event already exists", |
|
false, |
|
) |
|
ErrReplaceNotAllowed = NewProcessingError( |
|
"REPLACE_DENIED", |
|
"cannot replace event from different author", |
|
false, |
|
) |
|
ErrDeletedEvent = NewProcessingError( |
|
"DELETED", |
|
"event has been deleted", |
|
false, |
|
) |
|
ErrEphemeralNotStored = NewProcessingError( |
|
"EPHEMERAL", |
|
"ephemeral events are not stored", |
|
false, |
|
) |
|
ErrRateLimited = NewProcessingError( |
|
"RATE_LIMITED", |
|
"rate limit exceeded", |
|
true, |
|
) |
|
ErrSprocketRejected = NewProcessingError( |
|
"SPROCKET_REJECTED", |
|
"rejected by sprocket", |
|
false, |
|
) |
|
) |
|
|
|
// ============================================================================= |
|
// Policy Errors |
|
// ============================================================================= |
|
|
|
// PolicyError represents a policy violation. |
|
type PolicyError struct { |
|
Base |
|
RuleName string // The rule that was violated |
|
Action string // The action taken (block, reject) |
|
} |
|
|
|
// NewPolicyBlocked creates an error for a blocked event. |
|
func NewPolicyBlocked(ruleName, reason string) *PolicyError { |
|
return &PolicyError{ |
|
Base: Base{ |
|
code: "POLICY_BLOCKED", |
|
category: "policy", |
|
message: reason, |
|
}, |
|
RuleName: ruleName, |
|
Action: "block", |
|
} |
|
} |
|
|
|
// NewPolicyRejected creates an error for a rejected event. |
|
func NewPolicyRejected(ruleName, reason string) *PolicyError { |
|
return &PolicyError{ |
|
Base: Base{ |
|
code: "POLICY_REJECTED", |
|
category: "policy", |
|
message: reason, |
|
}, |
|
RuleName: ruleName, |
|
Action: "reject", |
|
} |
|
} |
|
|
|
// Policy error constants |
|
var ( |
|
ErrKindBlocked = &PolicyError{ |
|
Base: Base{ |
|
code: "KIND_BLOCKED", |
|
category: "policy", |
|
message: "event kind not allowed by policy", |
|
}, |
|
Action: "block", |
|
} |
|
ErrPubkeyBlocked = &PolicyError{ |
|
Base: Base{ |
|
code: "PUBKEY_BLOCKED", |
|
category: "policy", |
|
message: "pubkey blocked by policy", |
|
}, |
|
Action: "block", |
|
} |
|
ErrContentBlocked = &PolicyError{ |
|
Base: Base{ |
|
code: "CONTENT_BLOCKED", |
|
category: "policy", |
|
message: "content blocked by policy", |
|
}, |
|
Action: "block", |
|
} |
|
ErrScriptRejected = &PolicyError{ |
|
Base: Base{ |
|
code: "SCRIPT_REJECTED", |
|
category: "policy", |
|
message: "rejected by policy script", |
|
}, |
|
Action: "reject", |
|
} |
|
) |
|
|
|
// ============================================================================= |
|
// Storage Errors |
|
// ============================================================================= |
|
|
|
// StorageError represents a storage-layer error. |
|
type StorageError struct { |
|
Base |
|
} |
|
|
|
// NewStorageError creates a new storage error. |
|
func NewStorageError(code, message string, cause error, retryable bool) *StorageError { |
|
return &StorageError{ |
|
Base: Base{ |
|
code: code, |
|
category: "storage", |
|
message: message, |
|
cause: cause, |
|
retryable: retryable, |
|
}, |
|
} |
|
} |
|
|
|
// Storage error constants |
|
var ( |
|
ErrDatabaseUnavailable = NewStorageError( |
|
"DB_UNAVAILABLE", |
|
"database not available", |
|
nil, |
|
true, |
|
) |
|
ErrWriteTimeout = NewStorageError( |
|
"WRITE_TIMEOUT", |
|
"write operation timed out", |
|
nil, |
|
true, |
|
) |
|
ErrReadTimeout = NewStorageError( |
|
"READ_TIMEOUT", |
|
"read operation timed out", |
|
nil, |
|
true, |
|
) |
|
ErrStorageFull = NewStorageError( |
|
"STORAGE_FULL", |
|
"storage capacity exceeded", |
|
nil, |
|
false, |
|
) |
|
ErrCorruptedData = NewStorageError( |
|
"CORRUPTED_DATA", |
|
"data corruption detected", |
|
nil, |
|
false, |
|
) |
|
) |
|
|
|
// ============================================================================= |
|
// Service Errors |
|
// ============================================================================= |
|
|
|
// ServiceError represents a service-level error. |
|
type ServiceError struct { |
|
Base |
|
ServiceName string |
|
} |
|
|
|
// NewServiceError creates a new service error. |
|
func NewServiceError(code, service, message string, retryable bool) *ServiceError { |
|
return &ServiceError{ |
|
Base: Base{ |
|
code: code, |
|
category: "service", |
|
message: message, |
|
retryable: retryable, |
|
}, |
|
ServiceName: service, |
|
} |
|
} |
|
|
|
// Service error constants |
|
var ( |
|
ErrServiceUnavailable = NewServiceError( |
|
"SERVICE_UNAVAILABLE", |
|
"", |
|
"service temporarily unavailable", |
|
true, |
|
) |
|
ErrServiceTimeout = NewServiceError( |
|
"SERVICE_TIMEOUT", |
|
"", |
|
"service request timed out", |
|
true, |
|
) |
|
ErrServiceOverloaded = NewServiceError( |
|
"SERVICE_OVERLOADED", |
|
"", |
|
"service is overloaded", |
|
true, |
|
) |
|
) |
|
|
|
// ============================================================================= |
|
// Helper Functions |
|
// ============================================================================= |
|
|
|
// Is checks if an error matches a target domain error by code. |
|
func Is(err error, target DomainError) bool { |
|
if de, ok := err.(DomainError); ok { |
|
return de.Code() == target.Code() |
|
} |
|
return false |
|
} |
|
|
|
// Code returns the error code if err is a DomainError, empty string otherwise. |
|
func Code(err error) string { |
|
if de, ok := err.(DomainError); ok { |
|
return de.Code() |
|
} |
|
return "" |
|
} |
|
|
|
// Category returns the category of a domain error, or "unknown" for other errors. |
|
func Category(err error) string { |
|
if de, ok := err.(DomainError); ok { |
|
return de.Category() |
|
} |
|
return "unknown" |
|
} |
|
|
|
// IsRetryable checks if an error indicates the operation can be retried. |
|
func IsRetryable(err error) bool { |
|
if de, ok := err.(DomainError); ok { |
|
return de.IsRetryable() |
|
} |
|
return false |
|
} |
|
|
|
// IsValidation checks if an error is a validation error. |
|
func IsValidation(err error) bool { |
|
_, ok := err.(*ValidationError) |
|
return ok |
|
} |
|
|
|
// IsAuthorization checks if an error is an authorization error. |
|
func IsAuthorization(err error) bool { |
|
_, ok := err.(*AuthorizationError) |
|
return ok |
|
} |
|
|
|
// IsProcessing checks if an error is a processing error. |
|
func IsProcessing(err error) bool { |
|
_, ok := err.(*ProcessingError) |
|
return ok |
|
} |
|
|
|
// IsPolicy checks if an error is a policy error. |
|
func IsPolicy(err error) bool { |
|
_, ok := err.(*PolicyError) |
|
return ok |
|
} |
|
|
|
// IsStorage checks if an error is a storage error. |
|
func IsStorage(err error) bool { |
|
_, ok := err.(*StorageError) |
|
return ok |
|
} |
|
|
|
// NeedsAuth checks if an authorization error requires authentication. |
|
func NeedsAuth(err error) bool { |
|
if ae, ok := err.(*AuthorizationError); ok { |
|
return ae.NeedsAuth() |
|
} |
|
return false |
|
}
|
|
|