package domain import "fmt" // Error is an API-level failure carrying the contract's error code. // See api-contract.md §5. type Error struct { Code string Message string Details map[string]string cause error } func (e *Error) Error() string { return e.Message } func (e *Error) Unwrap() error { return e.cause } // NotFound reproduces store.js's thrown message verbatim: " not // found", using the frontend's entity name rather than the table name. func NotFound(entity, id string) *Error { return &Error{Code: "not_found", Message: fmt.Sprintf("%s %s not found", entity, id)} } func Invalid(msg string) *Error { return &Error{Code: "invalid_query", Message: msg} } func Validation(msg string, details map[string]string) *Error { if details == nil { details = map[string]string{} } return &Error{Code: "validation_failed", Message: msg, Details: details} } func Conflict(msg string) *Error { return &Error{Code: "conflict", Message: msg} } // Unauthenticated is every "you are not signed in" answer: no cookie, an // unknown token, an expired session, a suspended user, a wrong password, an // email that does not exist. // // One constructor for all of them, deliberately. The distinctions matter in the // server log and must not reach the client: which of those it was tells an // attacker whether an address is registered, whether an account is suspended, // or whether a guessed token was ever real. func Unauthenticated() *Error { return &Error{Code: "unauthorized", Message: "authentication required"} } // Forbidden is the answer to an authenticated caller whose role does not permit // the operation. // // Distinct from Unauthenticated: 401 means "I do not know who you are", 403 // means "I know exactly who you are and the answer is still no". Conflating // them makes a client retry a login that will not help. // // The message names neither the role the caller has nor the roles that would // have worked. That is not secrecy for its own sake — it is that an endpoint // which answers "employers only" to a talent user is an endpoint that maps the // organization's privilege structure for anyone who asks. // // Note what does NOT come through here: a row belonging to another // organization, or to another person, is not forbidden — it is absent. Those // answer 404 by way of a SQL predicate, so existence never leaks. func Forbidden() *Error { return &Error{Code: "forbidden", Message: "you do not have access to this operation"} } // RateLimited is the answer to too many failed sign-in attempts. func RateLimited(msg string) *Error { return &Error{Code: "rate_limited", Message: msg} } func Internal(err error) *Error { return &Error{Code: "internal", Message: "internal error", cause: err} }