package iam import ( "context" "time" "atlas9.dev/c/core" ) type TenantInvitation struct { Tenant core.ID Email string ExpiresAt time.Time } var ( CapTenantInvitationsCreate = NewCap("TenantInvitations.Create") CapTenantInvitationsRead = NewCap("TenantInvitations.Read") CapTenantInvitationsDelete = NewCap("TenantInvitations.Delete") ) type TenantInvitationStore interface { // Create inserts a new invitation and returns the combined "key.code" token // to embed in the email. Returns ErrAlreadyExists on (tenant, email) conflict. Create(ctx context.Context, tenantID core.ID, email string) (token string, err error) // GetByToken validates the token (timing-safe code compare + not expired) // and returns the invitation. Returns core.ErrNotFound on any failure mode // (missing, expired, wrong code) — the undifferentiated error is deliberate // and must not be split apart. GetByToken(ctx context.Context, token string, out *TenantInvitation) error // DeleteByToken removes the invitation whose token matches. Authorized by // valid token possession alone (no cap check). Used by self-service Accept // and Decline flows. DeleteByToken(ctx context.Context, token string) error // List returns pending invitations for a tenant. Owner-facing. List(ctx context.Context, tenantID core.ID, page core.PageReq, out *core.Page[TenantInvitation]) error // ListByEmail returns pending invitations addressed to an email. Allowed // when the principal's email matches the argument, or when the caller has // system access to CapTenantInvitationsRead. ListByEmail(ctx context.Context, email string, page core.PageReq, out *core.Page[TenantInvitation]) error // Delete rescinds an invitation by (tenant, email). Owner-facing. Delete(ctx context.Context, tenantID core.ID, email string) error }