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,148 @@
1
+ /**
2
+ * System Proxy — set/clear SOCKS proxy and port availability checks.
3
+ *
4
+ * Manages system-level proxy settings so browser/OS traffic goes through V2Ray.
5
+ */
6
+
7
+ import { execFileSync } from 'child_process';
8
+ import { createServer } from 'net';
9
+
10
+ import { _defaultState } from './state.js';
11
+
12
+ // ─── System Proxy (for V2Ray SOCKS5) ─────────────────────────────────────────
13
+
14
+ const WIN_REG = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
15
+
16
+ // Module-level fallback for saved proxy state — survives state object resets.
17
+ // Stores parsed values: { platform, proxyEnable: 0|1, proxyServer: string|null }
18
+ let _savedProxyState = null;
19
+
20
+ /** Parse ProxyEnable REG_DWORD value from reg query output. Returns 0 or 1. */
21
+ function _parseProxyEnable(regOutput) {
22
+ // Output format: " ProxyEnable REG_DWORD 0x1" or "0x0" or "0x00000001"
23
+ const match = regOutput.match(/ProxyEnable\s+REG_DWORD\s+(0x[0-9a-fA-F]+)/);
24
+ if (!match) return 0;
25
+ return parseInt(match[1], 16) !== 0 ? 1 : 0;
26
+ }
27
+
28
+ /** Parse ProxyServer REG_SZ value from reg query output. Returns string or null. */
29
+ function _parseProxyServer(regOutput) {
30
+ const match = regOutput.match(/ProxyServer\s+REG_SZ\s+(.+)/);
31
+ return match ? match[1].trim() : null;
32
+ }
33
+
34
+ /**
35
+ * Set system SOCKS proxy so browser/system traffic goes through V2Ray.
36
+ * Windows: registry (Internet Settings). macOS: networksetup. Linux: gsettings (GNOME).
37
+ *
38
+ * IMPORTANT: Saves the current proxy configuration BEFORE modifying it, so
39
+ * clearSystemProxy() can restore the original state (e.g., corporate proxy).
40
+ */
41
+ export function setSystemProxy(socksPort, state) {
42
+ const _state = state || _defaultState;
43
+ const port = String(Math.floor(Number(socksPort))); // sanitize to numeric string
44
+ try {
45
+ if (process.platform === 'win32') {
46
+ // Save current proxy state BEFORE modifying — restored in clearSystemProxy()
47
+ // This preserves corporate/custom proxies that were configured before Sentinel.
48
+ let proxyEnable = 0;
49
+ let proxyServer = null;
50
+ try {
51
+ const enableOut = execFileSync('reg', ['query', WIN_REG, '/v', 'ProxyEnable'], { encoding: 'utf8', stdio: 'pipe' });
52
+ proxyEnable = _parseProxyEnable(enableOut);
53
+ } catch { /* ProxyEnable not set — defaults to disabled */ }
54
+ try {
55
+ const serverOut = execFileSync('reg', ['query', WIN_REG, '/v', 'ProxyServer'], { encoding: 'utf8', stdio: 'pipe' });
56
+ proxyServer = _parseProxyServer(serverOut);
57
+ } catch { /* ProxyServer not set — no previous proxy server */ }
58
+
59
+ const saved = { platform: 'win32', proxyEnable, proxyServer };
60
+ _state.savedProxyState = saved;
61
+ _savedProxyState = saved; // Module-level fallback
62
+
63
+ // Now set Sentinel's SOCKS proxy
64
+ execFileSync('reg', ['add', WIN_REG, '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', '1', '/f'], { stdio: 'pipe' });
65
+ execFileSync('reg', ['add', WIN_REG, '/v', 'ProxyServer', '/t', 'REG_SZ', '/d', `socks=127.0.0.1:${port}`, '/f'], { stdio: 'pipe' });
66
+ } else if (process.platform === 'darwin') {
67
+ // macOS: set SOCKS proxy on all network services
68
+ const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8', stdio: 'pipe' })
69
+ .split('\n').filter(s => s && !s.startsWith('*') && !s.startsWith('An asterisk'));
70
+ for (const svc of services) {
71
+ try { execFileSync('networksetup', ['-setsocksfirewallproxy', svc, '127.0.0.1', port], { stdio: 'pipe' }); } catch {}
72
+ try { execFileSync('networksetup', ['-setsocksfirewallproxystate', svc, 'on'], { stdio: 'pipe' }); } catch {}
73
+ }
74
+ } else {
75
+ // Linux: GNOME gsettings (most common desktop)
76
+ try {
77
+ execFileSync('gsettings', ['set', 'org.gnome.system.proxy', 'mode', 'manual'], { stdio: 'pipe' });
78
+ execFileSync('gsettings', ['set', 'org.gnome.system.proxy.socks', 'host', '127.0.0.1'], { stdio: 'pipe' });
79
+ execFileSync('gsettings', ['set', 'org.gnome.system.proxy.socks', 'port', port], { stdio: 'pipe' });
80
+ } catch {} // gsettings not available (headless/non-GNOME) — silent no-op
81
+ }
82
+ _state.systemProxy = true;
83
+ } catch (e) { console.warn('[sentinel-sdk] setSystemProxy warning:', e.message); }
84
+ }
85
+
86
+ /**
87
+ * Clear system proxy — restores the ORIGINAL proxy state from before setSystemProxy().
88
+ * If the user had a corporate proxy (ProxyEnable=1 + ProxyServer=...), it is restored.
89
+ * If the user had no proxy (ProxyEnable=0), proxy is disabled and ProxyServer removed.
90
+ * Always call on disconnect/exit. Safe to call multiple times.
91
+ */
92
+ export function clearSystemProxy(state) {
93
+ const _state = state || _defaultState;
94
+ try {
95
+ if (process.platform === 'win32') {
96
+ // Use state-level saved proxy, fall back to module-level backup
97
+ const saved = _state.savedProxyState || _savedProxyState;
98
+
99
+ if (saved?.platform === 'win32' && saved.proxyEnable === 1) {
100
+ // User HAD a proxy enabled before — restore their ProxyEnable + ProxyServer
101
+ execFileSync('reg', ['add', WIN_REG, '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', '1', '/f'], { stdio: 'pipe' });
102
+ if (saved.proxyServer) {
103
+ execFileSync('reg', ['add', WIN_REG, '/v', 'ProxyServer', '/t', 'REG_SZ', '/d', saved.proxyServer, '/f'], { stdio: 'pipe' });
104
+ } else {
105
+ // ProxyEnable was 1 but no ProxyServer — unusual but restore faithfully
106
+ try { execFileSync('reg', ['delete', WIN_REG, '/v', 'ProxyServer', '/f'], { stdio: 'pipe' }); } catch {}
107
+ }
108
+ } else {
109
+ // User had NO proxy before (ProxyEnable=0 or no saved state) — disable
110
+ execFileSync('reg', ['add', WIN_REG, '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', '0', '/f'], { stdio: 'pipe' });
111
+ if (saved?.proxyServer) {
112
+ // Restore original ProxyServer value even if disabled — some apps check it
113
+ execFileSync('reg', ['add', WIN_REG, '/v', 'ProxyServer', '/t', 'REG_SZ', '/d', saved.proxyServer, '/f'], { stdio: 'pipe' });
114
+ } else {
115
+ try { execFileSync('reg', ['delete', WIN_REG, '/v', 'ProxyServer', '/f'], { stdio: 'pipe' }); } catch {} // may not exist
116
+ }
117
+ }
118
+ } else if (process.platform === 'darwin') {
119
+ const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8', stdio: 'pipe' })
120
+ .split('\n').filter(s => s && !s.startsWith('*') && !s.startsWith('An asterisk'));
121
+ for (const svc of services) {
122
+ try { execFileSync('networksetup', ['-setsocksfirewallproxystate', svc, 'off'], { stdio: 'pipe' }); } catch {}
123
+ }
124
+ } else {
125
+ try { execFileSync('gsettings', ['set', 'org.gnome.system.proxy', 'mode', 'none'], { stdio: 'pipe' }); } catch {} // gsettings unavailable — headless/non-GNOME
126
+ }
127
+ } catch (e) { console.warn('[sentinel-sdk] clearSystemProxy warning:', e.message); }
128
+ _state.systemProxy = false;
129
+ _state.savedProxyState = null;
130
+ _savedProxyState = null;
131
+ }
132
+
133
+ // ─── Port Availability ──────────────────────────────────────────────────────
134
+
135
+ /**
136
+ * Check if a port is available. Use this at startup to detect port conflicts
137
+ * from zombie processes (e.g., old server still running on the same port).
138
+ * @param {number} port - Port to check
139
+ * @returns {Promise<boolean>} true if port is free
140
+ */
141
+ export function checkPortFree(port) {
142
+ return new Promise((resolve) => {
143
+ const server = createServer();
144
+ server.once('error', () => resolve(false));
145
+ server.once('listening', () => { server.close(() => resolve(true)); });
146
+ server.listen(port, '127.0.0.1');
147
+ });
148
+ }
@@ -0,0 +1,428 @@
1
+ /**
2
+ * Connection Resilience — circuit breaker, auto-reconnect, fast reconnect.
3
+ *
4
+ * Handles node failure tracking and automatic recovery from dropped connections.
5
+ */
6
+
7
+ import axios from 'axios';
8
+ import { writeFileSync, mkdirSync, unlinkSync } from 'fs';
9
+ import { spawn } from 'child_process';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ import {
14
+ events, _defaultState, progress, checkAborted,
15
+ cachedCreateWallet, _endSessionOnChain, getStatus,
16
+ } from './state.js';
17
+
18
+ import { queryNode } from '../cosmjs-setup.js';
19
+ import { nodeStatusV3, buildV2RayClientConfig, waitForPort } from '../v3protocol.js';
20
+ import { installWgTunnel, disconnectWireGuard, WG_AVAILABLE } from '../wireguard.js';
21
+ import { writeWgConfig } from '../v3protocol.js';
22
+ import { resolveSpeedtestIPs } from '../speedtest.js';
23
+ import {
24
+ saveState, clearState, saveCredentials, loadCredentials, clearCredentials,
25
+ } from '../state.js';
26
+ import {
27
+ DEFAULT_LCD, DEFAULT_TIMEOUTS, sleep, resolveDnsServers,
28
+ } from '../defaults.js';
29
+ import { findV2RayExe } from './tunnel.js';
30
+ import { enableKillSwitch, isKillSwitchEnabled as _isKillSwitchEnabled } from './security.js';
31
+ import { setSystemProxy, clearSystemProxy, checkPortFree } from './proxy.js';
32
+ import { connectAuto } from './connect.js';
33
+
34
+ // ─── Circuit Breaker ─────────────────────────────────────────────────────────
35
+ // v22: Skip nodes that repeatedly fail. Resets after TTL expires.
36
+ // v25: Configurable threshold/TTL via configureCircuitBreaker().
37
+
38
+ const _circuitBreaker = new Map(); // address -> { count, lastFail }
39
+ let _cbTtl = 5 * 60_000; // default 5 minutes
40
+ let _cbThreshold = 3; // default 3 failures before tripping
41
+
42
+ export function recordNodeFailure(address) {
43
+ const entry = _circuitBreaker.get(address) || { count: 0, lastFail: 0 };
44
+ entry.count++;
45
+ entry.lastFail = Date.now();
46
+ _circuitBreaker.set(address, entry);
47
+ }
48
+
49
+ export function isCircuitOpen(address) {
50
+ const entry = _circuitBreaker.get(address);
51
+ if (!entry) return false;
52
+ if (Date.now() - entry.lastFail > _cbTtl) {
53
+ _circuitBreaker.delete(address);
54
+ return false;
55
+ }
56
+ return entry.count >= _cbThreshold;
57
+ }
58
+
59
+ export function resetCircuitBreaker(address) {
60
+ if (address) _circuitBreaker.delete(address);
61
+ else _circuitBreaker.clear();
62
+ }
63
+
64
+ /**
65
+ * Configure circuit breaker thresholds globally.
66
+ * @param {{ threshold?: number, ttlMs?: number }} opts
67
+ */
68
+ export function configureCircuitBreaker(opts = {}) {
69
+ if (opts.threshold != null) _cbThreshold = Math.max(1, Math.floor(opts.threshold));
70
+ if (opts.ttlMs != null) _cbTtl = Math.max(1000, Math.floor(opts.ttlMs));
71
+ }
72
+
73
+ /**
74
+ * Get circuit breaker status for observability.
75
+ * @param {string} [address] - Specific node, or omit for all.
76
+ * @returns {object} Status per node: { count, lastFail, isOpen }
77
+ */
78
+ export function getCircuitBreakerStatus(address) {
79
+ if (address) {
80
+ const entry = _circuitBreaker.get(address);
81
+ if (!entry) return null;
82
+ return { count: entry.count, lastFail: entry.lastFail, isOpen: isCircuitOpen(address) };
83
+ }
84
+ const result = {};
85
+ for (const [addr, entry] of _circuitBreaker) {
86
+ result[addr] = { count: entry.count, lastFail: entry.lastFail, isOpen: isCircuitOpen(addr) };
87
+ }
88
+ return result;
89
+ }
90
+
91
+ /** Clear circuit breaker entry for a specific node (on successful connect). */
92
+ export function clearCircuitBreaker(address) {
93
+ _circuitBreaker.delete(address);
94
+ }
95
+
96
+ // ─── Fast Reconnect (Credential Cache) ───────────────────────────────────────
97
+
98
+ /**
99
+ * Attempt fast reconnect using saved credentials. Skips payment and handshake.
100
+ * Returns null if no saved credentials, session expired, or tunnel setup fails.
101
+ *
102
+ * @param {object} opts - Same as connectDirect options
103
+ * @param {ConnectionState} [state] - Connection state instance
104
+ * @returns {Promise<object|null>} Connection result or null
105
+ */
106
+ export async function tryFastReconnect(opts, state = _defaultState) {
107
+ const saved = loadCredentials(opts.nodeAddress);
108
+ if (!saved) return null;
109
+
110
+ const onProgress = opts.onProgress || null;
111
+ const logFn = opts.log || console.log;
112
+ const fullTunnel = opts.fullTunnel !== false;
113
+ const killSwitch = opts.killSwitch === true;
114
+ const systemProxy = opts.systemProxy === true;
115
+
116
+ progress(onProgress, logFn, 'cache', `Found saved credentials for ${opts.nodeAddress}, verifying session...`);
117
+
118
+ // Verify session is still active on chain
119
+ try {
120
+ const lcd = opts.lcdUrl || DEFAULT_LCD;
121
+ const { wallet, account } = await cachedCreateWallet(opts.mnemonic);
122
+ const { findExistingSession } = await import('../cosmjs-setup.js');
123
+ const existingSession = await findExistingSession(lcd, account.address, opts.nodeAddress);
124
+ if (!existingSession || String(existingSession) !== saved.sessionId) {
125
+ clearCredentials(opts.nodeAddress);
126
+ progress(onProgress, logFn, 'cache', 'Saved session expired — proceeding with fresh payment');
127
+ return null;
128
+ }
129
+ } catch (err) {
130
+ // Chain query failed — can't verify session, fall back to normal flow
131
+ progress(onProgress, logFn, 'cache', `Session verification failed (${err.message}) — proceeding with fresh payment`);
132
+ clearCredentials(opts.nodeAddress);
133
+ return null;
134
+ }
135
+
136
+ progress(onProgress, logFn, 'cache', `Session ${saved.sessionId} still active — skipping payment and handshake`);
137
+
138
+ try {
139
+ if (saved.serviceType === 'wireguard') {
140
+ // Validate tunnel requirements
141
+ if (!WG_AVAILABLE) {
142
+ clearCredentials(opts.nodeAddress);
143
+ return null;
144
+ }
145
+
146
+ // Resolve split IPs
147
+ let resolvedSplitIPs = null;
148
+ if (opts.splitIPs && Array.isArray(opts.splitIPs) && opts.splitIPs.length > 0) {
149
+ resolvedSplitIPs = opts.splitIPs;
150
+ } else if (fullTunnel) {
151
+ resolvedSplitIPs = null;
152
+ } else {
153
+ try { resolvedSplitIPs = await resolveSpeedtestIPs(); } catch { resolvedSplitIPs = null; }
154
+ }
155
+
156
+ const confPath = writeWgConfig(
157
+ Buffer.from(saved.wgPrivateKey, 'base64'),
158
+ saved.wgAssignedAddrs,
159
+ saved.wgServerPubKey,
160
+ saved.wgServerEndpoint,
161
+ resolvedSplitIPs,
162
+ { dns: resolveDnsServers(opts.dns) },
163
+ );
164
+
165
+ progress(onProgress, logFn, 'tunnel', 'Installing WireGuard tunnel from cached credentials...');
166
+ const installDelays = [1500, 1500, 2000];
167
+ let tunnelInstalled = false;
168
+ for (let i = 0; i < installDelays.length; i++) {
169
+ await sleep(installDelays[i]);
170
+ try {
171
+ await installWgTunnel(confPath);
172
+ state.wgTunnel = 'wgsent0';
173
+ tunnelInstalled = true;
174
+ break;
175
+ } catch (installErr) {
176
+ if (i === installDelays.length - 1) throw installErr;
177
+ }
178
+ }
179
+
180
+ // Verify connectivity
181
+ progress(onProgress, logFn, 'verify', 'Verifying tunnel connectivity...');
182
+ const { verifyWgConnectivity } = await import('./tunnel.js');
183
+ const tunnelWorks = await verifyWgConnectivity();
184
+ if (!tunnelWorks) {
185
+ try { await disconnectWireGuard(); } catch {}
186
+ state.wgTunnel = null;
187
+ clearCredentials(opts.nodeAddress);
188
+ return null;
189
+ }
190
+
191
+ if (killSwitch) {
192
+ try { enableKillSwitch(saved.wgServerEndpoint); } catch {}
193
+ }
194
+
195
+ progress(onProgress, logFn, 'verify', 'WireGuard reconnected from cached credentials!');
196
+ const sessionIdStr = saved.sessionId;
197
+ saveState({ sessionId: sessionIdStr, serviceType: 'wireguard', wgTunnelName: 'wgsent0', confPath, systemProxySet: false });
198
+ state.connection = { sessionId: sessionIdStr, serviceType: 'wireguard', nodeAddress: opts.nodeAddress, connectedAt: Date.now() };
199
+ events.emit('connected', { sessionId: BigInt(sessionIdStr), serviceType: 'wireguard', nodeAddress: opts.nodeAddress, cached: true });
200
+ return {
201
+ sessionId: sessionIdStr,
202
+ serviceType: 'wireguard',
203
+ nodeAddress: opts.nodeAddress,
204
+ confPath,
205
+ cached: true,
206
+ cleanup: async () => {
207
+ if (_isKillSwitchEnabled()) {
208
+ const { disableKillSwitch } = await import('./security.js');
209
+ disableKillSwitch();
210
+ }
211
+ try { await disconnectWireGuard(); } catch {}
212
+ // End session on chain (fire-and-forget)
213
+ if (saved.sessionId && state._mnemonic) {
214
+ _endSessionOnChain(saved.sessionId, state._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
215
+ }
216
+ state.wgTunnel = null;
217
+ state.connection = null;
218
+ state._mnemonic = null;
219
+ clearState();
220
+ },
221
+ };
222
+
223
+ } else if (saved.serviceType === 'v2ray') {
224
+ const v2rayExePath = findV2RayExe(opts.v2rayExePath);
225
+ if (!v2rayExePath) {
226
+ clearCredentials(opts.nodeAddress);
227
+ return null;
228
+ }
229
+
230
+ // Fetch node info to get serverHost
231
+ const nodeInfo = await queryNode(opts.nodeAddress, { lcdUrl: opts.lcdUrl || DEFAULT_LCD });
232
+ const serverHost = new URL(nodeInfo.remote_url).hostname;
233
+
234
+ // Rebuild V2Ray config from saved metadata
235
+ // Sequential increment from random start avoids repeated collisions
236
+ // with TIME_WAIT ports that pure random retries can hit.
237
+ const startPort1 = 10800 + Math.floor(Math.random() * 1000);
238
+ let socksPort = startPort1;
239
+ for (let i = 0; i < 5; i++) {
240
+ socksPort = startPort1 + i;
241
+ if (await checkPortFree(socksPort)) break;
242
+ }
243
+ const config = buildV2RayClientConfig(serverHost, saved.v2rayConfig, saved.v2rayUuid, socksPort, { dns: resolveDnsServers(opts.dns), systemProxy: opts.systemProxy === true });
244
+
245
+ const tmpDir = path.join(os.tmpdir(), 'sentinel-v2ray');
246
+ mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
247
+ const cfgPath = path.join(tmpDir, 'config.json');
248
+
249
+ let workingOutbound = null;
250
+ for (const ob of config.outbounds) {
251
+ if (state.v2rayProc) {
252
+ state.v2rayProc.kill();
253
+ state.v2rayProc = null;
254
+ await sleep(2000);
255
+ }
256
+
257
+ const attempt = {
258
+ ...config,
259
+ outbounds: [ob],
260
+ routing: {
261
+ domainStrategy: 'IPIfNonMatch',
262
+ rules: [
263
+ { inboundTag: ['api'], outboundTag: 'api', type: 'field' },
264
+ { inboundTag: ['proxy'], outboundTag: ob.tag, type: 'field' },
265
+ ],
266
+ },
267
+ };
268
+
269
+ writeFileSync(cfgPath, JSON.stringify(attempt, null, 2), { mode: 0o600 });
270
+ const proc = spawn(v2rayExePath, ['run', '-config', cfgPath], { stdio: 'pipe' });
271
+ // Filter V2Ray stderr noise (fast reconnect path)
272
+ if (proc.stderr) {
273
+ proc.stderr.on('data', (chunk) => {
274
+ const lines = chunk.toString().split('\n');
275
+ for (const line of lines) {
276
+ const trimmed = line.trim();
277
+ if (!trimmed || trimmed.includes('insufficient header')) continue;
278
+ logFn?.(`[v2ray stderr] ${trimmed}`);
279
+ }
280
+ });
281
+ }
282
+ setTimeout(() => { try { unlinkSync(cfgPath); } catch {} }, 2000);
283
+
284
+ const ready = await waitForPort(socksPort, DEFAULT_TIMEOUTS.v2rayReady);
285
+ if (!ready || proc.exitCode !== null) {
286
+ proc.kill();
287
+ continue;
288
+ }
289
+
290
+ // Test SOCKS5 connectivity
291
+ let connected = false;
292
+ try {
293
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
294
+ const auth = config._socksAuth;
295
+ const proxyUrl = (auth?.user && auth?.pass)
296
+ ? `socks5://${auth.user}:${auth.pass}@127.0.0.1:${socksPort}`
297
+ : `socks5://127.0.0.1:${socksPort}`;
298
+ const agent = new SocksProxyAgent(proxyUrl);
299
+ try {
300
+ await axios.get('https://www.google.com', { httpAgent: agent, httpsAgent: agent, timeout: 10000, maxRedirects: 2, validateStatus: () => true });
301
+ connected = true;
302
+ } catch {} finally { agent.destroy(); }
303
+ } catch {}
304
+
305
+ if (connected) {
306
+ workingOutbound = ob;
307
+ state.v2rayProc = proc;
308
+ break;
309
+ }
310
+ proc.kill();
311
+ }
312
+
313
+ if (!workingOutbound) {
314
+ clearCredentials(opts.nodeAddress);
315
+ return null;
316
+ }
317
+
318
+ if (systemProxy && socksPort) {
319
+ setSystemProxy(socksPort, state);
320
+ }
321
+
322
+ progress(onProgress, logFn, 'verify', 'V2Ray reconnected from cached credentials!');
323
+ const sessionIdStr = saved.sessionId;
324
+ saveState({ sessionId: sessionIdStr, serviceType: 'v2ray', v2rayPid: state.v2rayProc?.pid, socksPort, systemProxySet: state.systemProxy, nodeAddress: opts.nodeAddress });
325
+ state.connection = { sessionId: sessionIdStr, serviceType: 'v2ray', nodeAddress: opts.nodeAddress, socksPort, connectedAt: Date.now() };
326
+ events.emit('connected', { sessionId: BigInt(sessionIdStr), serviceType: 'v2ray', nodeAddress: opts.nodeAddress, cached: true });
327
+ return {
328
+ sessionId: sessionIdStr,
329
+ serviceType: 'v2ray',
330
+ nodeAddress: opts.nodeAddress,
331
+ socksPort,
332
+ outbound: workingOutbound.tag,
333
+ cached: true,
334
+ cleanup: async () => {
335
+ if (state.v2rayProc) { state.v2rayProc.kill(); state.v2rayProc = null; await sleep(500); }
336
+ if (state.systemProxy) clearSystemProxy(state);
337
+ // End session on chain (fire-and-forget)
338
+ if (sessionIdStr && state._mnemonic) {
339
+ _endSessionOnChain(sessionIdStr, state._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
340
+ }
341
+ state.connection = null;
342
+ state._mnemonic = null;
343
+ clearState();
344
+ },
345
+ };
346
+ }
347
+ } catch (err) {
348
+ // Fast reconnect failed — clear stale credentials, fall back to normal flow
349
+ progress(onProgress, logFn, 'cache', `Fast reconnect failed (${err.message}) — falling back to normal flow`);
350
+ clearCredentials(opts.nodeAddress);
351
+ return null;
352
+ }
353
+
354
+ return null;
355
+ }
356
+
357
+ // ─── Auto-Reconnect (v26c) ───────────────────────────────────────────────────
358
+
359
+ /**
360
+ * Monitor connection and auto-reconnect on failure.
361
+ * Returns an object with .stop() to cancel monitoring.
362
+ *
363
+ * @param {object} opts - Same as connectAuto() options, plus:
364
+ * @param {number} [opts.pollIntervalMs=5000] - Health check interval
365
+ * @param {number} [opts.maxRetries=5] - Max consecutive reconnect attempts
366
+ * @param {number[]} [opts.backoffMs=[1000,2000,5000,10000,30000]] - Backoff delays
367
+ * @param {function} [opts.onReconnecting] - (attempt: number) => void
368
+ * @param {function} [opts.onReconnected] - (result: ConnectResult) => void
369
+ * @param {function} [opts.onGaveUp] - (errors: Error[]) => void
370
+ * @returns {{ stop: () => void }}
371
+ */
372
+ export function autoReconnect(opts) {
373
+ const pollMs = opts.pollIntervalMs || 5000;
374
+ const maxRetries = opts.maxRetries || 5;
375
+ const backoff = opts.backoffMs || [1000, 2000, 5000, 10000, 30000];
376
+ let wasConnected = false;
377
+ let retries = 0;
378
+ let timer = null;
379
+ let stopped = false;
380
+
381
+ const check = async () => {
382
+ if (stopped) return;
383
+ const status = getStatus();
384
+ const connected = !!status; // v28 fix: getStatus() returns null when disconnected, not { connected: false }
385
+
386
+ if (connected) {
387
+ wasConnected = true;
388
+ retries = 0;
389
+ return;
390
+ }
391
+
392
+ if (!wasConnected) return; // never connected yet, don't auto-reconnect
393
+
394
+ // Lost connection — attempt reconnect
395
+ if (retries >= maxRetries) {
396
+ if (opts.onGaveUp) try { opts.onGaveUp([]); } catch {}
397
+ return;
398
+ }
399
+
400
+ retries++;
401
+ if (opts.onReconnecting) try { opts.onReconnecting(retries); } catch {}
402
+
403
+ const delay = backoff[Math.min(retries - 1, backoff.length - 1)];
404
+ await sleep(delay);
405
+
406
+ if (stopped) return;
407
+ try {
408
+ const result = await connectAuto(opts);
409
+ retries = 0;
410
+ wasConnected = true;
411
+ if (opts.onReconnected) try { opts.onReconnected(result); } catch {}
412
+ } catch (err) {
413
+ // Don't count lock contention or aborts as real failures
414
+ if (err?.code === 'ALREADY_CONNECTED' || err?.code === 'ABORTED') {
415
+ retries--; // undo the increment — not a real connection failure
416
+ return;
417
+ }
418
+ events.emit('error', err);
419
+ }
420
+ };
421
+
422
+ timer = setInterval(check, pollMs);
423
+ if (timer.unref) timer.unref();
424
+
425
+ return {
426
+ stop: () => { stopped = true; if (timer) { clearInterval(timer); timer = null; } },
427
+ };
428
+ }