package idear import "time" // Member is a row in the roster: one person, at one role, in one // instance. // // Subject is the join to the app's own identity: idear does not own // email and password, and Subject is the only identifier both the // password and keymail identity plugins produce (see // docs/superpowers/specs/2026-08-23-idear-design.md §4). Email and // Name are a display cache, not the source of truth for who someone // is. // // Removal is DeactivatedAt, never a delete — see Active. type Member struct { ID int64 Subject string `gorm:"uniqueIndex"` Email string `gorm:"index"` Name string Role Role `gorm:"not null;index"` DeactivatedAt *time.Time CreatedAt time.Time UpdatedAt time.Time } // Active reports whether m exists and has not been deactivated. A nil // Member is never active, so callers can pass a lookup's zero value // straight in without a separate nil check. func (m *Member) Active() bool { return m != nil && m.DeactivatedAt == nil } // TableName namespaces the table as idear_members, not GORM's default // members, so it cannot collide with an app's own table of that name. func (Member) TableName() string { return "idear_members" } // Invitation is an offer to join the roster at a given Role, redeemed // by the holder of the plaintext token whose SHA-256 digest is stored // in TokenHash. Only the digest is ever persisted — see token.go — // matching sessions, which holds nothing but token digests either. type Invitation struct { ID int64 Email string `gorm:"index"` Role Role `gorm:"not null"` TokenHash string `gorm:"uniqueIndex"` InvitedBy int64 CreatedAt time.Time ExpiresAt time.Time AcceptedAt *time.Time RevokedAt *time.Time } // Pending reports whether i can still be redeemed as of now: not yet // accepted, not revoked, and not expired. All three must hold — a // single check standing for the other two would let a revoked or // expired invitation read as usable the moment the case that also // disqualifies it stops being exercised. func (i *Invitation) Pending(now time.Time) bool { return i.AcceptedAt == nil && i.RevokedAt == nil && i.ExpiresAt.After(now) } // TableName namespaces the table as idear_invitations, not GORM's // default invitations, so it cannot collide with an app's own table // of that name. func (Invitation) TableName() string { return "idear_invitations" }