blue-js-sdk 2.0.0

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.
Files changed (215) hide show
  1. package/CHANGELOG.md +446 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -0
  4. package/ai-path/ADMIN-ELEVATION.md +116 -0
  5. package/ai-path/AI-MANIFESTO.md +185 -0
  6. package/ai-path/BREAKING.md +74 -0
  7. package/ai-path/CHECKLIST.md +619 -0
  8. package/ai-path/CONNECTION-STEPS.md +724 -0
  9. package/ai-path/DECISION-TREE.md +378 -0
  10. package/ai-path/DEPENDENCIES.md +459 -0
  11. package/ai-path/E2E-FLOW.md +1555 -0
  12. package/ai-path/FAILURES.md +403 -0
  13. package/ai-path/GUIDE.md +1217 -0
  14. package/ai-path/README.md +558 -0
  15. package/ai-path/SPLIT-TUNNEL.md +266 -0
  16. package/ai-path/cli.js +535 -0
  17. package/ai-path/connect.js +884 -0
  18. package/ai-path/discover.js +178 -0
  19. package/ai-path/environment.js +266 -0
  20. package/ai-path/errors.js +86 -0
  21. package/ai-path/examples/autonomous-agent.mjs +220 -0
  22. package/ai-path/examples/multi-region.mjs +174 -0
  23. package/ai-path/examples/one-shot.mjs +31 -0
  24. package/ai-path/index.js +60 -0
  25. package/ai-path/pricing.js +136 -0
  26. package/ai-path/recommend.js +413 -0
  27. package/ai-path/run-admin.vbs +25 -0
  28. package/ai-path/setup.js +291 -0
  29. package/ai-path/wallet.js +137 -0
  30. package/app-helpers.js +363 -0
  31. package/app-settings.js +95 -0
  32. package/app-types.js +267 -0
  33. package/audit.js +847 -0
  34. package/batch.js +293 -0
  35. package/bin/setup.js +376 -0
  36. package/chain/authz.js +109 -0
  37. package/chain/broadcast.js +472 -0
  38. package/chain/client.js +160 -0
  39. package/chain/fee-grants.js +305 -0
  40. package/chain/index.js +891 -0
  41. package/chain/lcd.js +313 -0
  42. package/chain/queries.js +547 -0
  43. package/chain/rpc.js +408 -0
  44. package/chain/wallet.js +141 -0
  45. package/cli/config.js +143 -0
  46. package/cli/index.js +463 -0
  47. package/cli/output.js +182 -0
  48. package/cli.js +491 -0
  49. package/client/index.js +251 -0
  50. package/client.js +271 -0
  51. package/config/index.js +255 -0
  52. package/connection/connect.js +849 -0
  53. package/connection/disconnect.js +180 -0
  54. package/connection/discovery.js +321 -0
  55. package/connection/index.js +76 -0
  56. package/connection/proxy.js +148 -0
  57. package/connection/resilience.js +428 -0
  58. package/connection/security.js +232 -0
  59. package/connection/state.js +369 -0
  60. package/connection/tunnel.js +691 -0
  61. package/consumer.js +132 -0
  62. package/cosmjs-setup.js +1884 -0
  63. package/defaults.js +366 -0
  64. package/disk-cache.js +107 -0
  65. package/dist/client.d.ts +108 -0
  66. package/dist/client.d.ts.map +1 -0
  67. package/dist/client.js +400 -0
  68. package/dist/client.js.map +1 -0
  69. package/dist/index.d.ts +8 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +8 -0
  72. package/dist/index.js.map +1 -0
  73. package/errors/index.js +112 -0
  74. package/errors.js +218 -0
  75. package/examples/README.md +64 -0
  76. package/examples/connect-direct.mjs +106 -0
  77. package/examples/connect-plan.mjs +125 -0
  78. package/examples/error-handling.mjs +109 -0
  79. package/examples/query-nodes.mjs +94 -0
  80. package/examples/wallet-basics.mjs +61 -0
  81. package/generated/amino/amino.ts +9 -0
  82. package/generated/cosmos/base/v1beta1/coin.ts +365 -0
  83. package/generated/cosmos_proto/cosmos.ts +323 -0
  84. package/generated/gogoproto/gogo.ts +9 -0
  85. package/generated/google/protobuf/descriptor.ts +7601 -0
  86. package/generated/google/protobuf/duration.ts +208 -0
  87. package/generated/google/protobuf/timestamp.ts +238 -0
  88. package/generated/sentinel/lease/v1/events.ts +924 -0
  89. package/generated/sentinel/lease/v1/lease.ts +292 -0
  90. package/generated/sentinel/lease/v1/msg.ts +949 -0
  91. package/generated/sentinel/lease/v1/params.ts +164 -0
  92. package/generated/sentinel/node/v3/events.ts +881 -0
  93. package/generated/sentinel/node/v3/msg.ts +1002 -0
  94. package/generated/sentinel/node/v3/node.ts +263 -0
  95. package/generated/sentinel/node/v3/params.ts +183 -0
  96. package/generated/sentinel/plan/v3/events.ts +675 -0
  97. package/generated/sentinel/plan/v3/msg.ts +1191 -0
  98. package/generated/sentinel/plan/v3/plan.ts +283 -0
  99. package/generated/sentinel/provider/v2/events.ts +171 -0
  100. package/generated/sentinel/provider/v2/msg.ts +480 -0
  101. package/generated/sentinel/provider/v2/params.ts +131 -0
  102. package/generated/sentinel/provider/v2/provider.ts +246 -0
  103. package/generated/sentinel/session/v3/events.ts +480 -0
  104. package/generated/sentinel/session/v3/msg.ts +616 -0
  105. package/generated/sentinel/session/v3/params.ts +260 -0
  106. package/generated/sentinel/session/v3/proof.ts +180 -0
  107. package/generated/sentinel/session/v3/session.ts +384 -0
  108. package/generated/sentinel/subscription/v3/events.ts +1181 -0
  109. package/generated/sentinel/subscription/v3/msg.ts +1305 -0
  110. package/generated/sentinel/subscription/v3/params.ts +167 -0
  111. package/generated/sentinel/subscription/v3/subscription.ts +315 -0
  112. package/generated/sentinel/types/v1/bandwidth.ts +124 -0
  113. package/generated/sentinel/types/v1/price.ts +149 -0
  114. package/generated/sentinel/types/v1/renewal.ts +87 -0
  115. package/generated/sentinel/types/v1/status.ts +54 -0
  116. package/generated/typeRegistry.ts +27 -0
  117. package/index.js +486 -0
  118. package/node-connect.js +3015 -0
  119. package/operator.js +134 -0
  120. package/package.json +113 -0
  121. package/plan-operations.js +199 -0
  122. package/preflight.js +352 -0
  123. package/pricing/index.js +262 -0
  124. package/proto/amino/amino.proto +84 -0
  125. package/proto/cosmos/base/v1beta1/coin.proto +61 -0
  126. package/proto/cosmos_proto/cosmos.proto +112 -0
  127. package/proto/gogoproto/gogo.proto +145 -0
  128. package/proto/google/api/annotations.proto +31 -0
  129. package/proto/google/api/http.proto +370 -0
  130. package/proto/google/protobuf/any.proto +106 -0
  131. package/proto/google/protobuf/duration.proto +115 -0
  132. package/proto/google/protobuf/timestamp.proto +145 -0
  133. package/proto/sentinel/lease/v1/events.proto +52 -0
  134. package/proto/sentinel/lease/v1/genesis.proto +15 -0
  135. package/proto/sentinel/lease/v1/lease.proto +25 -0
  136. package/proto/sentinel/lease/v1/msg.proto +62 -0
  137. package/proto/sentinel/lease/v1/params.proto +17 -0
  138. package/proto/sentinel/node/v3/events.proto +50 -0
  139. package/proto/sentinel/node/v3/genesis.proto +15 -0
  140. package/proto/sentinel/node/v3/msg.proto +63 -0
  141. package/proto/sentinel/node/v3/node.proto +27 -0
  142. package/proto/sentinel/node/v3/params.proto +21 -0
  143. package/proto/sentinel/node/v3/querier.proto +63 -0
  144. package/proto/sentinel/plan/v3/events.proto +41 -0
  145. package/proto/sentinel/plan/v3/genesis.proto +21 -0
  146. package/proto/sentinel/plan/v3/msg.proto +83 -0
  147. package/proto/sentinel/plan/v3/plan.proto +32 -0
  148. package/proto/sentinel/plan/v3/querier.proto +53 -0
  149. package/proto/sentinel/provider/v2/events.proto +16 -0
  150. package/proto/sentinel/provider/v2/genesis.proto +15 -0
  151. package/proto/sentinel/provider/v2/msg.proto +35 -0
  152. package/proto/sentinel/provider/v2/params.proto +17 -0
  153. package/proto/sentinel/provider/v2/provider.proto +24 -0
  154. package/proto/sentinel/provider/v3/genesis.proto +15 -0
  155. package/proto/sentinel/provider/v3/params.proto +13 -0
  156. package/proto/sentinel/session/v3/events.proto +30 -0
  157. package/proto/sentinel/session/v3/genesis.proto +15 -0
  158. package/proto/sentinel/session/v3/msg.proto +50 -0
  159. package/proto/sentinel/session/v3/params.proto +25 -0
  160. package/proto/sentinel/session/v3/proof.proto +25 -0
  161. package/proto/sentinel/session/v3/querier.proto +100 -0
  162. package/proto/sentinel/session/v3/session.proto +50 -0
  163. package/proto/sentinel/subscription/v2/allocation.proto +21 -0
  164. package/proto/sentinel/subscription/v2/payout.proto +22 -0
  165. package/proto/sentinel/subscription/v3/events.proto +65 -0
  166. package/proto/sentinel/subscription/v3/genesis.proto +17 -0
  167. package/proto/sentinel/subscription/v3/msg.proto +83 -0
  168. package/proto/sentinel/subscription/v3/params.proto +21 -0
  169. package/proto/sentinel/subscription/v3/subscription.proto +33 -0
  170. package/proto/sentinel/types/v1/bandwidth.proto +19 -0
  171. package/proto/sentinel/types/v1/price.proto +21 -0
  172. package/proto/sentinel/types/v1/renewal.proto +21 -0
  173. package/proto/sentinel/types/v1/status.proto +16 -0
  174. package/protocol/encoding.js +341 -0
  175. package/protocol/events.js +361 -0
  176. package/protocol/handshake.js +297 -0
  177. package/protocol/index.js +15 -0
  178. package/protocol/messages.js +346 -0
  179. package/protocol/plans.js +199 -0
  180. package/protocol/v2ray.js +268 -0
  181. package/protocol/v3.js +723 -0
  182. package/protocol/wireguard.js +125 -0
  183. package/security/index.js +132 -0
  184. package/session-manager.js +329 -0
  185. package/session-tracker.js +80 -0
  186. package/setup.js +376 -0
  187. package/speedtest/index.js +528 -0
  188. package/speedtest.js +567 -0
  189. package/src/client.ts +502 -0
  190. package/src/index.ts +20 -0
  191. package/state/index.js +347 -0
  192. package/state.js +516 -0
  193. package/test-all-chain-ops.js +493 -0
  194. package/test-all-logic.js +199 -0
  195. package/test-all-msg-types.js +292 -0
  196. package/test-every-connection.js +208 -0
  197. package/test-feegrant-connect.js +98 -0
  198. package/test-logic.js +148 -0
  199. package/test-mainnet.js +176 -0
  200. package/test-plan-lifecycle.js +335 -0
  201. package/tls-trust.js +132 -0
  202. package/tsconfig.build.json +20 -0
  203. package/tsconfig.json +34 -0
  204. package/types/chain.d.ts +746 -0
  205. package/types/connection.d.ts +425 -0
  206. package/types/errors.d.ts +174 -0
  207. package/types/index.d.ts +1380 -0
  208. package/types/nodes.d.ts +187 -0
  209. package/types/pricing.d.ts +156 -0
  210. package/types/protocol.d.ts +332 -0
  211. package/types/session.d.ts +236 -0
  212. package/types/settings.d.ts +192 -0
  213. package/v3protocol.js +1053 -0
  214. package/wallet/index.js +153 -0
  215. package/wireguard.js +307 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Disconnect — clean up tunnels, system proxy, kill switch, and session state.
