package domain import ( "math" "slices" foundation "github.com/pharos-ai-factory/pharos-platform/services/beto/foundation/domain" ) // PartyAuthenticationSnapshot is a value snapshot for owner persistence mapping. // Issuer and Subject are private, exact authentication identifiers. type PartyAuthenticationSnapshot struct { PartyID string Issuer string Subject string VoPersonID string RetiredBindings []AuthenticationBinding AccessState AccessState IdentityState IdentityState AuthVersion uint64 } type PartyAuthentication struct { state PartyAuthenticationSnapshot events []foundation.DomainEvent } // RegisterParty accepts identity already verified by the application boundary. // The owner transaction supplies an independent canonical ID and enforces unique // issuer/subject ownership. func RegisterParty(partyID, issuer, subject string) (*PartyAuthentication, error) { state := PartyAuthenticationSnapshot{ PartyID: partyID, Issuer: issuer, Subject: subject, AccessState: AccessEnabled, IdentityState: IdentityEnabled, AuthVersion: 1, } if err := state.validate(); err != nil { return nil, err } return &PartyAuthentication{ state: state, events: []foundation.DomainEvent{ PartyRegistered{PartyID: partyID, AuthVersion: 1}, }, }, nil } func RestorePartyAuthentication(state PartyAuthenticationSnapshot) (*PartyAuthentication, error) { if err := state.validate(); err != nil { return nil, foundation.Corrupt(err.Error()) } return &PartyAuthentication{state: state.Copy()}, nil } func (p *PartyAuthentication) Snapshot() PartyAuthenticationSnapshot { return p.state.Copy() } func (p *PartyAuthentication) AuthenticationEligible() bool { return p.state.AuthVersion > 0 && p.state.AccessState == AccessEnabled && p.state.IdentityState == IdentityEnabled } func (p *PartyAuthentication) SetAccessState(state AccessState, reason ReasonCategory) error { if err := p.state.validate(); err != nil { return foundation.Corrupt(err.Error()) } if !validAccess(state) { return foundation.Reject("invalid_access_state", "A recognised platform access state is required") } if !validAccessReason(reason) { return foundation.Reject("invalid_reason_category", "An administrative or security reason is required") } if p.state.AccessState == state { return nil } if p.state.AuthVersion == math.MaxUint64 { return foundation.Reject("auth_version_exhausted", "Authentication generation cannot advance") } p.state.AccessState = state p.state.AuthVersion++ p.events = append(p.events, PartyAuthenticationChanged{ PartyID: p.state.PartyID, AuthVersion: p.state.AuthVersion, AccessState: state, AuthenticationEligible: p.AuthenticationEligible(), ReasonCategory: reason, }) return nil } func (p *PartyAuthentication) Events() []foundation.DomainEvent { return slices.Clone(p.events) } func (p *PartyAuthentication) DrainEvents() []foundation.DomainEvent { events := p.Events() p.events = nil return events } func (s PartyAuthenticationSnapshot) validate() error { if err := foundation.ValidateID(s.PartyID); err != nil { return err } if !validOpaque(s.Issuer) || !validOpaque(s.Subject) { return foundation.Reject("invalid_authentication_binding", "An exact issuer and subject are required") } if s.VoPersonID != "" && !validPersonID(s.VoPersonID) { return foundation.Reject("invalid_person_identity", "A valid opaque person identity is required") } if err := s.validateRetiredBindings(); err != nil { return err } if !validAccess(s.AccessState) { return foundation.Reject("invalid_access_state", "A recognised platform access state is required") } if s.IdentityState != IdentityEnabled { return foundation.Reject("invalid_identity_state", "An enabled verified identity is required") } if s.AuthVersion == 0 { return foundation.Reject("invalid_auth_version", "Authentication generation must be positive") } return nil }