From 55a209646a42438fefdd0856364be5fc0d7c1d24 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 20:32:06 +0000 Subject: [PATCH 1/2] feat: add institutional membership tiers and correct member directory Corrections per 2026-04 institutional review: - MLFO reclassified as Global Family Office (was incorrectly labeled central bank) - BIS Innovation Hub reclassified as Standards Body (does not hold observer seat) - Added missing entities: ICCC, SAID, PANDA, Order of Hospitallers (XOM) - Added BRICS founding + expanded member central banks (10 entries) New institutional tier taxonomy (7 tiers): sovereign_central_bank, global_family_office, settlement_member, infrastructure_operator, oversight_judicial, delegated_authority, standards_body Backend changes: - New auth/membership.go: tier types, DefaultTrackForTier mapping, MembershipStore with DB queries for member directory - New migration 0017: institutional_members + institutional_member_wallets tables with seed data for all corrected members - Updated wallet_auth.go getUserTrack(): now resolves institutional membership (via wallet junction table) before defaulting to Track 1 - WalletAuthResponse now includes institutional_tier and institution_name - New REST endpoints: GET /api/v1/membership/{tiers,members,members/:slug} - Added TrackLabel() helper in featureflags Frontend changes: - Added InstitutionalTier type and label map to access.ts - WalletAccessSession extended with institutionalTier/institutionName - Navbar getAccessTier() now displays institutional tier label when present - Session summary shows institution name Co-Authored-By: Nakamoto, S --- backend/api/rest/membership.go | 89 ++++++++ backend/api/rest/routes.go | 5 + backend/auth/membership.go | 213 ++++++++++++++++++ backend/auth/wallet_auth.go | 48 +++- .../0017_institutional_membership.down.sql | 5 + .../0017_institutional_membership.up.sql | 184 +++++++++++++++ backend/featureflags/flags.go | 16 ++ frontend/src/components/common/Navbar.tsx | 10 +- frontend/src/services/api/access.ts | 25 ++ 9 files changed, 581 insertions(+), 14 deletions(-) create mode 100644 backend/api/rest/membership.go create mode 100644 backend/auth/membership.go create mode 100644 backend/database/migrations/0017_institutional_membership.down.sql create mode 100644 backend/database/migrations/0017_institutional_membership.up.sql diff --git a/backend/api/rest/membership.go b/backend/api/rest/membership.go new file mode 100644 index 0000000..2566d94 --- /dev/null +++ b/backend/api/rest/membership.go @@ -0,0 +1,89 @@ +package rest + +import ( + "encoding/json" + "net/http" + "strings" + + "github.com/explorer/backend/auth" +) + +// handleMembershipTiers returns the canonical set of institutional tiers +// with their labels and default explorer access tracks. +// GET /api/v1/membership/tiers +func (s *Server) handleMembershipTiers(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + tiers := auth.ListTiers() + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "tiers": tiers, + }) +} + +// handleMembershipMembers returns all active institutional members. +// GET /api/v1/membership/members +func (s *Server) handleMembershipMembers(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + if !s.requireDB(w) { + return + } + + store := auth.NewMembershipStore(s.db) + members, err := store.ListMembers(r.Context()) + if err != nil { + writeInternalError(w, err.Error()) + return + } + if members == nil { + members = []auth.InstitutionalMember{} + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "members": members, + "total": len(members), + }) +} + +// handleMembershipMemberDetail returns a single member by slug. +// GET /api/v1/membership/members/{slug} +func (s *Server) handleMembershipMemberDetail(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeMethodNotAllowed(w) + return + } + + if !s.requireDB(w) { + return + } + + slug := strings.TrimPrefix(r.URL.Path, "/api/v1/membership/members/") + if slug == "" { + writeError(w, http.StatusBadRequest, "MISSING_SLUG", "Member slug is required") + return + } + + store := auth.NewMembershipStore(s.db) + member, err := store.GetMemberBySlug(r.Context(), slug) + if err != nil { + writeInternalError(w, err.Error()) + return + } + if member == nil { + writeNotFound(w, "Member") + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]interface{}{ + "member": member, + }) +} diff --git a/backend/api/rest/routes.go b/backend/api/rest/routes.go index 68a25ab..8fac635 100644 --- a/backend/api/rest/routes.go +++ b/backend/api/rest/routes.go @@ -67,6 +67,11 @@ func (s *Server) SetupRoutes(mux *http.ServeMux) { mux.HandleFunc("/api/v1/access/usage", s.handleAccessUsage) mux.HandleFunc("/api/v1/access/audit", s.handleAccessAudit) + // Institutional membership directory (public, read-only) + mux.HandleFunc("/api/v1/membership/tiers", s.handleMembershipTiers) + mux.HandleFunc("/api/v1/membership/members", s.handleMembershipMembers) + mux.HandleFunc("/api/v1/membership/members/", s.handleMembershipMemberDetail) + // Track 1 routes (public, optional auth) // Note: Track 1 endpoints should be registered with OptionalAuth middleware // mux.HandleFunc("/api/v1/track1/blocks/latest", s.track1Server.handleLatestBlocks) diff --git a/backend/auth/membership.go b/backend/auth/membership.go new file mode 100644 index 0000000..a46f5be --- /dev/null +++ b/backend/auth/membership.go @@ -0,0 +1,213 @@ +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 +} diff --git a/backend/auth/wallet_auth.go b/backend/auth/wallet_auth.go index b6b6519..15048c1 100644 --- a/backend/auth/wallet_auth.go +++ b/backend/auth/wallet_auth.go @@ -102,10 +102,12 @@ type WalletAuthRequest struct { // WalletAuthResponse represents a wallet authentication response type WalletAuthResponse struct { - Token string `json:"token"` - ExpiresAt time.Time `json:"expires_at"` - Track int `json:"track"` - Permissions []string `json:"permissions"` + Token string `json:"token"` + ExpiresAt time.Time `json:"expires_at"` + Track int `json:"track"` + Permissions []string `json:"permissions"` + InstitutionalTier *InstitutionalTier `json:"institutional_tier,omitempty"` + InstitutionName string `json:"institution_name,omitempty"` } // GenerateNonce generates a random nonce for wallet authentication @@ -223,17 +225,30 @@ func (w *WalletAuth) AuthenticateWallet(ctx context.Context, req *WalletAuthRequ // Get permissions for track permissions := getPermissionsForTrack(track) - return &WalletAuthResponse{ + resp := &WalletAuthResponse{ Token: token, ExpiresAt: expiresAt, Track: track, Permissions: permissions, - }, nil + } + + // Attach institutional membership info if present + store := NewMembershipStore(w.db) + if member, err := store.GetMemberByAddress(ctx, normalizedAddr); err == nil && member != nil { + resp.InstitutionalTier = &member.Tier + resp.InstitutionName = member.Name + } + + return resp, nil } -// getUserTrack gets the track level for a user address +// getUserTrack gets the track level for a user address. +// Resolution order: +// 1. Explicit per-address assignment in operator_roles (highest priority). +// 2. Institutional membership via institutional_member_wallets → tier default. +// 3. Fallback to Track 1 (public). func (w *WalletAuth) getUserTrack(ctx context.Context, address string) (int, error) { - // Check if user exists in operator_roles (Track 4) + // 1. Check explicit per-address assignment in operator_roles var track int var approved bool query := `SELECT track_level, approved FROM operator_roles WHERE address = $1` @@ -242,9 +257,20 @@ func (w *WalletAuth) getUserTrack(ctx context.Context, address string) (int, err return track, nil } - // Check if user is approved for Track 2 or 3 - // For now, default to Track 1 (public) - // In production, you'd have an approval table + // 2. Check institutional membership + var tier string + memberQuery := ` + SELECT m.tier + 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 + ` + err = w.db.QueryRow(ctx, memberQuery, address).Scan(&tier) + if err == nil { + return DefaultTrackForTier(InstitutionalTier(tier)), nil + } + + // 3. Default to Track 1 (public) return 1, nil } diff --git a/backend/database/migrations/0017_institutional_membership.down.sql b/backend/database/migrations/0017_institutional_membership.down.sql new file mode 100644 index 0000000..d4e72e7 --- /dev/null +++ b/backend/database/migrations/0017_institutional_membership.down.sql @@ -0,0 +1,5 @@ +-- 0017_institutional_membership.down.sql +DROP TRIGGER IF EXISTS update_institutional_members_updated_at ON institutional_members; +DROP TABLE IF EXISTS institutional_member_wallets; +DROP TABLE IF EXISTS institutional_members; +DROP TYPE IF EXISTS institutional_tier; diff --git a/backend/database/migrations/0017_institutional_membership.up.sql b/backend/database/migrations/0017_institutional_membership.up.sql new file mode 100644 index 0000000..efcf32d --- /dev/null +++ b/backend/database/migrations/0017_institutional_membership.up.sql @@ -0,0 +1,184 @@ +-- 0017_institutional_membership.up.sql +-- +-- Adds institutional membership tables and seeds the canonical DBIS member +-- directory. The tier taxonomy comes from https://d-bis.org/members#tiers +-- with corrections per institutional review (2026-04). + +-- Tier enum +DO $$ BEGIN + CREATE TYPE institutional_tier AS ENUM ( + 'sovereign_central_bank', + 'global_family_office', + 'settlement_member', + 'infrastructure_operator', + 'oversight_judicial', + 'delegated_authority', + 'standards_body' + ); +EXCEPTION WHEN duplicate_object THEN NULL; +END $$; + +-- Members directory +CREATE TABLE IF NOT EXISTS institutional_members ( + id SERIAL PRIMARY KEY, + slug VARCHAR(64) NOT NULL UNIQUE, + abbreviation VARCHAR(16) NOT NULL, + name TEXT NOT NULL, + tier institutional_tier NOT NULL, + description TEXT NOT NULL DEFAULT '', + jurisdiction TEXT, + lei VARCHAR(20), + latitude DOUBLE PRECISION, + longitude DOUBLE PRECISION, + map_label TEXT, + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_institutional_members_tier ON institutional_members(tier); +CREATE INDEX IF NOT EXISTS idx_institutional_members_active ON institutional_members(active); + +-- Junction: wallet addresses linked to institutional members +CREATE TABLE IF NOT EXISTS institutional_member_wallets ( + id SERIAL PRIMARY KEY, + member_id INTEGER NOT NULL REFERENCES institutional_members(id) ON DELETE CASCADE, + address VARCHAR(42) NOT NULL, + label TEXT, + active BOOLEAN NOT NULL DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), + UNIQUE(member_id, address) +); + +CREATE INDEX IF NOT EXISTS idx_imw_address ON institutional_member_wallets(address); +CREATE INDEX IF NOT EXISTS idx_imw_member_id ON institutional_member_wallets(member_id); + +-- Triggers +CREATE TRIGGER update_institutional_members_updated_at + BEFORE UPDATE ON institutional_members + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- ============================================================ +-- Seed data — canonical member directory +-- ============================================================ +-- Corrections from 2026-04 review: +-- • MLFO is a Global Family Office, NOT a central bank +-- • BIS Innovation Hub does NOT hold observer or any voting seat +-- • Added: ICCC, SAID, PANDA, Order of Hospitallers (XOM) +-- • Placeholder rows for BRICS founding central banks + +INSERT INTO institutional_members + (slug, abbreviation, name, tier, description, jurisdiction, lei, latitude, longitude, map_label) +VALUES + -- Existing (corrected) + ('omnl', 'OMNL', 'Organisation Mondiale du Numérique', + 'sovereign_central_bank', + 'Participating central bank — OMNL Head Office ledger and journal operator (Fineract / OMNL tenant). ARIN OrgId: OMNL.', + 'International / participating monetary union', '98450070C57395F6B906', + 39.61, -104.89, 'Greenwood Village, CO'), + + ('mlfo', 'MLFO', 'Mann Li Family Office', + 'global_family_office', + 'Founding family office (L.P.B.C., Colorado Entity 20241969162). Capital structure sponsor and BIS debit performance beneficiary. Registered agent: Pandora C. Walker.', + 'US-CO (Colorado)', NULL, + 40.02, -105.27, 'Boulder, CO'), + + ('defi-oracle', 'DFO', 'DeFi Oracle', + 'infrastructure_operator', + 'Infrastructure operator for Chain 138 ecosystem. Manages smart contract deployment (131 contracts), cross-chain bridges, PMM pools, and wallet integrations (MetaMask Snap, Ledger Live).', + 'US-CO (Colorado)', NULL, + 39.61, -104.89, 'Greenwood Village, CO'), + + ('bis-innovation-hub', 'BISIH', 'BIS Innovation Hub', + 'standards_body', + 'The BIS Innovation Hub is a publicly documented BIS entity whose mandate is to develop public goods in the technology space. DBIS is presented as the output of this Innovation Hub mandate. Does not hold an observer seat or voting rights.', + 'International (Basel, Switzerland)', NULL, + 47.55, 7.59, 'Basel, Switzerland'), + + -- Added entities + ('iccc', 'ICCC', 'International Criminal Court of Commerce', + 'oversight_judicial', + 'International court with oversight authority over DBIS ecosystem commercial disputes and enforcement.', + 'International', NULL, + NULL, NULL, NULL), + + ('said', 'SAID', 'SAID', + 'standards_body', + 'Standards and identity body within the DBIS institutional framework.', + 'International', NULL, + NULL, NULL, NULL), + + ('panda', 'PANDA', 'PANDA', + 'standards_body', + 'Standards and coordination body within the DBIS institutional framework.', + 'International', NULL, + NULL, NULL, NULL), + + ('xom', 'XOM', 'Sovereign Military Hospitaller Order of St. John of Jerusalem of Rhodes and of Malta', + 'delegated_authority', + 'The sovereign entity (Order of Hospitallers) extending DBIS the agency authority under which it operates. Recognised UN observer state.', + 'International (Rome)', NULL, + 41.90, 12.48, 'Rome, Italy'), + + -- BRICS founding member central banks (representative set) + ('cb-brazil', 'BCB', 'Banco Central do Brasil', + 'sovereign_central_bank', + 'Central Bank of Brazil — BRICS founding member.', + 'Brazil', NULL, + -15.79, -47.88, 'Brasília, Brazil'), + + ('cb-russia', 'CBR', 'Central Bank of the Russian Federation', + 'sovereign_central_bank', + 'Bank of Russia — BRICS founding member.', + 'Russia', NULL, + 55.76, 37.62, 'Moscow, Russia'), + + ('cb-india', 'RBI', 'Reserve Bank of India', + 'sovereign_central_bank', + 'Reserve Bank of India — BRICS founding member.', + 'India', NULL, + 18.93, 72.83, 'Mumbai, India'), + + ('cb-china', 'PBOC', 'People''s Bank of China', + 'sovereign_central_bank', + 'People''s Bank of China — BRICS founding member.', + 'China', NULL, + 39.91, 116.39, 'Beijing, China'), + + ('cb-south-africa', 'SARB', 'South African Reserve Bank', + 'sovereign_central_bank', + 'South African Reserve Bank — BRICS founding member.', + 'South Africa', NULL, + -25.75, 28.19, 'Pretoria, South Africa'), + + -- BRICS expanded members (2024+) + ('cb-egypt', 'CBE', 'Central Bank of Egypt', + 'sovereign_central_bank', + 'Central Bank of Egypt — BRICS member (2024).', + 'Egypt', NULL, + 30.04, 31.24, 'Cairo, Egypt'), + + ('cb-ethiopia', 'NBE', 'National Bank of Ethiopia', + 'sovereign_central_bank', + 'National Bank of Ethiopia — BRICS member (2024).', + 'Ethiopia', NULL, + 9.02, 38.75, 'Addis Ababa, Ethiopia'), + + ('cb-iran', 'CBI', 'Central Bank of the Islamic Republic of Iran', + 'sovereign_central_bank', + 'Central Bank of Iran — BRICS member (2024).', + 'Iran', NULL, + 35.70, 51.42, 'Tehran, Iran'), + + ('cb-uae', 'CBUAE', 'Central Bank of the UAE', + 'sovereign_central_bank', + 'Central Bank of the UAE — BRICS member (2024).', + 'United Arab Emirates', NULL, + 24.45, 54.65, 'Abu Dhabi, UAE'), + + ('cb-saudi-arabia', 'SAMA', 'Saudi Central Bank', + 'sovereign_central_bank', + 'Saudi Central Bank (formerly SAMA) — BRICS member (2024).', + 'Saudi Arabia', NULL, + 24.71, 46.68, 'Riyadh, Saudi Arabia') +ON CONFLICT (slug) DO NOTHING; diff --git a/backend/featureflags/flags.go b/backend/featureflags/flags.go index 7381863..6b04308 100644 --- a/backend/featureflags/flags.go +++ b/backend/featureflags/flags.go @@ -118,3 +118,19 @@ func GetAllFeatures() map[string]FeatureFlag { return FeatureFlags } +// TrackLabel returns a human-readable label for an access track number. +func TrackLabel(track int) string { + switch track { + case 1: + return "Explorer" + case 2: + return "Enhanced Explorer" + case 3: + return "Analytics" + case 4: + return "Operator" + default: + return "Unknown" + } +} + diff --git a/frontend/src/components/common/Navbar.tsx b/frontend/src/components/common/Navbar.tsx index 67f84df..5b1eb2e 100644 --- a/frontend/src/components/common/Navbar.tsx +++ b/frontend/src/components/common/Navbar.tsx @@ -3,7 +3,7 @@ import Link from 'next/link' import { usePathname, useRouter } from 'next/navigation' import { type ReactNode, useEffect, useId, useMemo, useRef, useState } from 'react' -import { accessApi, type WalletAccessSession } from '@/services/api/access' +import { accessApi, institutionalTierLabels, type WalletAccessSession } from '@/services/api/access' import BrandLockup from './BrandLockup' import HeaderCommandPalette, { type HeaderCommandItem } from './HeaderCommandPalette' import { useUiMode } from './UiModeContext' @@ -310,6 +310,9 @@ function SearchControl({ } function getAccessTier(walletSession: WalletAccessSession) { + if (walletSession.institutionalTier) { + return institutionalTierLabels[walletSession.institutionalTier] ?? walletSession.institutionalTier + } const permissions = walletSession.permissions || [] if (permissions.some((permission) => permission.startsWith('operator.'))) { return 'Operator Tier' @@ -326,10 +329,11 @@ function getAccessTier(walletSession: WalletAccessSession) { function getSessionSummary(walletSession: WalletAccessSession) { const permissionCount = walletSession.permissions?.length || 0 const tierLabel = getAccessTier(walletSession) + const institutionSuffix = walletSession.institutionName ? ` (${walletSession.institutionName})` : '' if (permissionCount > 0) { - return `${tierLabel} · ${permissionCount} permission${permissionCount === 1 ? '' : 's'}` + return `${tierLabel}${institutionSuffix} · ${permissionCount} permission${permissionCount === 1 ? '' : 's'}` } - return `${tierLabel} · Explorer access active` + return `${tierLabel}${institutionSuffix} · Explorer access active` } function UiModeToggle({ mobile = false }: { mobile?: boolean }) { diff --git a/frontend/src/services/api/access.ts b/frontend/src/services/api/access.ts index 97dcf95..1f1093d 100644 --- a/frontend/src/services/api/access.ts +++ b/frontend/src/services/api/access.ts @@ -21,12 +21,33 @@ export interface AccessSession { expires_at: string } +export type InstitutionalTier = + | 'sovereign_central_bank' + | 'global_family_office' + | 'settlement_member' + | 'infrastructure_operator' + | 'oversight_judicial' + | 'delegated_authority' + | 'standards_body' + +export const institutionalTierLabels: Record = { + sovereign_central_bank: 'Sovereign Central Bank', + global_family_office: 'Global Family Office', + settlement_member: 'Settlement Member', + infrastructure_operator: 'Infrastructure Operator', + oversight_judicial: 'Oversight & Judicial', + delegated_authority: 'Delegated Authority', + standards_body: 'Standards Body', +} + export interface WalletAccessSession { token: string expiresAt: string track: string permissions: string[] address: string + institutionalTier?: InstitutionalTier + institutionName?: string } export interface AccessProduct { @@ -220,6 +241,8 @@ export const accessApi = { expires_at: string track: string permissions: string[] + institutional_tier?: InstitutionalTier + institution_name?: string }>(`${ACCESS_API_PREFIX}/auth/wallet`, { method: 'POST', body: JSON.stringify({ address, signature, nonce }), @@ -230,6 +253,8 @@ export const accessApi = { track: response.track, permissions: response.permissions || [], address, + institutionalTier: response.institutional_tier, + institutionName: response.institution_name, } setStoredWalletSession(session) return session -- 2.34.1 From 9e17ed8ceb48fed8eeee201c086687cf07371006 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 20:48:21 +0000 Subject: [PATCH 2/2] fix: remove BIS Innovation Hub from member directory Co-Authored-By: Nakamoto, S --- .../migrations/0017_institutional_membership.up.sql | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/backend/database/migrations/0017_institutional_membership.up.sql b/backend/database/migrations/0017_institutional_membership.up.sql index efcf32d..4fc9dda 100644 --- a/backend/database/migrations/0017_institutional_membership.up.sql +++ b/backend/database/migrations/0017_institutional_membership.up.sql @@ -63,7 +63,7 @@ CREATE TRIGGER update_institutional_members_updated_at -- ============================================================ -- Corrections from 2026-04 review: -- • MLFO is a Global Family Office, NOT a central bank --- • BIS Innovation Hub does NOT hold observer or any voting seat +-- • BIS Innovation Hub removed from directory entirely -- • Added: ICCC, SAID, PANDA, Order of Hospitallers (XOM) -- • Placeholder rows for BRICS founding central banks @@ -89,12 +89,6 @@ VALUES 'US-CO (Colorado)', NULL, 39.61, -104.89, 'Greenwood Village, CO'), - ('bis-innovation-hub', 'BISIH', 'BIS Innovation Hub', - 'standards_body', - 'The BIS Innovation Hub is a publicly documented BIS entity whose mandate is to develop public goods in the technology space. DBIS is presented as the output of this Innovation Hub mandate. Does not hold an observer seat or voting rights.', - 'International (Basel, Switzerland)', NULL, - 47.55, 7.59, 'Basel, Switzerland'), - -- Added entities ('iccc', 'ICCC', 'International Criminal Court of Commerce', 'oversight_judicial', -- 2.34.1