Compare commits

...

6 Commits

Author SHA1 Message Date
Devin AI
9e17ed8ceb fix: remove BIS Innovation Hub from member directory
Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
2026-05-09 20:48:21 +00:00
Devin AI
55a209646a 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 <defi@defi-oracle.io>
2026-05-09 20:32:06 +00:00
Codex
1b5cebf505 Add Gitea live redeploy workflow
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 8s
phoenix-deploy Deployed to explorer-live
2026-04-23 09:51:01 -07:00
fe9edd842b Merge pull request 'security: tighten gitleaks regex + document history-purge audit trail' (#14) from devin/1776542851-harden-gitleaks-and-document-purge into master
Some checks failed
CI / Backend (go 1.23.x) (push) Successful in 51s
CI / Backend security scanners (push) Failing after 45s
CI / Frontend (node 20) (push) Successful in 2m5s
CI / gitleaks (secret scan) (push) Failing after 7s
e2e-full / e2e-full (push) Failing after 21s
2026-04-18 20:08:58 +00:00
fdb14dc420 security: tighten gitleaks regex for escaped form, document history-purge audit trail
Some checks failed
CI / Backend (go 1.23.x) (pull_request) Successful in 56s
CI / Backend security scanners (pull_request) Failing after 40s
CI / Frontend (node 20) (pull_request) Successful in 2m19s
CI / gitleaks (secret scan) (pull_request) Failing after 7s
e2e-full / e2e-full (pull_request) Has been skipped
Two small follow-ups to the out-of-band git-history rewrite that
purged L@ker$2010 / L@kers2010 / L@ker\$2010 from every branch and
tag:

.gitleaks.toml:
  - Regex was L@kers?\$?2010 which catches the expanded form but
    NOT the shell-escaped form (L@ker\$2010) that slipped past PR #3
    in scripts/setup-database.sh. PR #13 fixed the live leak but did
    not tighten the detector. New regex L@kers?\\?\$?2010 catches
    both forms so future pastes of either form fail CI.
  - Description rewritten without the literal password (the previous
    description was redacted by the history rewrite itself and read
    'Legacy hardcoded ... (***REDACTED-LEGACY-PW*** / ***REDACTED-LEGACY-PW***)'
    which was cryptic).

docs/SECURITY.md:
  - New 'History-purge audit trail' section recording what was done,
    how it was verified (0 literal password matches in any blob or
    commit message; 0 legacy-password findings from a post-rewrite
    gitleaks scan), and what operator cleanup is still required on
    the Gitea host to drop the 13 refs/pull/*/head refs that still
    pin the pre-rewrite commits (the update hook declined those refs
    over HTTPS, so only an admin on the Gitea VM can purge them via
    'git update-ref -d' + 'git gc --prune=now' in the bare repo).
  - New 'Re-introduction guard' subsection pointing at the tightened
    regex and commit 78e1ff5.

Verification:
  gitleaks detect --no-git --source . --config .gitleaks.toml   # 0 legacy hits
  git log --all -p | grep -cE 'L@ker\$2010|L@kers2010'         # 0
2026-04-18 20:08:13 +00:00
7c018965eb Merge pull request 'fix(scripts): require DB_PASSWORD env var in setup-database.sh' (#13) from devin/1776542488-fix-setup-database-hardcoded-password into master
Some checks failed
CI / Backend (go 1.23.x) (push) Has been cancelled
CI / Backend security scanners (push) Has been cancelled
CI / Frontend (node 20) (push) Has been cancelled
CI / gitleaks (secret scan) (push) Has been cancelled
2026-04-18 20:02:37 +00:00
14 changed files with 676 additions and 18 deletions

View 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\"}"

View File

@@ -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]

View 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,
})
}

View File

@@ -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
View 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
}

View File

@@ -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
}

View File

@@ -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;

View 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;

View File

@@ -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"
}
}

View File

@@ -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)

View File

@@ -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.

View File

@@ -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 }) {

View File

@@ -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

View File

@@ -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"