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..4fc9dda --- /dev/null +++ b/backend/database/migrations/0017_institutional_membership.up.sql @@ -0,0 +1,178 @@ +-- 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 removed from directory entirely +-- • 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'), + + -- 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 1f60787..46a8963 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 aea0b3c..a65ef91 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