@relay-federation/bridge 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli.js CHANGED
@@ -293,7 +293,7 @@ async function cmdStart () {
293
293
  }
294
294
 
295
295
  // ── 2. Core components ────────────────────────────────────
296
- const peerManager = new PeerManager({ maxPeers: config.maxPeers })
296
+ const peerManager = new PeerManager()
297
297
  const headerRelay = new HeaderRelay(peerManager)
298
298
  const txRelay = new TxRelay(peerManager)
299
299
 
@@ -320,9 +320,10 @@ async function cmdStart () {
320
320
  })
321
321
 
322
322
  // Wire peer health tracking
323
- peerManager.on('peer:connect', ({ pubkeyHex }) => {
323
+ peerManager.on('peer:connect', ({ pubkeyHex, endpoint }) => {
324
324
  peerHealth.recordSeen(pubkeyHex)
325
325
  scorer.setStakeAge(pubkeyHex, 7)
326
+ if (endpoint) gossipManager.addSeed({ pubkeyHex, endpoint, meshId: config.meshId })
326
327
  })
327
328
 
328
329
  peerManager.on('peer:disconnect', ({ pubkeyHex }) => {
@@ -430,6 +431,8 @@ async function cmdStart () {
430
431
  const { extractOpReturnData, decodePayload, PROTOCOL_PREFIX } = await import('../registry/lib/cbor.js')
431
432
  const { Transaction: BsvTx } = await import('@bsv/sdk')
432
433
 
434
+ // Registry bootstrapped via discoverNewPeers() after server start (no WoC dependency)
435
+
433
436
  watcher.on('utxo:received', async ({ txid, hash160 }) => {
434
437
  if (hash160 !== beaconHash160) return
435
438
 
@@ -641,14 +644,17 @@ async function cmdStart () {
641
644
  let gossipStarted = false
642
645
 
643
646
  // Start gossip after first peer connection completes.
644
- // With crypto handshake, peer:connect fires AFTER handshake verification,
645
- // so gossip won't race the handshake anymore.
647
+ // Delay 5s so all seed handshakes finish before gossip broadcasts
648
+ // (immediate broadcast would send announce/getpeers through connections
649
+ // whose inbound side is still waiting for verify — breaking the handshake).
646
650
  peerManager.on('peer:connect', () => {
647
651
  if (!gossipStarted) {
648
652
  gossipStarted = true
649
- gossipManager.start()
650
- gossipManager.requestPeersFromAll()
651
- console.log('Gossip started')
653
+ setTimeout(() => {
654
+ gossipManager.start()
655
+ gossipManager.requestPeersFromAll()
656
+ console.log('Gossip started')
657
+ }, 5000)
652
658
 
653
659
  // Periodic peer refresh — re-request peer lists every 10 minutes
654
660
  // Catches registrations missed during downtime or initial gossip
@@ -703,6 +709,7 @@ async function cmdStart () {
703
709
  // Connect to seed peers (accept both string URLs and {pubkeyHex, endpoint} objects)
704
710
  console.log(`Connecting to ${seedPeers.length} seed peer(s)...`)
705
711
  for (let i = 0; i < seedPeers.length; i++) {
712
+ if (i > 0) await new Promise(r => setTimeout(r, 2000)) // stagger to avoid handshake races
706
713
  const seed = seedPeers[i]
707
714
  const endpoint = typeof seed === 'string' ? seed : seed.endpoint
708
715
  const pubkey = typeof seed === 'string' ? `seed_${i}` : seed.pubkeyHex
@@ -770,6 +777,47 @@ async function cmdStart () {
770
777
  statusServer.startAppMonitoring()
771
778
  console.log(` Status: http://127.0.0.1:${statusPort}/status`)
772
779
 
780
+ // ── Peer discovery — bootstrap registry from seed peers, then periodic refresh ──
781
+ const knownEndpoints = new Set()
782
+ for (const sp of (config.seedPeers || [])) knownEndpoints.add(sp.endpoint)
783
+ knownEndpoints.add(config.endpoint)
784
+
785
+ async function discoverNewPeers () {
786
+ const peersToQuery = [...(config.seedPeers || [])]
787
+ for (const [, conn] of peerManager.peers) {
788
+ if (conn.endpoint && conn.readyState === 1) peersToQuery.push({ endpoint: conn.endpoint })
789
+ }
790
+ for (const peer of peersToQuery) {
791
+ try {
792
+ const ep = peer.endpoint || ''
793
+ const u = new URL(ep)
794
+ const statusUrl = 'http://' + u.hostname + ':' + (parseInt(u.port, 10) + 1000) + '/discover'
795
+ const res = await fetch(statusUrl, { signal: AbortSignal.timeout(5000) })
796
+ if (!res.ok) continue
797
+ const data = await res.json()
798
+ if (!data.bridges) continue
799
+ for (const b of data.bridges) {
800
+ if (!b.endpoint) continue
801
+ if (b.pubkeyHex) registeredPubkeys.add(b.pubkeyHex)
802
+ seedEndpoints.add(b.endpoint)
803
+ if (knownEndpoints.has(b.endpoint)) continue
804
+ knownEndpoints.add(b.endpoint)
805
+ const conn = peerManager.connectToPeer({ endpoint: b.endpoint, pubkeyHex: b.pubkeyHex })
806
+ if (conn) {
807
+ conn.on('open', () => performOutboundHandshake(conn))
808
+ const msg = `Discovered new peer: ${b.name || b.pubkeyHex?.slice(0, 16) || b.endpoint}`
809
+ console.log(msg)
810
+ statusServer.addLog(msg)
811
+ }
812
+ }
813
+ } catch {}
814
+ }
815
+ }
816
+ await discoverNewPeers()
817
+ console.log(` Registry: ${registeredPubkeys.size} trusted pubkeys after peer discovery`)
818
+ setTimeout(discoverNewPeers, 5000)
819
+ setInterval(discoverNewPeers, 300000)
820
+
773
821
  // ── 9. Log events (dual: console + status server ring buffer) ──
774
822
  peerManager.on('peer:connect', ({ pubkeyHex }) => {
775
823
  const msg = `Peer connected: ${pubkeyHex.slice(0, 16)}...`
@@ -4,6 +4,8 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Federation Mesh Explorer</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/three@0.137.0/build/three.min.js"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/three@0.137.0/examples/js/controls/OrbitControls.js"></script>
7
9
  <style>
8
10
  :root {
9
11
  --bg-base: #121518;
@@ -185,7 +187,7 @@
185
187
  .tab-content {
186
188
  flex: 1;
187
189
  overflow-y: auto;
188
- padding: 20px 24px;
190
+ padding: 14px 20px;
189
191
  }
190
192
  .tab-content::-webkit-scrollbar { width: 6px; }
191
193
  .tab-content::-webkit-scrollbar-track { background: transparent; }
@@ -196,14 +198,14 @@
196
198
  display: grid;
197
199
  grid-template-columns: repeat(5, 1fr);
198
200
  gap: 12px;
199
- margin-bottom: 20px;
201
+ margin-bottom: 12px;
200
202
  }
201
203
  .stat-card {
202
- padding: 18px 20px;
204
+ padding: 12px 16px;
203
205
  text-align: center;
204
206
  }
205
207
  .stat-card .stat-value {
206
- font-size: 26px;
208
+ font-size: 22px;
207
209
  font-weight: 700;
208
210
  color: var(--text-primary);
209
211
  font-variant-numeric: tabular-nums;
@@ -221,7 +223,7 @@
221
223
  /* ── Overview cards grid ─────────────────────────── */
222
224
  .cards-grid {
223
225
  display: grid;
224
- grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
226
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
225
227
  gap: 12px;
226
228
  margin-bottom: 20px;
227
229
  }
@@ -583,13 +585,15 @@
583
585
 
584
586
  /* ── Peers ───────────────────────────────────────── */
585
587
  .peer-entry {
586
- padding: 10px 0;
588
+ padding: 6px 0;
587
589
  border-bottom: 1px solid var(--border);
588
590
  font-size: 12px;
589
591
  }
590
592
  .peer-entry:last-child { border-bottom: none; }
591
593
  .peer-entry .peer-name { color: var(--accent-blue); font-family: var(--mono); }
592
- .peer-entry .peer-meta { color: var(--text-dim); margin-top: 3px; font-size: 11px; }
594
+ .peer-entry .peer-meta { color: var(--text-dim); margin-top: 2px; font-size: 11px; }
595
+ .peer-entry .score-bars { display: none; }
596
+ .peer-entry.expanded .score-bars { display: block; }
593
597
  .peer-dot {
594
598
  display: inline-block;
595
599
  width: 6px;
@@ -647,6 +651,88 @@
647
651
  .mini-map .mm-label { fill: var(--text-muted); font-size: 9px; font-family: inherit; text-anchor: middle; }
648
652
  .mini-map .mm-label.name { fill: var(--accent-blue); font-size: 10px; font-weight: 600; }
649
653
 
654
+ /* ── 3D Mesh Map ──────────────────────────────────── */
655
+ .mesh3d-container {
656
+ position: relative;
657
+ width: 100%;
658
+ height: 180px;
659
+ border-radius: var(--radius-sm);
660
+ overflow: hidden;
661
+ background: radial-gradient(ellipse at center, rgba(20,30,50,0.9) 0%, rgba(8,10,14,0.95) 100%);
662
+ cursor: pointer;
663
+ }
664
+ .mesh3d-overlay {
665
+ position: fixed;
666
+ top: 0; left: 0; right: 0; bottom: 0;
667
+ z-index: 9999;
668
+ background: rgba(0,0,0,0.85);
669
+ display: flex;
670
+ align-items: center;
671
+ justify-content: center;
672
+ }
673
+ .mesh3d-overlay .mesh3d-expanded {
674
+ position: relative;
675
+ width: 90vw;
676
+ height: 85vh;
677
+ border-radius: var(--radius);
678
+ overflow: hidden;
679
+ background: radial-gradient(ellipse at center, rgba(20,30,50,0.95) 0%, rgba(8,10,14,0.98) 100%);
680
+ border: 1px solid var(--border-light);
681
+ }
682
+ .mesh3d-overlay .mesh3d-expanded canvas {
683
+ display: block;
684
+ width: 100% !important;
685
+ height: 100% !important;
686
+ }
687
+ .mesh3d-overlay .mesh3d-close {
688
+ position: absolute;
689
+ top: 12px; right: 16px;
690
+ z-index: 10;
691
+ background: rgba(0,0,0,0.5);
692
+ border: 1px solid var(--border);
693
+ color: var(--text-muted);
694
+ font-size: 18px;
695
+ width: 32px; height: 32px;
696
+ border-radius: 50%;
697
+ cursor: pointer;
698
+ display: flex;
699
+ align-items: center;
700
+ justify-content: center;
701
+ }
702
+ .mesh3d-overlay .mesh3d-close:hover { color: var(--text-primary); background: rgba(255,255,255,0.1); }
703
+ .mesh3d-container canvas {
704
+ display: block;
705
+ width: 100% !important;
706
+ height: 100% !important;
707
+ border-radius: var(--radius-sm);
708
+ }
709
+ .mesh3d-fallback {
710
+ display: flex;
711
+ flex-direction: column;
712
+ align-items: center;
713
+ justify-content: center;
714
+ height: 100%;
715
+ color: var(--text-dim);
716
+ font-size: 12px;
717
+ gap: 8px;
718
+ }
719
+ .mesh3d-label {
720
+ position: absolute;
721
+ font-size: 10px;
722
+ font-family: var(--mono);
723
+ pointer-events: none;
724
+ white-space: nowrap;
725
+ transform: translate(-50%, -50%);
726
+ background: rgba(0,0,0,0.7);
727
+ padding: 2px 7px;
728
+ border-radius: 4px;
729
+ border: 1px solid rgba(255,255,255,0.1);
730
+ letter-spacing: 0.5px;
731
+ }
732
+ .mesh3d-label.center { color: var(--accent-blue); font-weight: 700; font-size: 12px; border-color: rgba(88,166,255,0.3); background: rgba(88,166,255,0.12); text-shadow: 0 0 8px rgba(88,166,255,0.5); }
733
+ .mesh3d-label.online { color: #7ee8a0; border-color: rgba(63,185,80,0.25); text-shadow: 0 0 6px rgba(63,185,80,0.4); }
734
+ .mesh3d-label.offline { color: var(--accent-red); opacity: 0.7; border-color: rgba(248,81,73,0.2); }
735
+
650
736
  /* ── Modal ───────────────────────────────────────── */
651
737
  .modal-overlay {
652
738
  display: none;
@@ -917,11 +1003,12 @@ function generateQR(text) {
917
1003
  }
918
1004
 
919
1005
  // ── Configuration ──────────────────────────────────
920
- const SEED_BRIDGES = [
921
- { name: 'bridge-alpha', url: 'http://144.202.48.217:9333' },
922
- { name: 'bridge-beta', url: 'http://45.63.77.31:9333' }
1006
+ // Seed URLs only — names come from each bridge's /status response
1007
+ const SEED_URLS = [
1008
+ 'http://144.202.48.217:9333',
1009
+ 'http://45.63.77.31:9333'
923
1010
  ];
924
- let BRIDGES = [...SEED_BRIDGES];
1011
+ let BRIDGES = SEED_URLS.map(url => ({ name: null, url }));
925
1012
  const POLL_INTERVAL = 5000;
926
1013
  let discoveryDone = false;
927
1014
 
@@ -978,10 +1065,17 @@ async function fetchBridge(bridge) {
978
1065
  const r = await fetch(bridge.url + '/status' + getAuthParam(bridge.url), { signal: AbortSignal.timeout(5000) });
979
1066
  if (!r.ok) throw new Error('HTTP ' + r.status);
980
1067
  const data = await r.json();
981
- data._name = (data.bridge && data.bridge.name) || bridge.name; data._url = bridge.url; data._error = null; data._lastSeen = Date.now();
1068
+ // Bridge self-reports its name use that as canonical, fall back to URL-derived name
1069
+ const selfName = (data.bridge && data.bridge.name) ? data.bridge.name : null;
1070
+ const fallbackName = bridge.name || ('bridge-' + new URL(bridge.url).hostname.split('.').pop());
1071
+ data._name = selfName || fallbackName;
1072
+ // Update BRIDGES entry so the name sticks for future polls
1073
+ if (selfName && !bridge.name) bridge.name = selfName;
1074
+ data._url = bridge.url; data._error = null; data._lastSeen = Date.now();
982
1075
  return data;
983
1076
  } catch (e) {
984
- return { _name: bridge.name, _url: bridge.url, _error: e.message,
1077
+ const fallbackName = bridge.name || ('bridge-' + new URL(bridge.url).hostname.split('.').pop());
1078
+ return { _name: fallbackName, _url: bridge.url, _error: e.message,
985
1079
  bridge: { pubkeyHex: null, endpoint: null, meshId: null, uptimeSeconds: 0 },
986
1080
  peers: { connected: 0, max: 0, list: [] },
987
1081
  headers: { bestHeight: -1, bestHash: null, count: 0 },
@@ -998,10 +1092,11 @@ async function fetchMempool(bridge) {
998
1092
  }
999
1093
  async function discoverBridges() {
1000
1094
  const known = new Map();
1001
- for (const b of SEED_BRIDGES) known.set(b.url, b.name);
1002
- for (const seed of SEED_BRIDGES) {
1095
+ // Seed URLs start with no name — /status will fill them in
1096
+ for (const url of SEED_URLS) known.set(url, null);
1097
+ for (const url of SEED_URLS) {
1003
1098
  try {
1004
- const r = await fetch(seed.url + '/discover', { signal: AbortSignal.timeout(5000) });
1099
+ const r = await fetch(url + '/discover', { signal: AbortSignal.timeout(5000) });
1005
1100
  if (!r.ok) continue;
1006
1101
  const data = await r.json();
1007
1102
  if (!data.bridges) continue;
@@ -1009,8 +1104,8 @@ async function discoverBridges() {
1009
1104
  if (!b.statusUrl) continue;
1010
1105
  const base = b.statusUrl.replace(/\/status$/, '');
1011
1106
  if (!known.has(base)) {
1012
- const name = b.name || 'bridge-' + (b.pubkeyHex ? b.pubkeyHex.slice(0, 8) : known.size);
1013
- known.set(base, name);
1107
+ // Use name from /discover if available, otherwise placeholder until /status fills it in
1108
+ known.set(base, b.name || null);
1014
1109
  }
1015
1110
  }
1016
1111
  } catch {}
@@ -1088,6 +1183,8 @@ function setTab(tab) {
1088
1183
  if (inp) savedExplorerInput = inp.value;
1089
1184
  if (res) savedExplorerResult = res.innerHTML;
1090
1185
  }
1186
+ // Destroy 3D scene when leaving overview
1187
+ if (activeTab === 'overview' && tab !== 'overview') destroyMeshMap3D();
1091
1188
  activeTab = tab;
1092
1189
  const buttons = document.querySelectorAll('#headerTabs button');
1093
1190
  const tabs = ['overview', 'mempool', 'explorer', 'inscriptions', 'tokens', 'apps'];
@@ -1101,6 +1198,13 @@ function renderActiveTab(fromPoll) {
1101
1198
  const el = document.getElementById('tabContent');
1102
1199
  // Skip apps/explorer re-render during poll (preserves state)
1103
1200
  if (fromPoll && (activeTab === 'apps' || activeTab === 'explorer' || activeTab === 'inscriptions' || activeTab === 'tokens')) return;
1201
+ // On poll, if overview tab has active 3D scene, just update data — don't rebuild DOM
1202
+ if (fromPoll && activeTab === 'overview' && mesh3d) {
1203
+ updateMeshMap3D();
1204
+ return;
1205
+ }
1206
+ // Destroy 3D before rebuilding overview DOM
1207
+ if (activeTab === 'overview') destroyMeshMap3D();
1104
1208
  let html;
1105
1209
  switch (activeTab) {
1106
1210
  case 'overview': html = renderOverviewTab(); break;
@@ -1115,6 +1219,20 @@ function renderActiveTab(fromPoll) {
1115
1219
  if (el.innerHTML !== html) {
1116
1220
  el.innerHTML = html;
1117
1221
  if (activeTab === 'explorer') restoreExplorer();
1222
+ // Init 3D mesh after overview DOM is in place
1223
+ if (activeTab === 'overview') {
1224
+ const onlineBridges = [...bridgeData.values()].filter(b => !b._error);
1225
+ if (onlineBridges.length > 0) {
1226
+ setTimeout(() => {
1227
+ if (!initMeshMap3D('mesh3dCanvas')) {
1228
+ // Fallback to 2D SVG if WebGL unavailable
1229
+ const b = bridgeData.get(selectedBridge);
1230
+ const container = document.getElementById('mesh3dCanvas');
1231
+ if (container && b) container.innerHTML = '<div class="mesh3d-fallback">' + renderMiniMap(b) + '<span>WebGL not available</span></div>';
1232
+ }
1233
+ }, 50);
1234
+ }
1235
+ }
1118
1236
  }
1119
1237
  }
1120
1238
 
@@ -1159,6 +1277,14 @@ function renderOverviewTab() {
1159
1277
 
1160
1278
  if (b._error) html += '<div class="error-banner">' + b._error + '</div>';
1161
1279
 
1280
+ // 3D Mesh Map — compact inline, click to expand
1281
+ const onlineBridgeCount = [...bridgeData.values()].filter(bb => !bb._error).length;
1282
+ if (onlineBridgeCount > 0) {
1283
+ html += '<div class="detail-card glass" style="margin-bottom:12px;padding:10px 14px"><h3 style="margin-bottom:6px">Mesh Topology (' + onlineBridgeCount + ' nodes, ' + bridgeData.size + ' known) <span style="font-size:9px;color:var(--text-dim);font-weight:400;text-transform:none;letter-spacing:0">— click to expand</span></h3>';
1284
+ html += '<div class="mesh3d-container" id="mesh3dCanvas" onclick="expandMeshMap()"></div>';
1285
+ html += '</div>';
1286
+ }
1287
+
1162
1288
  html += '<div class="cards-grid">';
1163
1289
 
1164
1290
  // Bridge info card
@@ -1206,17 +1332,22 @@ function renderOverviewTab() {
1206
1332
  }
1207
1333
 
1208
1334
  // Peers card
1209
- html += '<div class="detail-card glass"><h3>Peers (' + b.peers.connected + '/' + b.peers.max + ')</h3>';
1335
+ html += '<div class="detail-card glass"><h3>Peers (' + b.peers.connected + ')</h3>';
1210
1336
  if (b.peers.list.length === 0) {
1211
1337
  html += '<div style="color:var(--text-dim);font-size:12px">No peers connected</div>';
1212
1338
  } else {
1213
- for (const p of b.peers.list) {
1339
+ const showAll = window._showAllPeers || false;
1340
+ const visiblePeers = showAll ? b.peers.list : b.peers.list.slice(0, 3);
1341
+ for (const p of visiblePeers) {
1214
1342
  const dotC = p.connected ? 'green' : 'red';
1215
- html += '<div class="peer-entry"><div><span class="peer-dot ' + dotC + '"></span><span class="peer-name">' + truncPubkey(p.pubkeyHex) + '</span> <span style="color:var(--text-dim)">score ' + (p.score !== undefined ? p.score.toFixed(2) : '?') + '</span></div>';
1343
+ html += '<div class="peer-entry" style="cursor:pointer" onclick="this.classList.toggle(\'expanded\')"><div><span class="peer-dot ' + dotC + '"></span><span class="peer-name">' + truncPubkey(p.pubkeyHex) + '</span> <span style="color:var(--text-dim)">score ' + (p.score !== undefined ? p.score.toFixed(2) : '?') + '</span></div>';
1216
1344
  html += '<div class="peer-meta">' + (p.endpoint || '') + (p.health ? ' &middot; ' + p.health : '') + '</div>';
1217
- if (p.scoreBreakdown) html += renderScoreBars(p.scoreBreakdown);
1345
+ if (p.scoreBreakdown) html += '<div class="score-bars">' + renderScoreBars(p.scoreBreakdown) + '</div>';
1218
1346
  html += '</div>';
1219
1347
  }
1348
+ if (b.peers.list.length > 3) {
1349
+ html += '<div style="text-align:center;padding:4px 0"><button class="btn ghost" style="font-size:11px;padding:2px 10px" onclick="window._showAllPeers=!window._showAllPeers;renderActiveTab()">' + (showAll ? 'Show less' : 'Show all ' + b.peers.list.length + ' peers') + '</button></div>';
1350
+ }
1220
1351
  }
1221
1352
  html += '</div>';
1222
1353
 
@@ -1235,11 +1366,6 @@ function renderOverviewTab() {
1235
1366
  html += '</div>';
1236
1367
  }
1237
1368
 
1238
- // Mini map card
1239
- if (b.peers && b.peers.list && b.peers.list.length > 0) {
1240
- html += '<div class="detail-card glass"><h3>Mesh Map</h3>' + renderMiniMap(b) + '</div>';
1241
- }
1242
-
1243
1369
  html += '</div>'; // cards-grid
1244
1370
 
1245
1371
  html += '<div style="color:var(--text-dim);font-size:10px;text-align:center;padding:12px 0">Updated ' + new Date().toLocaleTimeString() + '</div>';
@@ -1686,6 +1812,385 @@ function renderParsedData(type, protocol, parsed, txid, vout) {
1686
1812
  }
1687
1813
 
1688
1814
 
1815
+ // ── 3D Mesh Map ───────────────────────────────────
1816
+ let mesh3d = null; // { renderer, scene, camera, controls, animId, nodes, lines, particles, labels, container, lastInteraction }
1817
+
1818
+ function hasWebGL() {
1819
+ try { const c = document.createElement('canvas'); return !!(window.WebGLRenderingContext && (c.getContext('webgl') || c.getContext('experimental-webgl'))); } catch { return false; }
1820
+ }
1821
+
1822
+ function initMeshMap3D(containerId) {
1823
+ if (mesh3d) destroyMeshMap3D();
1824
+ const container = document.getElementById(containerId);
1825
+ if (!container || !hasWebGL() || typeof THREE === 'undefined') return false;
1826
+
1827
+ const w = container.clientWidth, h = container.clientHeight;
1828
+
1829
+ // ── Build topology from ALL bridges ──────────────
1830
+ const allBridges = [...bridgeData.values()].filter(b => !b._error);
1831
+ const n = allBridges.length;
1832
+ if (n === 0) return false;
1833
+
1834
+ // Map pubkey → bridge name for cross-referencing
1835
+ const pubkeyToName = new Map();
1836
+ for (const b of allBridges) {
1837
+ if (b.bridge && b.bridge.pubkeyHex) pubkeyToName.set(b.bridge.pubkeyHex, b._name);
1838
+ }
1839
+
1840
+ // Build edge list: pairs of bridge indices that are connected
1841
+ const edges = [];
1842
+ const edgeSet = new Set();
1843
+ for (let i = 0; i < n; i++) {
1844
+ const b = allBridges[i];
1845
+ if (!b.peers || !b.peers.list) continue;
1846
+ for (const p of b.peers.list) {
1847
+ if (!p.connected) continue;
1848
+ const peerName = pubkeyToName.get(p.pubkeyHex);
1849
+ if (!peerName) continue;
1850
+ const j = allBridges.findIndex(bb => bb._name === peerName);
1851
+ if (j < 0 || j === i) continue;
1852
+ const key = Math.min(i, j) + ':' + Math.max(i, j);
1853
+ if (edgeSet.has(key)) continue;
1854
+ edgeSet.add(key);
1855
+ // Get score data from the peer entry
1856
+ const score = p.score !== undefined ? p.score : 0.5;
1857
+ const breakdown = p.scoreBreakdown || null;
1858
+ edges.push({ a: i, b: j, score, breakdown });
1859
+ }
1860
+ }
1861
+
1862
+ // ── Scene setup ──────────────────────────────────
1863
+ const scene = new THREE.Scene();
1864
+ const camera = new THREE.PerspectiveCamera(50, w / h, 0.1, 200);
1865
+ // Scale camera distance based on node count
1866
+ const camDist = Math.max(14, n * 2.5);
1867
+ camera.position.set(0, camDist * 0.4, camDist);
1868
+
1869
+ const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
1870
+ renderer.setSize(w, h);
1871
+ renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
1872
+ renderer.setClearColor(0x000000, 0);
1873
+ container.appendChild(renderer.domElement);
1874
+
1875
+ const controls = new THREE.OrbitControls(camera, renderer.domElement);
1876
+ controls.enableDamping = true;
1877
+ controls.dampingFactor = 0.08;
1878
+ controls.enablePan = false;
1879
+ controls.minDistance = 5;
1880
+ controls.maxDistance = camDist * 2;
1881
+ controls.autoRotate = true;
1882
+ controls.autoRotateSpeed = 0.5;
1883
+
1884
+ // ── Lights ───────────────────────────────────────
1885
+ scene.add(new THREE.AmbientLight(0x334466, 0.8));
1886
+ const dirLight = new THREE.DirectionalLight(0xffffff, 0.9);
1887
+ dirLight.position.set(5, 10, 7);
1888
+ scene.add(dirLight);
1889
+ const rimLight = new THREE.DirectionalLight(0x58a6ff, 0.3);
1890
+ rimLight.position.set(-3, -5, -3);
1891
+ scene.add(rimLight);
1892
+
1893
+ // ── Starfield ────────────────────────────────────
1894
+ const starGeo = new THREE.BufferGeometry();
1895
+ const starPositions = new Float32Array(400 * 3);
1896
+ for (let i = 0; i < 400; i++) {
1897
+ starPositions[i * 3] = (Math.random() - 0.5) * 120;
1898
+ starPositions[i * 3 + 1] = (Math.random() - 0.5) * 120;
1899
+ starPositions[i * 3 + 2] = (Math.random() - 0.5) * 120;
1900
+ }
1901
+ starGeo.setAttribute('position', new THREE.BufferAttribute(starPositions, 3));
1902
+ scene.add(new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 0.15, transparent: true, opacity: 0.35 })));
1903
+
1904
+ // ── Node positions — even distribution in 3D ─────
1905
+ const orbitRadius = Math.max(5, n * 1.2);
1906
+ const nodePositions = [];
1907
+ for (let i = 0; i < n; i++) {
1908
+ if (n === 1) {
1909
+ nodePositions.push(new THREE.Vector3(0, 0, 0));
1910
+ } else if (n === 2) {
1911
+ nodePositions.push(new THREE.Vector3(i === 0 ? -3 : 3, 0, 0));
1912
+ } else {
1913
+ // Distribute on a circle (flatten Y) for small counts, Fibonacci sphere for large
1914
+ const phi = Math.acos(1 - 2 * (i + 0.5) / n);
1915
+ const theta = Math.PI * (1 + Math.sqrt(5)) * i;
1916
+ const x = orbitRadius * Math.sin(phi) * Math.cos(theta);
1917
+ const y = orbitRadius * Math.cos(phi) * 0.35;
1918
+ const z = orbitRadius * Math.sin(phi) * Math.sin(theta);
1919
+ nodePositions.push(new THREE.Vector3(x, y, z));
1920
+ }
1921
+ }
1922
+
1923
+ // ── Create bridge nodes ──────────────────────────
1924
+ const nodes = [];
1925
+ const labelEls = [];
1926
+ const glowRings = [];
1927
+
1928
+ for (let i = 0; i < n; i++) {
1929
+ const b = allBridges[i];
1930
+ const isSelected = b._name === selectedBridge;
1931
+ const isOnline = !b._error;
1932
+ const peerCount = b.peers ? b.peers.connected : 0;
1933
+
1934
+ // All bridges same size — color based on health only
1935
+ const radius = 0.65;
1936
+ const nodeGeo = new THREE.SphereGeometry(radius, 32, 32);
1937
+
1938
+ let color, emissive, emissiveIntensity;
1939
+ if (isOnline && peerCount > 0) {
1940
+ color = 0x3fb950; emissive = 0x2a5a2a; emissiveIntensity = 0.7;
1941
+ } else if (isOnline) {
1942
+ color = 0xd29922; emissive = 0x4a3a10; emissiveIntensity = 0.5;
1943
+ } else {
1944
+ color = 0xf85149; emissive = 0x5a2020; emissiveIntensity = 0.5;
1945
+ }
1946
+
1947
+ const nodeMat = new THREE.MeshStandardMaterial({ color, emissive, emissiveIntensity, roughness: 0.2, metalness: 0.6 });
1948
+ const nodeMesh = new THREE.Mesh(nodeGeo, nodeMat);
1949
+ nodeMesh.position.copy(nodePositions[i]);
1950
+ scene.add(nodeMesh);
1951
+
1952
+ // Point light per node — same for all
1953
+ const lightColor = isOnline ? 0x3fb950 : 0xf85149;
1954
+ const pointLight = new THREE.PointLight(lightColor, 0.8, 6);
1955
+ nodeMesh.add(pointLight);
1956
+
1957
+ // Subtle selection ring — thin white ring if this is the sidebar-selected bridge
1958
+ if (isSelected) {
1959
+ const selRing = new THREE.Mesh(
1960
+ new THREE.RingGeometry(0.85, 0.95, 48),
1961
+ new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.35, side: THREE.DoubleSide })
1962
+ );
1963
+ selRing.rotation.x = -Math.PI / 2;
1964
+ selRing.position.copy(nodePositions[i]);
1965
+ scene.add(selRing);
1966
+ glowRings.push(selRing);
1967
+ }
1968
+
1969
+ nodes.push({ mesh: nodeMesh, bridge: b, isSelected });
1970
+
1971
+ // HTML label — bridge name, all same style (online/offline only)
1972
+ const label = document.createElement('div');
1973
+ label.className = 'mesh3d-label ' + (isOnline ? 'online' : 'offline');
1974
+ label.textContent = b._name;
1975
+ container.appendChild(label);
1976
+ labelEls.push({ el: label, nodeMesh });
1977
+ }
1978
+
1979
+ // ── Create edges between connected bridges ───────
1980
+ const edgeObjs = [];
1981
+ const particles = [];
1982
+
1983
+ for (const edge of edges) {
1984
+ const posA = nodePositions[edge.a];
1985
+ const posB = nodePositions[edge.b];
1986
+
1987
+ // Bezier midpoint — arc upward for visual clarity
1988
+ const mid = new THREE.Vector3(
1989
+ (posA.x + posB.x) * 0.5,
1990
+ (posA.y + posB.y) * 0.5 + orbitRadius * 0.2,
1991
+ (posA.z + posB.z) * 0.5
1992
+ );
1993
+ const curve = new THREE.QuadraticBezierCurve3(posA.clone(), mid, posB.clone());
1994
+ const tubeGeo = new THREE.TubeGeometry(curve, 48, 0.03, 8, false);
1995
+ const tubeMat = new THREE.MeshBasicMaterial({ color: 0x58a6ff, transparent: true, opacity: 0.45 });
1996
+ const tubeMesh = new THREE.Mesh(tubeGeo, tubeMat);
1997
+ scene.add(tubeMesh);
1998
+ edgeObjs.push({ mesh: tubeMesh, curve, edge });
1999
+
2000
+ // Traffic particles — count driven by mempool activity, speed by latency
2001
+ // Get mempool sizes for the two bridges on this edge
2002
+ const bridgeA = allBridges[edge.a];
2003
+ const bridgeB = allBridges[edge.b];
2004
+ const mempoolA = bridgeA.txs ? bridgeA.txs.mempool : 0;
2005
+ const mempoolB = bridgeB.txs ? bridgeB.txs.mempool : 0;
2006
+ const traffic = Math.max(mempoolA, mempoolB);
2007
+ // 1 idle particle (keepalive pulse) + 1 per 5 mempool txs, max 8
2008
+ const pCount = Math.min(8, 1 + Math.floor(traffic / 5));
2009
+ // Speed: slow idle crawl when quiet, faster with more RTT score (inverted — high score = low latency = faster)
2010
+ const rtt = (edge.breakdown && edge.breakdown.responseTime) ? edge.breakdown.responseTime : 0.5;
2011
+ const baseSpeed = traffic > 0 ? 0.0015 : 0.0006; // idle crawl vs active
2012
+ const speed = baseSpeed + rtt * 0.002;
2013
+ for (let j = 0; j < pCount; j++) {
2014
+ const pGeo = new THREE.SphereGeometry(0.07, 8, 8);
2015
+ // Idle particles are dimmer, active ones brighter
2016
+ const brightness = traffic > 0 ? 0x7ec8ff : 0x4a7a9a;
2017
+ const pMat = new THREE.MeshBasicMaterial({ color: brightness, transparent: true, opacity: traffic > 0 ? 0.9 : 0.4 });
2018
+ const pMesh = new THREE.Mesh(pGeo, pMat);
2019
+ scene.add(pMesh);
2020
+ particles.push({ mesh: pMesh, curve, t: j / pCount, speed, direction: j % 2 === 0 ? 1 : -1, isIdle: traffic === 0 });
2021
+ }
2022
+ }
2023
+
2024
+ // ── Auto-rotate pause ────────────────────────────
2025
+ let lastInteraction = 0;
2026
+ const onInteract = () => { lastInteraction = Date.now(); controls.autoRotate = false; };
2027
+ renderer.domElement.addEventListener('pointerdown', onInteract);
2028
+ renderer.domElement.addEventListener('wheel', onInteract);
2029
+
2030
+ // ── Animation loop ───────────────────────────────
2031
+ const clock = new THREE.Clock();
2032
+ let animId;
2033
+
2034
+ function animate() {
2035
+ animId = requestAnimationFrame(animate);
2036
+ clock.getDelta();
2037
+ const t = clock.getElapsedTime();
2038
+
2039
+ if (!controls.autoRotate && Date.now() - lastInteraction > 10000) controls.autoRotate = true;
2040
+ controls.update();
2041
+
2042
+ // Edge pulsing
2043
+ for (let i = 0; i < edgeObjs.length; i++) {
2044
+ edgeObjs[i].mesh.material.opacity = 0.3 + 0.2 * Math.sin(t * 2 + i * 1.3);
2045
+ }
2046
+
2047
+ // Glow ring animation on selected bridge
2048
+ for (let i = 0; i < glowRings.length; i++) {
2049
+ const gr = glowRings[i];
2050
+ gr.material.opacity = (i % 2 === 0 ? 0.15 : 0.05) + (i % 2 === 0 ? 0.12 : 0.05) * Math.sin(t * 1.5 + i);
2051
+ gr.rotation.z = t * (i % 2 === 0 ? 0.1 : -0.08);
2052
+ }
2053
+
2054
+ // Particles along edges — idle ones crawl & fade, active ones zip
2055
+ for (const p of particles) {
2056
+ p.t += p.speed * p.direction;
2057
+ if (p.t > 1) p.t -= 1;
2058
+ if (p.t < 0) p.t += 1;
2059
+ const pos = p.curve.getPointAt(Math.max(0, Math.min(1, p.t)));
2060
+ p.mesh.position.copy(pos);
2061
+ if (p.isIdle) {
2062
+ // Slow breathing pulse — barely visible keepalive
2063
+ p.mesh.material.opacity = 0.15 + 0.2 * Math.sin(p.t * Math.PI);
2064
+ } else {
2065
+ p.mesh.material.opacity = 0.4 + 0.5 * Math.sin(p.t * Math.PI);
2066
+ }
2067
+ }
2068
+
2069
+ // Project labels 3D → 2D
2070
+ const rect = renderer.domElement.getBoundingClientRect();
2071
+ for (const lb of labelEls) {
2072
+ const pos = lb.nodeMesh.position.clone();
2073
+ pos.y += (lb.el.classList.contains('center') ? 1.6 : 1.2);
2074
+ pos.project(camera);
2075
+ const sx = (pos.x * 0.5 + 0.5) * rect.width;
2076
+ const sy = (-pos.y * 0.5 + 0.5) * rect.height;
2077
+ lb.el.style.left = sx + 'px';
2078
+ lb.el.style.top = sy + 'px';
2079
+ lb.el.style.display = pos.z > 1 ? 'none' : '';
2080
+ }
2081
+
2082
+ renderer.render(scene, camera);
2083
+ }
2084
+ animate();
2085
+
2086
+ // Resize — reads from mesh3d.container so it works after expand/collapse swap
2087
+ const onResize = () => {
2088
+ const c = mesh3d ? mesh3d.container : container;
2089
+ const nw = c.clientWidth, nh = c.clientHeight;
2090
+ if (nw === 0 || nh === 0) return;
2091
+ camera.aspect = nw / nh;
2092
+ camera.updateProjectionMatrix();
2093
+ renderer.setSize(nw, nh);
2094
+ };
2095
+ window.addEventListener('resize', onResize);
2096
+
2097
+ mesh3d = { renderer, scene, camera, controls, animId, nodes, edges: edgeObjs, particles, labels: labelEls, container, onResize, glowRings };
2098
+ return true;
2099
+ }
2100
+
2101
+ function updateMeshMap3D() {
2102
+ if (!mesh3d) return;
2103
+ const allBridges = [...bridgeData.values()].filter(b => !b._error);
2104
+ // If bridge count changed, full rebuild needed
2105
+ if (allBridges.length !== mesh3d.nodes.length) return;
2106
+ // Update node visuals
2107
+ for (let i = 0; i < mesh3d.nodes.length && i < allBridges.length; i++) {
2108
+ const node = mesh3d.nodes[i];
2109
+ const b = allBridges[i];
2110
+ const isOnline = !b._error;
2111
+ const peerCount = b.peers ? b.peers.connected : 0;
2112
+ if (isOnline && peerCount > 0) {
2113
+ node.mesh.material.color.setHex(0x3fb950);
2114
+ node.mesh.material.emissive.setHex(0x2a5a2a);
2115
+ } else if (isOnline) {
2116
+ node.mesh.material.color.setHex(0xd29922);
2117
+ node.mesh.material.emissive.setHex(0x4a3a10);
2118
+ } else {
2119
+ node.mesh.material.color.setHex(0xf85149);
2120
+ node.mesh.material.emissive.setHex(0x5a2020);
2121
+ }
2122
+ node.bridge = b;
2123
+ // Update label
2124
+ if (mesh3d.labels[i]) {
2125
+ mesh3d.labels[i].el.className = 'mesh3d-label ' + (isOnline ? 'online' : 'offline');
2126
+ }
2127
+ }
2128
+ }
2129
+
2130
+ function destroyMeshMap3D() {
2131
+ if (!mesh3d) return;
2132
+ // Remove overlay if open
2133
+ const overlay = document.getElementById('mesh3dOverlay');
2134
+ if (overlay) overlay.remove();
2135
+ cancelAnimationFrame(mesh3d.animId);
2136
+ window.removeEventListener('resize', mesh3d.onResize);
2137
+ // Dispose geometries and materials
2138
+ mesh3d.scene.traverse(obj => {
2139
+ if (obj.geometry) obj.geometry.dispose();
2140
+ if (obj.material) {
2141
+ if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose());
2142
+ else obj.material.dispose();
2143
+ }
2144
+ });
2145
+ mesh3d.renderer.dispose();
2146
+ // Remove canvas
2147
+ if (mesh3d.renderer.domElement && mesh3d.renderer.domElement.parentNode) {
2148
+ mesh3d.renderer.domElement.parentNode.removeChild(mesh3d.renderer.domElement);
2149
+ }
2150
+ // Remove labels
2151
+ for (const lb of mesh3d.labels) {
2152
+ if (lb.el && lb.el.parentNode) lb.el.parentNode.removeChild(lb.el);
2153
+ }
2154
+ mesh3d = null;
2155
+ }
2156
+
2157
+ function expandMeshMap() {
2158
+ if (!mesh3d) return;
2159
+ // Create overlay
2160
+ const overlay = document.createElement('div');
2161
+ overlay.className = 'mesh3d-overlay';
2162
+ overlay.id = 'mesh3dOverlay';
2163
+ overlay.innerHTML = '<div class="mesh3d-expanded"><button class="mesh3d-close" onclick="collapseMeshMap()">&times;</button></div>';
2164
+ document.body.appendChild(overlay);
2165
+ // Click backdrop to close
2166
+ overlay.addEventListener('click', function(e) { if (e.target === overlay) collapseMeshMap(); });
2167
+ // Move canvas + labels into expanded container
2168
+ const expanded = overlay.querySelector('.mesh3d-expanded');
2169
+ expanded.appendChild(mesh3d.renderer.domElement);
2170
+ for (const lb of mesh3d.labels) {
2171
+ if (lb.el) expanded.appendChild(lb.el);
2172
+ }
2173
+ mesh3d.container = expanded;
2174
+ // Resize renderer to new container
2175
+ setTimeout(() => { mesh3d.onResize(); }, 50);
2176
+ }
2177
+
2178
+ function collapseMeshMap() {
2179
+ const overlay = document.getElementById('mesh3dOverlay');
2180
+ if (!overlay) return;
2181
+ // Move canvas + labels back to inline container
2182
+ const inline = document.getElementById('mesh3dCanvas');
2183
+ if (mesh3d && inline) {
2184
+ inline.appendChild(mesh3d.renderer.domElement);
2185
+ for (const lb of mesh3d.labels) {
2186
+ if (lb.el) inline.appendChild(lb.el);
2187
+ }
2188
+ mesh3d.container = inline;
2189
+ setTimeout(() => { mesh3d.onResize(); }, 50);
2190
+ }
2191
+ overlay.remove();
2192
+ }
2193
+
1689
2194
 
1690
2195
  // ── Mini peer map ──────────────────────────────────
1691
2196
  function renderMiniMap(b) {
package/lib/actions.js CHANGED
@@ -24,7 +24,7 @@ export async function runRegister ({ config, store, log }) {
24
24
  log('step', `Mesh: ${config.meshId}`)
25
25
  log('step', `Capabilities: ${config.capabilities.join(', ')}`)
26
26
 
27
- const { buildRegistrationTx } = await import('../../registry/lib/registration.js')
27
+ const { buildRegistrationTx } = await import('@relay-federation/registry/lib/registration.js')
28
28
  const { BSVNodeClient } = await import('./bsv-node-client.js')
29
29
 
30
30
  // Get UTXOs from local store
@@ -65,7 +65,7 @@ export async function runRegister ({ config, store, log }) {
65
65
 
66
66
  try {
67
67
  // Step 1: Build and broadcast stake bond tx
68
- const { buildStakeBondTx } = await import('../../registry/lib/stake-bond.js')
68
+ const { buildStakeBondTx } = await import('@relay-federation/registry/lib/stake-bond.js')
69
69
  const { MIN_STAKE_SATS } = await import('@relay-federation/common/protocol')
70
70
 
71
71
  log('step', `Building stake bond (${MIN_STAKE_SATS} sats)...`)
@@ -136,7 +136,7 @@ export async function runDeregister ({ config, store, reason = 'shutdown', log }
136
136
  log('step', `Pubkey: ${config.pubkeyHex}`)
137
137
  log('step', `Reason: ${reason}`)
138
138
 
139
- const { buildDeregistrationTx } = await import('../../registry/lib/registration.js')
139
+ const { buildDeregistrationTx } = await import('@relay-federation/registry/lib/registration.js')
140
140
  const { BSVNodeClient } = await import('./bsv-node-client.js')
141
141
 
142
142
  // Get UTXOs from local store
@@ -83,7 +83,6 @@ export class AnchorManager extends EventEmitter {
83
83
 
84
84
  /**
85
85
  * Connect to all configured anchors that we're not already connected to.
86
- * Respects maxPeers — if at capacity, still tries anchors (they're priority).
87
86
  *
88
87
  * @returns {number} Number of new connections initiated
89
88
  */
package/lib/config.js CHANGED
@@ -51,7 +51,6 @@ export async function initConfig (dir = DEFAULT_DIR, opts = {}) {
51
51
  port: 8333,
52
52
  statusPort: 9333,
53
53
  statusSecret: randomBytes(32).toString('hex'),
54
- maxPeers: 20,
55
54
  dataDir: join(dir, 'data'),
56
55
  seedPeers: [],
57
56
  // apps: [
@@ -20,11 +20,9 @@ import { PeerConnection } from './peer-connection.js'
20
20
  export class PeerManager extends EventEmitter {
21
21
  /**
22
22
  * @param {object} [opts]
23
- * @param {number} [opts.maxPeers=20] - Maximum number of peer connections
24
23
  */
25
- constructor (opts = {}) {
24
+ constructor () {
26
25
  super()
27
- this.maxPeers = opts.maxPeers || 20
28
26
  /** @type {Map<string, PeerConnection>} pubkeyHex → PeerConnection */
29
27
  this.peers = new Map()
30
28
  this._server = null
@@ -36,17 +34,13 @@ export class PeerManager extends EventEmitter {
36
34
  * @param {object} peer - Peer from buildPeerList()
37
35
  * @param {string} peer.pubkeyHex
38
36
  * @param {string} peer.endpoint
39
- * @returns {PeerConnection|null} The connection, or null if at capacity
37
+ * @returns {PeerConnection|null} The connection, or null if already connected
40
38
  */
41
39
  connectToPeer (peer) {
42
40
  if (this.peers.has(peer.pubkeyHex)) {
43
41
  return this.peers.get(peer.pubkeyHex)
44
42
  }
45
43
 
46
- if (this.peers.size >= this.maxPeers) {
47
- return null
48
- }
49
-
50
44
  const conn = new PeerConnection({
51
45
  endpoint: peer.endpoint,
52
46
  pubkeyHex: peer.pubkeyHex
@@ -74,11 +68,6 @@ export class PeerManager extends EventEmitter {
74
68
  return null
75
69
  }
76
70
 
77
- if (this.peers.size >= this.maxPeers) {
78
- socket.close()
79
- return null
80
- }
81
-
82
71
  const conn = new PeerConnection({
83
72
  endpoint,
84
73
  pubkeyHex,
@@ -169,7 +158,7 @@ export class PeerManager extends EventEmitter {
169
158
  // If cryptographic handshake is available, use it
170
159
  if (opts.handshake && msg.nonce && Array.isArray(msg.versions)) {
171
160
  const isSeed = opts.seedEndpoints && opts.seedEndpoints.has(msg.endpoint)
172
- const result = opts.handshake.handleHello(msg, isSeed ? null : (opts.registeredPubkeys || null))
161
+ const result = opts.handshake.handleHello(msg, null) // cryptographic handshake is the security layer — no whitelist gate
173
162
  if (result.error) {
174
163
  ws.send(JSON.stringify({ type: 'error', error: result.error }))
175
164
  ws.close()
@@ -119,7 +119,6 @@ export class StatusServer {
119
119
  },
120
120
  peers: {
121
121
  connected: this._peerManager ? this._peerManager.connectedCount() : 0,
122
- max: this._peerManager ? this._peerManager.maxPeers : 0,
123
122
  list: peers
124
123
  },
125
124
  headers: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relay-federation/bridge",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "Bridge server — WebSocket peering, header sync, tx relay, CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "dependencies": {
13
13
  "@bsv/sdk": "^1.10.1",
14
14
  "@relay-federation/common": "^0.1.0",
15
+ "@relay-federation/registry": "^0.1.0",
15
16
  "level": "^10.0.0",
16
17
  "ws": "^8.19.0"
17
18
  },