3
+ *
4
+ * Handles graceful and emergency disconnection, cleanup handler registration,
5
+ * and session recovery.
6
+ */
7
+
8
+ import {
9
+ events, _defaultState, _activeStates,
10
+ clearWalletCache, _endSessionOnChain,
11
+ markCleanupRegistered, isCleanupRegistered,
12
+ progress, checkAborted, cachedCreateWallet, _recordMetric,
13
+ setAbortConnect, setConnectLock,
14
+ } from './state.js';
15
+ import { disableKillSwitch, isKillSwitchEnabled, disableDnsLeakPrevention } from './security.js';
16
+ import { clearSystemProxy } from './proxy.js';
17
+ import { killV2RayProc, killOrphanV2Ray, performHandshake, validateTunnelRequirements } from './tunnel.js';
18
+
19
+ import { disconnectWireGuard, emergencyCleanupSync } from '../wireguard.js';
20
+ import { flushSpeedTestDnsCache } from '../speedtest.js';
21
+ import {
22
+ clearState, recoverOrphans, markSessionActive,
23
+ } from '../state.js';
24
+ import { ValidationError, ErrorCodes } from '../errors.js';
25
+ import { DEFAULT_LCD, DEFAULT_TIMEOUTS } from '../defaults.js';
26
+ import { nodeStatusV3 } from '../v3protocol.js';
27
+ import { queryNode, privKeyFromMnemonic } from '../cosmjs-setup.js';
28
+ import { createNodeHttpsAgent } from '../tls-trust.js';
29
+
30
+ // ─── Disconnect ──────────────────────────────────────────────────────────────
31
+
32
+ /**
33
+ * Clean up all active tunnels and system proxy.
34
+ * ALWAYS call this on exit — a stale WireGuard tunnel will kill your internet.
35
+ */
36
+ /** Disconnect a specific state instance (internal). */
37
+ export async function disconnectState(state) {
38
+ // v30: Signal any running connectAuto() retry loop to abort, and release the
39
+ // connection lock so the user can reconnect after disconnect completes.
40
+ setAbortConnect(true);
41
+ setConnectLock(false);
42
+
43
+ const prev = state.connection;
44
+ // v29: try/finally ensures state.connection is ALWAYS cleared, even if
45
+ // disableKillSwitch() or clearSystemProxy() throw. Previously, an exception
46
+ // here left state.connection set → phantom "connected" status (IP leak).
47
+ try {
48
+ if (isKillSwitchEnabled()) {
49
+ try { disableKillSwitch(); } catch (e) { console.warn('[sentinel-sdk] Kill switch disable warning:', e.message); }
50
+ }
51
+ if (state.systemProxy) {
52
+ try { clearSystemProxy(); } catch (e) { console.warn('[sentinel-sdk] System proxy clear warning:', e.message); }
53
+ }
54
+ if (state.v2rayProc) {
55
+ killV2RayProc(state.v2rayProc);
56
+ state.v2rayProc = null;
57
+ }
58
+ if (state.wgTunnel) {
59
+ try { await disconnectWireGuard(); } catch (e) { console.warn('[sentinel-sdk] WireGuard disconnect warning:', e.message); }
60
+ state.wgTunnel = null;
61
+ // v34: Restore DNS to DHCP after WireGuard disconnect.
62
+ // WireGuard config sets DNS (10.8.0.1 or custom). When the adapter is removed,
63
+ // the system DNS may remain changed (observed: Cloudflare 1.1.1.1 persisted after
64
+ // split tunnel test). Always restore to DHCP to prevent DNS leak/persistence.
65
+ try { disableDnsLeakPrevention(); } catch (e) { console.warn('[sentinel-sdk] DNS restore warning:', e.message); }
66
+ }
67
+
68
+ // End session on chain (best-effort, fire-and-forget — never blocks disconnect)
69
+ if (prev?.sessionId && state._mnemonic) {
70
+ _endSessionOnChain(prev.sessionId, state._mnemonic).catch(e => {
71
+ console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
72
+ });
73
+ }
74
+ } finally {
75
+ // ALWAYS clear connection state — even if teardown threw
76
+ state._mnemonic = null;
77
+ state.connection = null;
78
+ clearState();
79
+ clearWalletCache(); // v34: Clear cached wallet objects (private keys) from memory
80
+ flushSpeedTestDnsCache(); // v25: Clear stale DNS cache between connections (#14)
81
+ if (prev) events.emit('disconnected', { nodeAddress: prev.nodeAddress, serviceType: prev.serviceType, reason: 'user' });
82
+ }
83
+ }
84
+
85
+ export async function disconnect() {
86
+ return disconnectState(_defaultState);
87
+ }
88
+
89
+ // ─── Cleanup Registration ───────────────────────────────────────────────────
90
+
91
+ /**
92
+ * Register exit handlers to clean up tunnels on crash/exit.
93
+ * Call this once at app startup.
94
+ */
95
+ export function registerCleanupHandlers() {
96
+ if (isCleanupRegistered()) return; // prevent duplicate handler stacking
97
+ markCleanupRegistered();
98
+ const orphans = recoverOrphans(); // recover state-tracked orphans from crash
99
+ if (orphans?.cleaned?.length) console.log('[sentinel-sdk] Recovered orphans:', orphans.cleaned.join(', '));
100
+ emergencyCleanupSync(); // kill stale tunnels from previous crash
101
+ killOrphanV2Ray(); // kill orphaned v2ray from previous crash
102
+ process.on('exit', () => { if (isKillSwitchEnabled()) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); });
103
+ process.on('SIGINT', () => { if (isKillSwitchEnabled()) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); process.exit(130); });
104
+ process.on('SIGTERM', () => { if (isKillSwitchEnabled()) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); process.exit(143); });
105
+ process.on('uncaughtException', (err) => {
106
+ console.error('Uncaught exception:', err);
107
+ if (isKillSwitchEnabled()) disableKillSwitch();
108
+ clearSystemProxy();
109
+ killOrphanV2Ray();
110
+ emergencyCleanupSync();
111
+ process.exit(1);
112
+ });
113
+ }
114
+
115
+ // ─── Session Recovery (v25) ──────────────────────────────────────────────────
116
+
117
+ /**
118
+ * Retry handshake on an already-paid session. Use when connect fails AFTER payment.
119
+ * The error.details from a failed connect contains { sessionId, nodeAddress } — pass those here.
120
+ *
121
+ * @param {object} opts - Same as connectDirect (mnemonic, v2rayExePath, etc.)
122
+ * @param {string|bigint} opts.sessionId - Session ID from the failed connection error
123
+ * @param {string} opts.nodeAddress - Node address from the failed connection error
124
+ * @returns {Promise<ConnectResult>}
125
+ */
126
+ export async function recoverSession(opts) {
127
+ if (!opts?.sessionId) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.sessionId');
128
+ if (!opts?.nodeAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.nodeAddress');
129
+ if (!opts?.mnemonic) throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'recoverSession requires opts.mnemonic');
130
+
131
+ const logFn = opts.log || console.log;
132
+ const onProgress = opts.onProgress || null;
133
+ const sessionId = BigInt(opts.sessionId);
134
+ const timeouts = { ...DEFAULT_TIMEOUTS, ...opts.timeouts };
135
+ const tlsTrust = opts.tlsTrust || 'tofu';
136
+ const state = opts._state || _defaultState;
137
+
138
+ // Fetch node info
139
+ progress(onProgress, logFn, 'recover', `Recovering session ${sessionId} on ${opts.nodeAddress}...`);
140
+ const nodeAgent = createNodeHttpsAgent(opts.nodeAddress, tlsTrust);
141
+
142
+ // Get node status (we need serviceType and remote URL)
143
+ const lcdUrl = opts.lcdUrl || DEFAULT_LCD;
144
+ const nodeInfo = await queryNode(opts.nodeAddress, { lcdUrl });
145
+
146
+ const status = await nodeStatusV3(nodeInfo.remote_url, nodeAgent);
147
+ const resolvedV2rayPath = validateTunnelRequirements(status.type, opts.v2rayExePath);
148
+
149
+ const privKey = await privKeyFromMnemonic(opts.mnemonic);
150
+ const extremeDrift = status.type === 'v2ray' && status.clockDriftSec !== null && Math.abs(status.clockDriftSec) > 120;
151
+
152
+ try {
153
+ const result = await performHandshake({
154
+ serviceType: status.type,
155
+ remoteUrl: nodeInfo.remote_url,
156
+ serverHost: new URL(nodeInfo.remote_url).hostname,
157
+ sessionId,
158
+ privKey,
159
+ v2rayExePath: resolvedV2rayPath,
160
+ fullTunnel: opts.fullTunnel !== false,
161
+ splitIPs: opts.splitIPs,
162
+ systemProxy: opts.systemProxy === true,
163
+ dns: opts.dns,
164
+ onProgress,
165
+ logFn,
166
+ extremeDrift,
167
+ clockDriftSec: status.clockDriftSec,
168
+ nodeAddress: opts.nodeAddress,
169
+ timeouts,
170
+ signal: opts.signal,
171
+ nodeAgent,
172
+ state,
173
+ });
174
+ markSessionActive(String(sessionId), opts.nodeAddress);
175
+ events.emit('connected', { sessionId, serviceType: status.type, nodeAddress: opts.nodeAddress });
176
+ return result;
177
+ } finally {
178
+ privKey.fill(0);
179
+ }
180
+ }
@@ -0,0 +1,321 @@
1
+ /**
2
+ * Node Discovery — query, fetch, enrich, index, and score nodes.
3
+ *
4
+ * Handles LCD queries for online nodes, caching, quality scoring,
5
+ * and geographic indexing.
6
+ */
7
+
8
+ import {
9
+ fetchActiveNodes, filterNodes, resolveNodeUrl,
10
+ } from '../cosmjs-setup.js';
11
+ import { nodeStatusV3 } from '../v3protocol.js';
12
+ import {
13
+ BROKEN_NODES, tryWithFallback, LCD_ENDPOINTS, LAST_VERIFIED,
14
+ } from '../defaults.js';
15
+
16
+ // ─── Node List Cache ─────────────────────────────────────────────────────────
17
+ // v21: Cache queryOnlineNodes results for 5 minutes. Returns cached results
18
+ // immediately on repeat calls and refreshes in background if stale.
19
+ // v25: Deduplicated concurrent refreshes + flushNodeCache() export.
20
+
21
+ const NODE_CACHE_TTL = 5 * 60_000; // 5 minutes
22
+ let _nodeCache = null; // { nodes, timestamp, key }
23
+ let _inflightRefresh = null; // Promise — prevents duplicate concurrent refreshes
24
+
25
+ /** Clear the node list cache. Next queryOnlineNodes() call will fetch fresh data. */
26
+ export function flushNodeCache() {
27
+ _nodeCache = null;
28
+ _inflightRefresh = null;
29
+ }
30
+
31
+ // ─── Node Quality Scoring ───────────────────────────────────────────────────
32
+
33
+ /**
34
+ * Score a node's expected connection quality (0-100).
35
+ * Based on real success rates from 400+ node tests.
36
+ * Higher = more likely to produce a working tunnel.
37
+ */
38
+ export function scoreNode(status) {
39
+ let score = 50; // baseline
40
+
41
+ // WireGuard is simpler and more reliable than V2Ray
42
+ if (status.type === 'wireguard') score += 20;
43
+
44
+ // Clock drift penalty — VMess fails at >120s, VLess is immune.
45
+ // We can't know VMess vs VLess until handshake, but high drift is still risky.
46
+ if (status.clockDriftSec !== null) {
47
+ const drift = Math.abs(status.clockDriftSec);
48
+ if (drift > 120) score -= 40; // VMess will fail entirely (VLess OK but rare)
49
+ else if (drift > 60) score -= 15;
50
+ else if (drift > 30) score -= 5;
51
+ }
52
+
53
+ // Peer count — fewer peers = less congestion
54
+ if (status.peers !== undefined) {
55
+ if (status.peers === 0) score += 10; // empty node = fast
56
+ else if (status.peers < 5) score += 5;
57
+ else if (status.peers > 20) score -= 10;
58
+ }
59
+
60
+ return Math.max(0, Math.min(100, score));
61
+ }
62
+
63
+ // ─── Query Nodes ─────────────────────────────────────────────────────────────
64
+
65
+ /**
66
+ * Fetch active nodes from LCD and check which are actually online.
67
+ * Returns array sorted by quality score (best first).
68
+ *
69
+ * Built-in quality scoring (from 400+ node tests):
70
+ * - WireGuard nodes scored higher than V2Ray (simpler tunnel, fewer failure modes)
71
+ * - V2Ray with grpc/tls deprioritized (0% success rate in testing)
72
+ * - High clock drift nodes penalized (VMess fails silently at >120s)
73
+ * - Nodes with fewer peers scored higher (less congestion)
74
+ *
75
+ * @param {object} options
76
+ * @param {string} options.lcdUrl - LCD endpoint (default: https://lcd.sentinel.co)
77
+ * @param {string} options.serviceType - Filter: 'wireguard' | 'v2ray' | null (both)
78
+ * @param {number} options.maxNodes - Max nodes to check online status (default: 100)
79
+ * @param {number} options.concurrency - Parallel online checks (default: 20)
80
+ * @param {boolean} options.sort - Sort by quality score, best first (default: true). Set false for random order.
81
+ */
82
+ export async function queryOnlineNodes(options = {}) {
83
+ // v25: waitForFresh skips cache entirely
84
+ if (options.waitForFresh) {
85
+ const nodes = await _queryOnlineNodesImpl(options);
86
+ _nodeCache = { nodes, timestamp: Date.now(), key: `${options.lcdUrl || 'default'}_${options.serviceType || 'all'}_${options.maxNodes || 100}` };
87
+ return nodes;
88
+ }
89
+
90
+ // v21: Node cache — return cached results if fresh, background-refresh if stale
91
+ const cacheKey = `${options.lcdUrl || 'default'}_${options.serviceType || 'all'}_${options.maxNodes || 100}`;
92
+ if (!options.noCache && _nodeCache && _nodeCache.key === cacheKey && Date.now() - _nodeCache.timestamp < NODE_CACHE_TTL) {
93
+ // Cache hit — fire deduplicated background refresh but return instantly
94
+ if (!_inflightRefresh) {
95
+ _inflightRefresh = _queryOnlineNodesImpl(options).then(nodes => {
96
+ _nodeCache = { nodes, timestamp: Date.now(), key: cacheKey };
97
+ }).catch(e => {
98
+ if (typeof console !== 'undefined') console.warn('[sentinel-sdk] Node cache refresh failed:', e.message);
99
+ }).finally(() => { _inflightRefresh = null; });
100
+ }
101
+ return _nodeCache.nodes;
102
+ }
103
+
104
+ // No cache — deduplicate concurrent cold fetches
105
+ if (!_inflightRefresh) {
106
+ _inflightRefresh = _queryOnlineNodesImpl(options).then(nodes => {
107
+ _nodeCache = { nodes, timestamp: Date.now(), key: cacheKey };
108
+ return nodes;
109
+ }).finally(() => { _inflightRefresh = null; });
110
+ }
111
+ const nodes = await _inflightRefresh;
112
+ return nodes || _nodeCache?.nodes || [];
113
+ }
114
+
115
+ async function _queryOnlineNodesImpl(options = {}) {
116
+ const maxNodes = options.maxNodes || 5000; // v25b: raised from 100 — chain has 1000+ nodes
117
+ const concurrency = options.concurrency || 20;
118
+ const shouldSort = options.sort !== false; // default true
119
+ const logFn = options.log || null;
120
+ const brokenAddrs = new Set(BROKEN_NODES.map(n => n.address));
121
+
122
+ // 1. Fetch ALL active nodes from LCD — uses lcdPaginatedSafe (handles broken pagination)
123
+ let nodes = [];
124
+ if (options.lcdUrl) {
125
+ nodes = await fetchActiveNodes(options.lcdUrl);
126
+ } else {
127
+ const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'LCD node list');
128
+ nodes = result;
129
+ }
130
+
131
+ // Resolve remote_addrs → remote_url (LCD v3 returns "IP:PORT" array, not "https://..." string)
132
+ nodes = nodes.map(n => {
133
+ try { n.remote_url = resolveNodeUrl(n); } catch { n.remote_url = null; }
134
+ return n;
135
+ });
136
+
137
+ // Filter: must accept udvpn, must have URL, skip known broken nodes (verified ${LAST_VERIFIED})
138
+ nodes = nodes.filter(n =>
139
+ n.remote_url &&
140
+ !brokenAddrs.has(n.address) &&
141
+ (n.gigabyte_prices || []).some(p => p.denom === 'udvpn')
142
+ );
143
+
144
+ // Warn if maxNodes truncates results
145
+ if (maxNodes < nodes.length && logFn) {
146
+ logFn(`[queryOnlineNodes] Warning: ${nodes.length} nodes on chain, returning ${maxNodes} (capped by maxNodes)`);
147
+ }
148
+
149
+ // Shuffle and limit
150
+ // Fisher-Yates shuffle (unbiased)
151
+ for (let i = nodes.length - 1; i > 0; i--) {
152
+ const j = Math.floor(Math.random() * (i + 1));
153
+ [nodes[i], nodes[j]] = [nodes[j], nodes[i]];
154
+ }
155
+ nodes = nodes.slice(0, maxNodes);
156
+
157
+ // 2. Check online status in parallel batches
158
+ const online = [];
159
+ let probed = 0;
160
+ const onNodeProbed = options.onNodeProbed; // callback: ({ total, probed, online }) => void
161
+ for (let i = 0; i < nodes.length; i += concurrency) {
162
+ const batch = nodes.slice(i, i + concurrency);
163
+ const results = await Promise.allSettled(
164
+ batch.map(async (node) => {
165
+ const status = await nodeStatusV3(node.remote_url);
166
+ if (options.serviceType && status.type !== options.serviceType) return null;
167
+ return {
168
+ address: node.address,
169
+ remoteUrl: node.remote_url,
170
+ serviceType: status.type,
171
+ moniker: status.moniker,
172
+ country: status.location.country,
173
+ city: status.location.city,
174
+ peers: status.peers,
175
+ clockDriftSec: status.clockDriftSec,
176
+ gigabytePrices: node.gigabyte_prices,
177
+ hourlyPrices: node.hourly_prices,
178
+ qualityScore: scoreNode(status),
179
+ };
180
+ })
181
+ );
182
+ for (const r of results) {
183
+ if (r.status === 'fulfilled' && r.value) online.push(r.value);
184
+ }
185
+ probed += batch.length;
186
+ if (onNodeProbed) try { onNodeProbed({ total: nodes.length, probed, online: online.length }); } catch {}
187
+ }
188
+
189
+ // 3. Sort by quality score (best first) unless disabled
190
+ if (shouldSort) {
191
+ online.sort((a, b) => b.qualityScore - a.qualityScore);
192
+ }
193
+
194
+ return online;
195
+ }
196
+
197
+ // ─── Full Node Catalog (LCD only, no per-node status checks) ────────────────
198
+
199
+ /**
200
+ * Fetch ALL active nodes from the LCD. No per-node HTTP checks — instant.
201
+ *
202
+ * Returns every node that accepts udvpn, with LCD data only:
203
+ * address, remote_url, gigabyte_prices, hourly_prices.
204
+ *
205
+ * Use this for: building node lists/maps, country pickers, price comparisons.
206
+ * Use queryOnlineNodes() when you need verified online status + quality scores.
207
+ *
208
+ * @param {object} [options]
209
+ * @param {string} [options.lcdUrl] - LCD endpoint (uses fallback chain if omitted)
210
+ * @returns {Promise<Array>} All active nodes (900+)
211
+ */
212
+ export async function fetchAllNodes(options = {}) {
213
+ let nodes;
214
+ if (options.lcdUrl) {
215
+ nodes = await fetchActiveNodes(options.lcdUrl);
216
+ } else {
217
+ const { result } = await tryWithFallback(
218
+ LCD_ENDPOINTS,
219
+ async (url) => fetchActiveNodes(url),
220
+ 'LCD full node list',
221
+ );
222
+ nodes = result;
223
+ }
224
+
225
+ // Filter: must accept udvpn, must have a resolvable URL
226
+ return nodes.filter(n =>
227
+ n.remote_url &&
228
+ (n.gigabyte_prices || []).some(p => p.denom === 'udvpn')
229
+ );
230
+ }
231
+
232
+ /**
233
+ * Build a geographic index from a node list for instant country/city lookups.
234
+ *
235
+ * Requires enriched nodes (with country/city fields from nodeStatusV3).
236
+ * For LCD-only nodes, call enrichNodes() first.
237
+ *
238
+ * @param {Array} nodes - Array of node objects with country/city fields
239
+ * @returns {{ countries: Object, cities: Object, stats: Object }}
240
+ * - countries: { "Germany": [node, ...], "United States": [...] }
241
+ * - cities: { "Berlin": [node, ...], "New York": [...] }
242
+ * - stats: { totalNodes, totalCountries, totalCities, byCountry: [{country, count}] }
243
+ */
244
+ export function buildNodeIndex(nodes) {
245
+ const countries = {};
246
+ const cities = {};
247
+
248
+ for (const node of nodes) {
249
+ const country = node.country || node.location?.country || 'Unknown';
250
+ const city = node.city || node.location?.city || 'Unknown';
251
+
252
+ if (!countries[country]) countries[country] = [];
253
+ countries[country].push(node);
254
+
255
+ const cityKey = city === 'Unknown' ? `${city} (${country})` : city;
256
+ if (!cities[cityKey]) cities[cityKey] = [];
257
+ cities[cityKey].push(node);
258
+ }
259
+
260
+ // Stats sorted by node count (most nodes first)
261
+ const byCountry = Object.entries(countries)
262
+ .map(([country, nodes]) => ({ country, count: nodes.length }))
263
+ .sort((a, b) => b.count - a.count);
264
+
265
+ return {
266
+ countries,
267
+ cities,
268
+ stats: {
269
+ totalNodes: nodes.length,
270
+ totalCountries: Object.keys(countries).length,
271
+ totalCities: Object.keys(cities).length,
272
+ byCountry,
273
+ },
274
+ };
275
+ }
276
+
277
+ /**
278
+ * Enrich LCD nodes with type/country/city by probing each node's status API.
279
+ *
280
+ * @param {Array} nodes - Raw LCD nodes from fetchAllNodes()
281
+ * @param {object} [options]
282
+ * @param {number} [options.concurrency=30] - Parallel probes
283
+ * @param {function} [options.onProgress] - Callback: ({ total, done, enriched }) => void
284
+ * @returns {Promise<Array>} Enriched nodes with serviceType, country, city, moniker, qualityScore
285
+ */
286
+ export async function enrichNodes(nodes, options = {}) {
287
+ const concurrency = options.concurrency || 30;
288
+ const enriched = [];
289
+ let done = 0;
290
+
291
+ for (let i = 0; i < nodes.length; i += concurrency) {
292
+ const batch = nodes.slice(i, i + concurrency);
293
+ const results = await Promise.allSettled(
294
+ batch.map(async (node) => {
295
+ const status = await nodeStatusV3(node.remote_url);
296
+ return {
297
+ address: node.address,
298
+ remoteUrl: node.remote_url,
299
+ serviceType: status.type,
300
+ moniker: status.moniker,
301
+ country: status.location.country,
302
+ city: status.location.city,
303
+ peers: status.peers,
304
+ clockDriftSec: status.clockDriftSec,
305
+ gigabytePrices: node.gigabyte_prices,
306
+ hourlyPrices: node.hourly_prices,
307
+ qualityScore: scoreNode(status),
308
+ };
309
+ })
310
+ );
311
+ for (const r of results) {
312
+ if (r.status === 'fulfilled' && r.value) enriched.push(r.value);
313
+ }
314
+ done += batch.length;
315
+ if (options.onProgress) {
316
+ try { options.onProgress({ total: nodes.length, done, enriched: enriched.length }); } catch {}
317
+ }
318
+ }
319
+
320
+ return enriched;
321
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Connection Module — barrel re-export of all connection submodules.
3
+ *
4
+ * This is the single entry point for the connection/ directory.
5
+ * node-connect.js re-exports from here for backwards compatibility.
6
+ */
7
+
8
+ // ─── State ───────────────────────────────────────────────────────────────────
9
+ export {
10
+ events,
11
+ ConnectionState,
12
+ _defaultState,
13
+ isConnecting,
14
+ clearWalletCache,
15
+ getConnectionMetrics,
16
+ isConnected,
17
+ getStatus,
18
+ verifyConnection,
19
+ } from './state.js';
20
+
21
+ // ─── Connect ─────────────────────────────────────────────────────────────────
22
+ export {
23
+ connectDirect,
24
+ connectAuto,
25
+ connectViaPlan,
26
+ connectViaSubscription,
27
+ quickConnect,
28
+ createConnectConfig,
29
+ } from './connect.js';
30
+
31
+ // ─── Disconnect ──────────────────────────────────────────────────────────────
32
+ export {
33
+ disconnect,
34
+ disconnectState,
35
+ registerCleanupHandlers,
36
+ recoverSession,
37
+ } from './disconnect.js';
38
+
39
+ // ─── Discovery ───────────────────────────────────────────────────────────────
40
+ export {
41
+ queryOnlineNodes,
42
+ fetchAllNodes,
43
+ enrichNodes,
44
+ buildNodeIndex,
45
+ flushNodeCache,
46
+ } from './discovery.js';
47
+
48
+ // ─── Security ────────────────────────────────────────────────────────────────
49
+ export {
50
+ enableKillSwitch,
51
+ disableKillSwitch,
52
+ isKillSwitchEnabled,
53
+ enableDnsLeakPrevention,
54
+ disableDnsLeakPrevention,
55
+ } from './security.js';
56
+
57
+ // ─── Resilience ──────────────────────────────────────────────────────────────
58
+ export {
59
+ resetCircuitBreaker,
60
+ configureCircuitBreaker,
61
+ getCircuitBreakerStatus,
62
+ autoReconnect,
63
+ tryFastReconnect,
64
+ } from './resilience.js';
65
+
66
+ // ─── Proxy ───────────────────────────────────────────────────────────────────
67
+ export {
68
+ setSystemProxy,
69
+ clearSystemProxy,
70
+ checkPortFree,
71
+ } from './proxy.js';
72
+
73
+ // ─── Tunnel ──────────────────────────────────────────────────────────────────
74
+ export {
75
+ verifyDependencies,
76
+ } from './tunnel.js';