package auth import ( "context" "fmt" "strings" "github.com/jackc/pgx/v5/pgxpool" ) // InstitutionalTier represents a DBIS institutional membership tier. // These are the canonical tiers from d-bis.org/members#tiers. type InstitutionalTier string const ( TierSovereignCentralBank InstitutionalTier = "sovereign_central_bank" TierGlobalFamilyOffice InstitutionalTier = "global_family_office" TierSettlementMember InstitutionalTier = "settlement_member" TierInfrastructureOp InstitutionalTier = "infrastructure_operator" TierOversightJudicial InstitutionalTier = "oversight_judicial" TierDelegatedAuthority InstitutionalTier = "delegated_authority" TierStandardsBody InstitutionalTier = "standards_body" ) // InstitutionalTierLabel returns the human-readable label for a tier. func InstitutionalTierLabel(t InstitutionalTier) string { switch t { case TierSovereignCentralBank: return "Sovereign Central Bank" case TierGlobalFamilyOffice: return "Global Family Office" case TierSettlementMember: return "Settlement Member" case TierInfrastructureOp: return "Infrastructure Operator" case TierOversightJudicial: return "Oversight & Judicial" case TierDelegatedAuthority: return "Delegated Authority" case TierStandardsBody: return "Standards Body" default: return string(t) } } // DefaultTrackForTier maps an institutional membership tier to the default // explorer access track. Higher tracks inherit all lower-track permissions. // // Track 1 — Public explorer (read blocks, txs, basic address) // Track 2 — Enhanced explorer (full address, tokens, tx history, search) // Track 3 — Analytics (flows, bridge analytics, risk, distribution) // Track 4 — Operator (bridge control, validators, protocol config) func DefaultTrackForTier(tier InstitutionalTier) int { switch tier { case TierSovereignCentralBank: return 3 // analytics access; operator granted per-address case TierGlobalFamilyOffice: return 3 case TierSettlementMember: return 2 case TierInfrastructureOp: return 4 case TierOversightJudicial: return 3 case TierDelegatedAuthority: return 3 case TierStandardsBody: return 2 default: return 1 } } // InstitutionalMember represents an entity in the DBIS member directory. type InstitutionalMember struct { ID int `json:"id"` Slug string `json:"slug"` Abbreviation string `json:"abbreviation"` Name string `json:"name"` Tier InstitutionalTier `json:"tier"` Description string `json:"description"` Jurisdiction string `json:"jurisdiction,omitempty"` LEI string `json:"lei,omitempty"` Latitude float64 `json:"latitude,omitempty"` Longitude float64 `json:"longitude,omitempty"` MapLabel string `json:"map_label,omitempty"` Active bool `json:"active"` } // MembershipStore provides read/write access to the institutional members table. type MembershipStore struct { db *pgxpool.Pool } // NewMembershipStore creates a new MembershipStore. func NewMembershipStore(db *pgxpool.Pool) *MembershipStore { return &MembershipStore{db: db} } func isMissingMembershipTableError(err error) bool { return err != nil && strings.Contains(err.Error(), `relation "institutional_members" does not exist`) } // ListMembers returns all active institutional members. func (s *MembershipStore) ListMembers(ctx context.Context) ([]InstitutionalMember, error) { rows, err := s.db.Query(ctx, ` SELECT id, slug, abbreviation, name, tier, description, COALESCE(jurisdiction, ''), COALESCE(lei, ''), COALESCE(latitude, 0), COALESCE(longitude, 0), COALESCE(map_label, ''), active FROM institutional_members WHERE active = TRUE ORDER BY tier, name `) if err != nil { if isMissingMembershipTableError(err) { return nil, nil } return nil, fmt.Errorf("list members: %w", err) } defer rows.Close() var members []InstitutionalMember for rows.Next() { var m InstitutionalMember if err := rows.Scan( &m.ID, &m.Slug, &m.Abbreviation, &m.Name, &m.Tier, &m.Description, &m.Jurisdiction, &m.LEI, &m.Latitude, &m.Longitude, &m.MapLabel, &m.Active, ); err != nil { return nil, fmt.Errorf("scan member: %w", err) } members = append(members, m) } return members, rows.Err() } // GetMemberBySlug returns a single member by URL slug. func (s *MembershipStore) GetMemberBySlug(ctx context.Context, slug string) (*InstitutionalMember, error) { var m InstitutionalMember err := s.db.QueryRow(ctx, ` SELECT id, slug, abbreviation, name, tier, description, COALESCE(jurisdiction, ''), COALESCE(lei, ''), COALESCE(latitude, 0), COALESCE(longitude, 0), COALESCE(map_label, ''), active FROM institutional_members WHERE slug = $1 `, slug).Scan( &m.ID, &m.Slug, &m.Abbreviation, &m.Name, &m.Tier, &m.Description, &m.Jurisdiction, &m.LEI, &m.Latitude, &m.Longitude, &m.MapLabel, &m.Active, ) if err != nil { if isMissingMembershipTableError(err) { return nil, nil } return nil, fmt.Errorf("get member by slug: %w", err) } return &m, nil } // GetMemberByAddress looks up the institutional member linked to a wallet // address via the institutional_member_wallets junction table. func (s *MembershipStore) GetMemberByAddress(ctx context.Context, address string) (*InstitutionalMember, error) { var m InstitutionalMember err := s.db.QueryRow(ctx, ` SELECT m.id, m.slug, m.abbreviation, m.name, m.tier, m.description, COALESCE(m.jurisdiction, ''), COALESCE(m.lei, ''), COALESCE(m.latitude, 0), COALESCE(m.longitude, 0), COALESCE(m.map_label, ''), m.active FROM institutional_members m JOIN institutional_member_wallets w ON w.member_id = m.id WHERE LOWER(w.address) = LOWER($1) AND w.active = TRUE AND m.active = TRUE `, address).Scan( &m.ID, &m.Slug, &m.Abbreviation, &m.Name, &m.Tier, &m.Description, &m.Jurisdiction, &m.LEI, &m.Latitude, &m.Longitude, &m.MapLabel, &m.Active, ) if err != nil { if isMissingMembershipTableError(err) || strings.Contains(err.Error(), "no rows") { return nil, nil } return nil, fmt.Errorf("get member by address: %w", err) } return &m, nil } // ListTiers returns the canonical set of institutional membership tiers // with their labels and default access tracks. func ListTiers() []struct { Tier InstitutionalTier `json:"tier"` Label string `json:"label"` DefaultTrack int `json:"default_track"` } { tiers := []InstitutionalTier{ TierSovereignCentralBank, TierGlobalFamilyOffice, TierSettlementMember, TierInfrastructureOp, TierOversightJudicial, TierDelegatedAuthority, TierStandardsBody, } result := make([]struct { Tier InstitutionalTier `json:"tier"` Label string `json:"label"` DefaultTrack int `json:"default_track"` }, len(tiers)) for i, t := range tiers { result[i].Tier = t result[i].Label = InstitutionalTierLabel(t) result[i].DefaultTrack = DefaultTrackForTier(t) } return result }