// SPDX-License-Identifier: MPL-2.0 package auth import ( "context" "errors" "strings" "time" "unicode" "unicode/utf8" ) var ( ErrProfileInput = errors.New("auth: invalid profile change") ErrProfileAccess = errors.New("auth: profile session is unavailable") ErrProfileConflict = errors.New("auth: profile changed; reload before editing") ErrUsernameUnavailable = errors.New("auth: username is unavailable") ) // OwnProfile contains mutable identity, not credentials or organization roles. // Revision is independent of timestamps and increases for every profile edit. type OwnProfile struct { UserID, Username, Email, DisplayName string Revision int64 } // ProfileEdit is a trusted repository command, not an HTTP input model. The // application must authenticate the session, validate CSRF/origin and rate-limit // mutations. Username edits additionally require recent reauthentication (and // any account-specific MFA). For password reauthentication, supply the verified // hash so a concurrent password reset invalidates the write. After verified // passkey approval, leave it empty. Do not log or serialize this command. type ProfileEdit struct { UserID string SessionDigest [32]byte ExpectedRevision int64 Field, Value string ExpectedPasswordHash string } // NormalizeProfileValue validates only supported fields. Email is deliberately // absent: verified mailbox changes need a separate pending/confirmation flow. func NormalizeProfileValue(field, value string) (string, error) { value = strings.TrimSpace(value) switch field { case "username": if !identifierPattern.MatchString(value) { return "", ErrProfileInput } case "display_name": if value == "" || len(value) > 128 || !utf8.ValidString(value) { return "", ErrProfileInput } for _, r := range value { if unicode.IsControl(r) { return "", ErrProfileInput } } default: return "", ErrProfileInput } return value, nil } // OwnProfileRepository is optional; no change to the authentication Repository // interface is required. It derives access from the current session, never from // a site-wide administrator flag. Implementations atomically recheck identity, // session and revision, mutate one field, and append the audit. Username edits // revoke other sessions but preserve the acting session. They never reassign // stable IDs, memberships, passkeys, billing identities or historical records. type OwnProfileRepository interface { OwnProfile(context.Context, [32]byte, time.Time) (OwnProfile, error) UpdateOwnProfile(context.Context, ProfileEdit, AuditEvent) (OwnProfile, error) }