Compare commits
6 Commits
devin/1776
...
devin/1778
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e17ed8ceb | ||
|
|
55a209646a | ||
|
|
1b5cebf505 | ||
| fe9edd842b | |||
| fdb14dc420 | |||
| 7c018965eb |
43
.gitea/workflows/deploy-live.yml
Normal file
43
.gitea/workflows/deploy-live.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
name: Deploy Explorer Live
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [main, master]
|
||||
paths:
|
||||
- '.gitea/workflows/deploy-live.yml'
|
||||
- 'backend/**'
|
||||
- 'config/**'
|
||||
- 'deployment/**'
|
||||
- 'docs/**'
|
||||
- 'frontend/**'
|
||||
- 'scripts/**'
|
||||
- 'package.json'
|
||||
- 'package-lock.json'
|
||||
- 'Makefile'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Validate live deploy assets
|
||||
run: |
|
||||
test -f scripts/deploy-explorer-config-to-vmid5000.sh
|
||||
test -f scripts/deploy-explorer-ai-to-vmid5000.sh
|
||||
test -f scripts/deploy-next-frontend-to-vmid5000.sh
|
||||
test -f deployment/LIVE_DEPLOYMENT_MAP.md
|
||||
|
||||
- name: Trigger explorer-live deployment
|
||||
run: |
|
||||
SHA="$(git rev-parse HEAD)"
|
||||
BRANCH="${GITHUB_REF_NAME:-}"
|
||||
if [ -z "$BRANCH" ] || [ "$BRANCH" = "HEAD" ]; then
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
fi
|
||||
curl -sSf -X POST "${{ secrets.PHOENIX_DEPLOY_URL }}" \
|
||||
-H "Authorization: Bearer ${{ secrets.PHOENIX_DEPLOY_TOKEN }}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"repo\":\"${{ gitea.repository }}\",\"sha\":\"${SHA}\",\"branch\":\"${BRANCH}\",\"target\":\"explorer-live\"}"
|
||||
@@ -12,8 +12,8 @@ useDefault = true
|
||||
|
||||
[[rules]]
|
||||
id = "explorer-legacy-db-password-L@ker"
|
||||
description = "Legacy hardcoded Postgres / SSH password (***REDACTED-LEGACY-PW*** / ***REDACTED-LEGACY-PW***)"
|
||||
regex = '''L@kers?\$?2010'''
|
||||
description = "Legacy hardcoded Postgres / SSH password (redacted). Matches both the expanded form and the shell-escaped form (backslash-dollar) that appeared in scripts/setup-database.sh."
|
||||
regex = '''L@kers?\\?\$?2010'''
|
||||
tags = ["password", "explorer-legacy"]
|
||||
|
||||
[allowlist]
|
||||
|
||||
89
backend/api/rest/membership.go
Normal file
89
backend/api/rest/membership.go
Normal file
@@ -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,
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
213
backend/auth/membership.go
Normal file
213
backend/auth/membership.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
178
backend/database/migrations/0017_institutional_membership.up.sql
Normal file
178
backend/database/migrations/0017_institutional_membership.up.sql
Normal file
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ That file reflects the live split deployment now in production:
|
||||
- Frontend deploy: [`scripts/deploy-next-frontend-to-vmid5000.sh`](../scripts/deploy-next-frontend-to-vmid5000.sh)
|
||||
- Config deploy: [`scripts/deploy-explorer-config-to-vmid5000.sh`](../scripts/deploy-explorer-config-to-vmid5000.sh)
|
||||
- Explorer config/API deploy: [`scripts/deploy-explorer-ai-to-vmid5000.sh`](../scripts/deploy-explorer-ai-to-vmid5000.sh)
|
||||
- Gitea live redeploy action: [`.gitea/workflows/deploy-live.yml`](../.gitea/workflows/deploy-live.yml), target `explorer-live`
|
||||
- RPC/API-key edge enforcement: [`ACCESS_EDGE_ENFORCEMENT_RUNBOOK.md`](./ACCESS_EDGE_ENFORCEMENT_RUNBOOK.md)
|
||||
- Public health audit: [`scripts/check-explorer-health.sh`](../scripts/check-explorer-health.sh)
|
||||
- Full public smoke: [`check-explorer-e2e.sh`](../../scripts/verify/check-explorer-e2e.sh)
|
||||
|
||||
@@ -63,6 +63,58 @@ initial public review.
|
||||
- Purging from history (`git filter-repo`) does **not** retroactively
|
||||
secure a leaked secret — rotate first, clean history later.
|
||||
|
||||
## History-purge audit trail
|
||||
|
||||
Following the rotation checklist above, the legacy `L@ker$2010` /
|
||||
`L@kers2010` / `L@ker\$2010` password strings were purged from every
|
||||
branch and tag in this repository using `git filter-repo
|
||||
--replace-text` followed by a `--replace-message` pass for commit
|
||||
message text. The rewritten history was force-pushed with
|
||||
`git push --mirror --force`.
|
||||
|
||||
Verification post-rewrite:
|
||||
|
||||
```
|
||||
git log --all -p | grep -cE 'L@ker\$2010|L@kers2010|L@ker\\\$2010'
|
||||
0
|
||||
gitleaks detect --no-git --source . --config .gitleaks.toml
|
||||
0 legacy-password findings
|
||||
```
|
||||
|
||||
### Residual server-side state (not purgable from the client)
|
||||
|
||||
Gitea's `refs/pull/*/head` refs (the read-only mirror of each PR's
|
||||
original head commit) **cannot be force-updated over HTTPS** — the
|
||||
server's `update` hook declines them. After a history rewrite the
|
||||
following cleanup must be performed **on the Gitea host** by an
|
||||
administrator:
|
||||
|
||||
1. Run `gitea admin repo-sync-release-archive` and
|
||||
`gitea doctor --run all --fix` if available.
|
||||
2. Or manually, as the gitea user on the server:
|
||||
```bash
|
||||
cd /var/lib/gitea/data/gitea-repositories/d-bis/explorer-monorepo.git
|
||||
git for-each-ref --format='%(refname)' 'refs/pull/*/head' | \
|
||||
xargs -n1 git update-ref -d
|
||||
git gc --prune=now --aggressive
|
||||
```
|
||||
3. Restart Gitea.
|
||||
|
||||
Until this server-side cleanup is performed, the 13 `refs/pull/*/head`
|
||||
refs still pin the pre-rewrite commits containing the legacy
|
||||
password. This does not affect branches, the default clone, or
|
||||
`master` — but the old commits remain reachable by SHA through the
|
||||
Gitea web UI (e.g. on the merged PR's **Files Changed** tab).
|
||||
|
||||
### Re-introduction guard
|
||||
|
||||
The `.gitleaks.toml` rule `explorer-legacy-db-password-L@ker` was
|
||||
tightened from `L@kers?\$?2010` to `L@kers?\\?\$?2010` so it also
|
||||
catches the shell-escaped form that slipped past the original PR #3
|
||||
scrub (see commit `78e1ff5`). Future attempts to paste any variant of
|
||||
the legacy password — in source, shell scripts, or env files — will
|
||||
fail the `gitleaks` CI job wired in PR #5.
|
||||
|
||||
## Build-time / CI checks (wired in PR #5)
|
||||
|
||||
- `gitleaks` pre-commit + CI gate on every PR.
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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<InstitutionalTier, string> = {
|
||||
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
|
||||
|
||||
@@ -9,6 +9,7 @@ set -euo pipefail
|
||||
|
||||
VMID="${VMID:-5000}"
|
||||
FRONTEND_PORT="${FRONTEND_PORT:-3000}"
|
||||
FORCE_REMOTE_PCT="${FORCE_REMOTE_PCT:-0}"
|
||||
SERVICE_NAME="solacescanscout-frontend"
|
||||
APP_ROOT="/opt/solacescanscout/frontend"
|
||||
PROXMOX_R630_02="${PROXMOX_HOST_R630_02:-192.168.11.12}"
|
||||
@@ -53,7 +54,7 @@ push_into_vmid() {
|
||||
local destination_path="$2"
|
||||
local perms="${3:-0644}"
|
||||
|
||||
if [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
|
||||
if [[ "$FORCE_REMOTE_PCT" != "1" ]] && [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
|
||||
install -D -m "$perms" "$source_path" "$destination_path"
|
||||
elif command -v pct >/dev/null 2>&1; then
|
||||
pct push "$VMID" "$source_path" "$destination_path" --perms "$perms"
|
||||
@@ -68,7 +69,7 @@ push_into_vmid() {
|
||||
run_in_vmid() {
|
||||
local command="$1"
|
||||
|
||||
if [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
|
||||
if [[ "$FORCE_REMOTE_PCT" != "1" ]] && [[ -f /proc/1/cgroup ]] && grep -q "lxc" /proc/1/cgroup 2>/dev/null; then
|
||||
bash -lc "$command"
|
||||
elif command -v pct >/dev/null 2>&1; then
|
||||
pct exec "$VMID" -- bash -lc "$command"
|
||||
|
||||
Reference in New Issue
Block a user