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,232 @@
1
+ /**
2
+ * Security — kill switch, DNS leak prevention.
3
+ *
4
+ * Controls firewall rules and DNS settings to prevent traffic leaks
5
+ * when the VPN tunnel is active.
6
+ */
7
+
8
+ import { execFileSync } from 'child_process';
9
+ import { writeFileSync, unlinkSync } from 'fs';
10
+
11
+ import { _defaultState } from './state.js';
12
+ import { saveState } from '../state.js';
13
+ import { TunnelError } from '../errors.js';
14
+
15
+ // ─── Kill Switch (Firewall / Packet Filter) ────────────────────────────────
16
+
17
+ let _killSwitchEnabled = false;
18
+
19
+ /**
20
+ * Enable kill switch — blocks all non-tunnel traffic.
21
+ * Windows: netsh advfirewall, macOS: pfctl, Linux: iptables.
22
+ * Call after WireGuard tunnel is installed.
23
+ * @param {string} serverEndpoint - WireGuard server "IP:PORT"
24
+ * @param {string} [tunnelName='wgsent0'] - WireGuard interface name
25
+ */
26
+ export function enableKillSwitch(serverEndpoint, tunnelName = 'wgsent0') {
27
+ const [serverIp, serverPort] = serverEndpoint.split(':');
28
+
29
+ if (process.platform === 'win32') {
30
+ // Windows: netsh advfirewall
31
+ // Block all outbound by default
32
+ execFileSync('netsh', ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,blockoutbound'], { stdio: 'pipe' });
33
+
34
+ // Wrap allow rules in try-catch — if any fail after block-all, restore default policy
35
+ // to prevent permanent internet loss from partial firewall state.
36
+ try {
37
+ // Allow tunnel interface
38
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-Tunnel', 'dir=out', `interface=${tunnelName}`, 'action=allow'], { stdio: 'pipe' });
39
+
40
+ // Allow WireGuard endpoint (UDP to server)
41
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-WG-Endpoint', 'dir=out', 'action=allow', 'protocol=udp', `remoteip=${serverIp}`, `remoteport=${serverPort}`], { stdio: 'pipe' });
42
+
43
+ // Allow loopback
44
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-Loopback', 'dir=out', 'action=allow', 'remoteip=127.0.0.1'], { stdio: 'pipe' });
45
+
46
+ // Allow DHCP
47
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-DHCP', 'dir=out', 'action=allow', 'protocol=udp', 'localport=68', 'remoteport=67'], { stdio: 'pipe' });
48
+
49
+ // Allow DNS only through tunnel
50
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Allow-DNS-Tunnel', 'dir=out', 'action=allow', 'protocol=udp', 'remoteip=10.8.0.1', 'remoteport=53'], { stdio: 'pipe' });
51
+
52
+ // Block IPv6 (prevent leaks)
53
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule', 'name=SentinelVPN-Block-IPv6', 'dir=out', 'action=block', 'protocol=any', 'remoteip=::/0'], { stdio: 'pipe' });
54
+ } catch (err) {
55
+ // Emergency restore — unblock outbound so user isn't locked out
56
+ try { execFileSync('netsh', ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,allowoutbound'], { stdio: 'pipe' }); } catch { /* last resort */ }
57
+ _killSwitchEnabled = false;
58
+ throw new TunnelError('KILL_SWITCH_FAILED', `Kill switch failed: ${err.message}`);
59
+ }
60
+
61
+ } else if (process.platform === 'darwin') {
62
+ // macOS: pfctl (packet filter)
63
+ const pfRules = [
64
+ '# Sentinel VPN Kill Switch',
65
+ 'block out all',
66
+ `pass out on ${tunnelName} all`,
67
+ `pass out proto udp from any to ${serverIp} port ${serverPort}`,
68
+ 'pass out on lo0 all',
69
+ 'pass out proto udp from any port 68 to any port 67',
70
+ 'pass out proto udp from any to 10.8.0.1 port 53',
71
+ 'block out inet6 all',
72
+ ].join('\n') + '\n';
73
+
74
+ const pfPath = '/tmp/sentinel-killswitch.conf';
75
+ writeFileSync(pfPath, pfRules, { mode: 0o600 });
76
+
77
+ // Save current pf state for restore
78
+ try { execFileSync('pfctl', ['-sr'], { encoding: 'utf8', stdio: 'pipe' }); } catch { /* may not have existing rules */ }
79
+
80
+ // Load rules and enable pf
81
+ execFileSync('pfctl', ['-f', pfPath], { stdio: 'pipe' });
82
+ execFileSync('pfctl', ['-e'], { stdio: 'pipe' });
83
+
84
+ } else {
85
+ // Linux: iptables
86
+ // Flush existing sentinel rules first
87
+ try { execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
88
+
89
+ // Allow loopback
90
+ execFileSync('iptables', ['-A', 'OUTPUT', '-o', 'lo', '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
91
+
92
+ // Allow tunnel interface
93
+ execFileSync('iptables', ['-A', 'OUTPUT', '-o', tunnelName, '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
94
+
95
+ // Allow WireGuard server endpoint
96
+ execFileSync('iptables', ['-A', 'OUTPUT', '-d', serverIp, '-p', 'udp', '--dport', serverPort, '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
97
+
98
+ // Allow DHCP
99
+ execFileSync('iptables', ['-A', 'OUTPUT', '-p', 'udp', '--sport', '68', '--dport', '67', '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
100
+
101
+ // Allow DNS only through tunnel
102
+ execFileSync('iptables', ['-A', 'OUTPUT', '-d', '10.8.0.1', '-p', 'udp', '--dport', '53', '-j', 'ACCEPT', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
103
+
104
+ // Block everything else
105
+ execFileSync('iptables', ['-A', 'OUTPUT', '-j', 'DROP', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' });
106
+
107
+ // Block IPv6
108
+ try { execFileSync('ip6tables', ['-A', 'OUTPUT', '-j', 'DROP', '-m', 'comment', '--comment', 'sentinel-vpn'], { stdio: 'pipe' }); } catch { /* ip6tables may not be available */ }
109
+ }
110
+
111
+ _killSwitchEnabled = true;
112
+ // Persist kill switch state — survives crash so recoverOrphans() can restore internet
113
+ try {
114
+ const conn = _defaultState.connection || {};
115
+ saveState({ sessionId: conn.sessionId, serviceType: conn.serviceType, nodeAddress: conn.nodeAddress, killSwitchEnabled: true });
116
+ } catch {} // best-effort
117
+ }
118
+
119
+ /**
120
+ * Disable kill switch — restore normal routing.
121
+ * Windows: removes netsh rules, macOS: disables pfctl, Linux: removes iptables rules.
122
+ */
123
+ export function disableKillSwitch() {
124
+ if (!_killSwitchEnabled) return;
125
+
126
+ if (process.platform === 'win32') {
127
+ // Windows: remove firewall rules
128
+ const rules = [
129
+ 'SentinelVPN-Allow-Tunnel',
130
+ 'SentinelVPN-Allow-WG-Endpoint',
131
+ 'SentinelVPN-Allow-Loopback',
132
+ 'SentinelVPN-Allow-DHCP',
133
+ 'SentinelVPN-Allow-DNS-Tunnel',
134
+ 'SentinelVPN-Block-IPv6',
135
+ ];
136
+ for (const rule of rules) {
137
+ try { execFileSync('netsh', ['advfirewall', 'firewall', 'delete', 'rule', `name=${rule}`], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
138
+ }
139
+
140
+ // Restore default outbound policy
141
+ try { execFileSync('netsh', ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,allowoutbound'], { stdio: 'pipe' }); } catch { /* best effort */ }
142
+
143
+ } else if (process.platform === 'darwin') {
144
+ // macOS: disable pf and remove temp rules
145
+ try { execFileSync('pfctl', ['-d'], { stdio: 'pipe' }); } catch { /* pf may already be disabled */ }
146
+ try { unlinkSync('/tmp/sentinel-killswitch.conf'); } catch { /* file may not exist */ }
147
+
148
+ } else {
149
+ // Linux: remove all sentinel-vpn rules
150
+ let hasRules = true;
151
+ while (hasRules) {
152
+ try {
153
+ execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'ACCEPT'], { stdio: 'pipe' });
154
+ } catch {
155
+ hasRules = false;
156
+ }
157
+ }
158
+ try { execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
159
+ try { execFileSync('ip6tables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
160
+ }
161
+
162
+ _killSwitchEnabled = false;
163
+ // Persist cleared kill switch state
164
+ try {
165
+ const conn = _defaultState.connection || {};
166
+ saveState({ sessionId: conn.sessionId, serviceType: conn.serviceType, nodeAddress: conn.nodeAddress, killSwitchEnabled: false });
167
+ } catch {} // best-effort
168
+ }
169
+
170
+ /** Check if kill switch is enabled */
171
+ export function isKillSwitchEnabled() { return _killSwitchEnabled; }
172
+
173
+ // ─── DNS Leak Prevention ────────────────────────────────────────────────────
174
+
175
+ /**
176
+ * Enable DNS leak prevention by forcing all DNS through the VPN tunnel.
177
+ * Windows: netsh interface ipv4 set dnsservers + firewall rules
178
+ * macOS: networksetup -setdnsservers
179
+ * Linux: write /etc/resolv.conf
180
+ * @param {string} [dnsServer='10.8.0.1'] - DNS server inside the tunnel
181
+ * @param {string} [tunnelInterface='wgsent0'] - WireGuard tunnel interface name
182
+ */
183
+ export function enableDnsLeakPrevention(dnsServer = '10.8.0.1', tunnelInterface = 'wgsent0') {
184
+ const platform = process.platform;
185
+ if (platform === 'win32') {
186
+ // Set DNS on all interfaces to tunnel DNS
187
+ execFileSync('netsh', ['interface', 'ipv4', 'set', 'dnsservers', tunnelInterface, 'static', dnsServer, 'primary'], { stdio: 'pipe' });
188
+ // Block DNS on non-tunnel interfaces
189
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule',
190
+ 'name=SentinelDNSBlock', 'dir=out', 'protocol=udp', 'remoteport=53',
191
+ 'action=block'], { stdio: 'pipe' });
192
+ execFileSync('netsh', ['advfirewall', 'firewall', 'add', 'rule',
193
+ 'name=SentinelDNSAllow', 'dir=out', 'protocol=udp', 'remoteport=53',
194
+ 'interface=' + tunnelInterface, 'action=allow'], { stdio: 'pipe' });
195
+ } else if (platform === 'darwin') {
196
+ // macOS: set DNS via networksetup for all services
197
+ const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8' })
198
+ .split('\n').filter(s => s && !s.startsWith('*'));
199
+ for (const svc of services) {
200
+ try { execFileSync('networksetup', ['-setdnsservers', svc.trim(), dnsServer], { stdio: 'pipe' }); } catch { /* best effort */ }
201
+ }
202
+ } else {
203
+ // Linux: backup and overwrite resolv.conf
204
+ try { execFileSync('cp', ['/etc/resolv.conf', '/etc/resolv.conf.sentinel.bak'], { stdio: 'pipe' }); } catch { /* backup may fail if file missing */ }
205
+ writeFileSync('/etc/resolv.conf', `nameserver ${dnsServer}\n`);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Disable DNS leak prevention and restore normal DNS resolution.
211
+ * Windows: removes firewall rules, resets DNS to DHCP
212
+ * macOS: clears DNS overrides
213
+ * Linux: restores /etc/resolv.conf from backup
214
+ */
215
+ export function disableDnsLeakPrevention() {
216
+ const platform = process.platform;
217
+ if (platform === 'win32') {
218
+ try { execFileSync('netsh', ['advfirewall', 'firewall', 'delete', 'rule', 'name=SentinelDNSBlock'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
219
+ try { execFileSync('netsh', ['advfirewall', 'firewall', 'delete', 'rule', 'name=SentinelDNSAllow'], { stdio: 'pipe' }); } catch { /* rule may not exist */ }
220
+ // Reset DNS to DHCP
221
+ try { execFileSync('netsh', ['interface', 'ipv4', 'set', 'dnsservers', 'Wi-Fi', 'dhcp'], { stdio: 'pipe' }); } catch { /* interface may not exist */ }
222
+ try { execFileSync('netsh', ['interface', 'ipv4', 'set', 'dnsservers', 'Ethernet', 'dhcp'], { stdio: 'pipe' }); } catch { /* interface may not exist */ }
223
+ } else if (platform === 'darwin') {
224
+ const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8' })
225
+ .split('\n').filter(s => s && !s.startsWith('*'));
226
+ for (const svc of services) {
227
+ try { execFileSync('networksetup', ['-setdnsservers', svc.trim(), 'empty'], { stdio: 'pipe' }); } catch { /* best effort */ }
228
+ }
229
+ } else {
230
+ try { execFileSync('cp', ['/etc/resolv.conf.sentinel.bak', '/etc/resolv.conf'], { stdio: 'pipe' }); } catch { /* backup may not exist */ }
231
+ }
232
+ }
@@ -0,0 +1,369 @@
1
+ /**
2
+ * Connection State — shared state, event emitter, metrics, wallet cache, and helpers.
3
+ *
4
+ * This module owns all mutable state that other connection modules depend on.
5
+ * Import ConnectionState, _defaultState, events, etc. from here.
6
+ */
7
+
8
+ import { EventEmitter } from 'events';
9
+ import { execFileSync } from 'child_process';
10
+ import axios from 'axios';
11
+ import { sha256 as _sha256 } from '@cosmjs/crypto';
12
+
13
+ import {
14
+ createWallet, createClient, broadcast, buildEndSessionMsg,
15
+ } from '../cosmjs-setup.js';
16
+ import {
17
+ SentinelError, NodeError, ErrorCodes,
18
+ } from '../errors.js';
19
+ import {
20
+ saveState, clearState,
21
+ } from '../state.js';
22
+ import {
23
+ sleep, RPC_ENDPOINTS, tryWithFallback,
24
+ } from '../defaults.js';
25
+
26
+ // ─── Event Emitter ───────────────────────────────────────────────────────────
27
+ // Subscribe to SDK lifecycle events without polling:
28
+ // import { events } from './connection/state.js';
29
+ // events.on('connected', ({ sessionId, serviceType }) => updateUI());
30
+ // events.on('disconnected', ({ reason }) => showNotification());
31
+ // events.on('progress', ({ step, detail }) => updateProgressBar());
32
+
33
+ export const events = new EventEmitter();
34
+
35
+ // ─── Cleanup Safety ──────────────────────────────────────────────────────────
36
+ // Track whether registerCleanupHandlers() has been called. If a developer calls
37
+ // connect() without registering, they risk orphaning WireGuard adapters or V2Ray
38
+ // processes on crash/SIGINT — the "Dead Internet" bug.
39
+ let _cleanupRegistered = false;
40
+
41
+ export function isCleanupRegistered() { return _cleanupRegistered; }
42
+ export function markCleanupRegistered() { _cleanupRegistered = true; }
43
+
44
+ export function warnIfNoCleanup(fnName) {
45
+ if (!_cleanupRegistered) {
46
+ throw new SentinelError(ErrorCodes.INVALID_OPTIONS,
47
+ `${fnName}() called without registerCleanupHandlers(). ` +
48
+ `If your app crashes, WireGuard/V2Ray tunnels will orphan and kill the user's internet. ` +
49
+ `Call registerCleanupHandlers() once at app startup, or use quickConnect() which does it automatically.`
50
+ );
51
+ }
52
+ }
53
+
54
+ // ─── Connection Mutex ─────────────────────────────────────────────────────────
55
+ // v27: Prevent concurrent connection attempts (backported from C# SemaphoreSlim).
56
+ // Only one connect call may be in-flight at a time. quickConnect inherits via connectAuto.
57
+ let _connectLock = false;
58
+
59
+ // v30: Abort flag — disconnect() sets this to stop a running connectAuto() retry loop.
60
+ // Without this, disconnect() clears tunnel state but connectAuto() keeps retrying,
61
+ // paying for new sessions. The user cannot reconnect because _connectLock stays held.
62
+ let _abortConnect = false;
63
+
64
+ /** Check if a connection attempt is currently in progress. */
65
+ export function isConnecting() { return _connectLock; }
66
+ export function getConnectLock() { return _connectLock; }
67
+ export function setConnectLock(v) { _connectLock = v; }
68
+ export function getAbortConnect() { return _abortConnect; }
69
+ export function setAbortConnect(v) { _abortConnect = v; }
70
+
71
+ // ─── Connection State ─────────────────────────────────────────────────────────
72
+ // v22: Encapsulated state enables per-instance connections via SentinelClient.
73
+ // Module-level functions use _defaultState for backward compatibility.
74
+
75
+ // Global registry of active states — used by exit handlers to clean up all instances
76
+ export const _activeStates = new Set();
77
+
78
+ export class ConnectionState {
79
+ constructor() {
80
+ this.v2rayProc = null;
81
+ this.wgTunnel = null;
82
+ this.systemProxy = false;
83
+ this.connection = null; // { nodeAddress, serviceType, sessionId, connectedAt, socksPort? }
84
+ this.savedProxyState = null;
85
+ this._mnemonic = null; // Stored for session-end TX on disconnect (zeroed after use)
86
+ _activeStates.add(this);
87
+ }
88
+ get isConnected() { return !!(this.v2rayProc || this.wgTunnel); }
89
+ destroy() { _activeStates.delete(this); }
90
+ }
91
+
92
+ export const _defaultState = new ConnectionState();
93
+
94
+ // Default logger — can be overridden per-call via opts.log
95
+ export let defaultLog = console.log;
96
+
97
+ // ─── Wallet Cache ────────────────────────────────────────────────────────────
98
+ // v21: Cache wallet derivation (BIP39 → SLIP-10 is CPU-bound, ~300ms).
99
+ // Same mnemonic always produces the same wallet — safe to cache.
100
+ // Keyed by full SHA256 of mnemonic to avoid storing the raw mnemonic.
101
+
102
+ const _walletCache = new Map();
103
+
104
+ export async function cachedCreateWallet(mnemonic) {
105
+ const key = Buffer.from(_sha256(Buffer.from(mnemonic))).toString('hex'); // full SHA256 — no truncation
106
+ if (_walletCache.has(key)) return _walletCache.get(key);
107
+ const result = await createWallet(mnemonic);
108
+ _walletCache.set(key, result);
109
+ return result;
110
+ }
111
+
112
+ /** Clear the wallet derivation cache. Call after disconnect to release key material from memory. */
113
+ export function clearWalletCache() {
114
+ _walletCache.clear();
115
+ }
116
+
117
+ // ─── Connection Metrics (v25) ────────────────────────────────────────────────
118
+ // Track per-node connection stats for reliability tracking over time.
119
+
120
+ const _connectionMetrics = new Map(); // nodeAddress -> { attempts, successes, failures, avgTimeMs, lastAttempt }
121
+
122
+ export function _recordMetric(nodeAddress, success, durationMs) {
123
+ const entry = _connectionMetrics.get(nodeAddress) || { attempts: 0, successes: 0, failures: 0, totalTimeMs: 0, lastAttempt: 0 };
124
+ entry.attempts++;
125
+ if (success) entry.successes++; else entry.failures++;
126
+ entry.totalTimeMs += durationMs || 0;
127
+ entry.lastAttempt = Date.now();
128
+ _connectionMetrics.set(nodeAddress, entry);
129
+ }
130
+
131
+ /**
132
+ * Get connection metrics for observability.
133
+ * @param {string} [nodeAddress] - Specific node, or omit for all.
134
+ * @returns {object} Per-node stats: { attempts, successes, failures, successRate, avgTimeMs, lastAttempt }
135
+ */
136
+ export function getConnectionMetrics(nodeAddress) {
137
+ const format = (entry) => ({
138
+ ...entry,
139
+ successRate: entry.attempts > 0 ? entry.successes / entry.attempts : 0,
140
+ avgTimeMs: entry.attempts > 0 ? Math.round(entry.totalTimeMs / entry.attempts) : 0,
141
+ });
142
+ if (nodeAddress) {
143
+ const entry = _connectionMetrics.get(nodeAddress);
144
+ return entry ? format(entry) : null;
145
+ }
146
+ const result = {};
147
+ for (const [addr, entry] of _connectionMetrics) result[addr] = format(entry);
148
+ return result;
149
+ }
150
+
151
+ // ─── Abort helper ────────────────────────────────────────────────────────────
152
+
153
+ export function checkAborted(signal) {
154
+ if (signal?.aborted) {
155
+ throw new SentinelError(ErrorCodes.ABORTED, 'Connection aborted', { reason: signal.reason });
156
+ }
157
+ }
158
+
159
+ // ─── Progress helper ─────────────────────────────────────────────────────────
160
+
161
+ export function progress(cb, logFn, step, detail, meta = {}) {
162
+ const entry = { event: `sdk.${step}`, detail, ts: Date.now(), ...meta };
163
+ events.emit('progress', entry);
164
+ if (logFn) try { logFn(`[${step}] ${detail}`); } catch {} // user callback may throw — don't crash SDK
165
+ if (cb) try { cb(step, detail, entry); } catch {} // user callback may throw — don't crash SDK
166
+ }
167
+
168
+ // ─── Node Inactive Retry Helper ──────────────────────────────────────────────
169
+ // LCD may show node as active, but chain rejects TX with code 105 ("invalid
170
+ // status inactive") if the node went offline between query and payment.
171
+ // Retry once after 15s in case LCD data was stale.
172
+
173
+ export function _isNodeInactiveError(err) {
174
+ const msg = String(err?.message || '');
175
+ const code = err?.details?.code;
176
+ return msg.includes('invalid status inactive') || code === 105;
177
+ }
178
+
179
+ export async function broadcastWithInactiveRetry(client, address, msgs, logFn, onProgress) {
180
+ try {
181
+ return await broadcast(client, address, msgs);
182
+ } catch (err) {
183
+ if (_isNodeInactiveError(err)) {
184
+ progress(onProgress, logFn, 'session', 'Node reported inactive (code 105) — LCD stale data. Retrying in 15s...');
185
+ await sleep(15000);
186
+ try {
187
+ return await broadcast(client, address, msgs);
188
+ } catch (retryErr) {
189
+ if (_isNodeInactiveError(retryErr)) {
190
+ throw new NodeError(ErrorCodes.NODE_INACTIVE, 'Node went inactive between query and payment (code 105). LCD stale data confirmed after retry.', {
191
+ original: retryErr.message,
192
+ code: 105,
193
+ });
194
+ }
195
+ throw retryErr;
196
+ }
197
+ }
198
+ throw err;
199
+ }
200
+ }
201
+
202
+ // ─── Uptime Formatter ────────────────────────────────────────────────────────
203
+
204
+ export function formatUptime(ms) {
205
+ const s = Math.floor(ms / 1000);
206
+ const h = Math.floor(s / 3600);
207
+ const m = Math.floor((s % 3600) / 60);
208
+ if (h > 0) return `${h}h ${m}m`;
209
+ if (m > 0) return `${m}m ${s % 60}s`;
210
+ return `${s}s`;
211
+ }
212
+
213
+ // ─── Session End (on-chain cleanup) ──────────────────────────────────────────
214
+
215
+ /**
216
+ * End a session on-chain. Best-effort, fire-and-forget.
217
+ * Prevents stale session accumulation on nodes.
218
+ * @param {string|bigint} sessionId - Session ID to end
219
+ * @param {string} mnemonic - BIP39 mnemonic for signing the TX
220
+ * @private
221
+ */
222
+ export async function _endSessionOnChain(sessionId, mnemonic) {
223
+ const { wallet, account } = await cachedCreateWallet(mnemonic);
224
+ const client = await tryWithFallback(
225
+ RPC_ENDPOINTS,
226
+ async (url) => createClient(url, wallet),
227
+ 'RPC connect (session end)',
228
+ ).then(r => r.result);
229
+ const msg = buildEndSessionMsg(account.address, sessionId);
230
+ const fee = { amount: [{ denom: 'udvpn', amount: '20000' }], gas: '200000' };
231
+ const result = await client.signAndBroadcast(account.address, [msg], fee);
232
+ if (result.code !== 0) {
233
+ console.warn(`[sentinel-sdk] End session TX failed (code ${result.code}): ${result.rawLog}`);
234
+ } else {
235
+ console.log(`[sentinel-sdk] Session ${sessionId} ended on chain (TX ${result.transactionHash})`);
236
+ }
237
+ }
238
+
239
+ // ─── Connection Status (VPN UX: user must always know if they're connected) ─
240
+
241
+ /**
242
+ * Check if a VPN tunnel is currently active.
243
+ * Use this to show connected/disconnected state in UI — like the VPN icon.
244
+ */
245
+ export function isConnected() {
246
+ return _defaultState.isConnected;
247
+ }
248
+
249
+ /**
250
+ * Get current connection status. Returns null if disconnected.
251
+ * Apps should poll this (e.g. every 5s) to update UI — like NordVPN's status bar.
252
+ * v25: Includes healthChecks for tunnel/proxy liveness.
253
+ */
254
+ export function getStatus() {
255
+ if (!_defaultState.connection) return null;
256
+
257
+ // v29: Cross-check tunnel liveness FIRST — if connection object exists but neither
258
+ // tunnel handle is truthy, the state is phantom (tunnel torn down, connection stale).
259
+ // This prevents IP leak where user thinks they're connected but traffic goes direct.
260
+ if (!_defaultState.wgTunnel && !_defaultState.v2rayProc) {
261
+ const stale = _defaultState.connection;
262
+ _defaultState.connection = null;
263
+ // End session on chain (fire-and-forget) to prevent stale session leaks
264
+ if (stale?.sessionId && _defaultState._mnemonic) {
265
+ _endSessionOnChain(stale.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
266
+ }
267
+ clearState();
268
+ events.emit('disconnected', { nodeAddress: stale.nodeAddress, serviceType: stale.serviceType, reason: 'phantom_state' });
269
+ return null;
270
+ }
271
+
272
+ const conn = _defaultState.connection;
273
+ const uptimeMs = Date.now() - conn.connectedAt;
274
+
275
+ // v25: Health checks — distinguish tunnel states
276
+ const healthChecks = {
277
+ tunnelActive: false,
278
+ proxyListening: false,
279
+ systemProxyValid: _defaultState.systemProxy,
280
+ };
281
+
282
+ if (_defaultState.wgTunnel) {
283
+ // WireGuard: check if adapter exists
284
+ if (process.platform === 'win32') {
285
+ try {
286
+ const out = execFileSync('netsh', ['interface', 'show', 'interface', 'name=wgsent0'], { encoding: 'utf8', stdio: 'pipe', timeout: 3000 });
287
+ healthChecks.tunnelActive = out.includes('Connected');
288
+ } catch {
289
+ // Adapter gone — tunnel is dead
290
+ healthChecks.tunnelActive = false;
291
+ }
292
+ } else {
293
+ // Non-Windows: trust state (no easy check)
294
+ healthChecks.tunnelActive = true;
295
+ }
296
+ }
297
+
298
+ if (_defaultState.v2rayProc) {
299
+ // V2Ray: check if process is alive
300
+ healthChecks.tunnelActive = !_defaultState.v2rayProc.killed && _defaultState.v2rayProc.exitCode === null;
301
+ // Proxy listening = process alive (async port check removed — was broken, fired after return)
302
+ healthChecks.proxyListening = healthChecks.tunnelActive;
303
+ if (conn.socksPort) {
304
+ }
305
+ }
306
+
307
+ // v28: Auto-clear phantom state — if connection exists but tunnel is dead,
308
+ // clean up stale state. Prevents ghost "connected" status after tunnel dies.
309
+ if (!healthChecks.tunnelActive && !_defaultState.v2rayProc && !_defaultState.wgTunnel) {
310
+ // Both tunnel handles are null — connection state is stale
311
+ if (conn?.sessionId && _defaultState._mnemonic) {
312
+ _endSessionOnChain(conn.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
313
+ }
314
+ _defaultState.connection = null;
315
+ clearState();
316
+ return null;
317
+ }
318
+ if (_defaultState.wgTunnel && !healthChecks.tunnelActive) {
319
+ // WireGuard state says connected but tunnel is dead — auto-cleanup
320
+ if (conn?.sessionId && _defaultState._mnemonic) {
321
+ _endSessionOnChain(conn.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
322
+ }
323
+ _defaultState.wgTunnel = null;
324
+ _defaultState.connection = null;
325
+ clearState();
326
+ events.emit('disconnected', { nodeAddress: conn.nodeAddress, serviceType: conn.serviceType, reason: 'tunnel_died' });
327
+ return null;
328
+ }
329
+ if (_defaultState.v2rayProc && !healthChecks.tunnelActive) {
330
+ // V2Ray process died — auto-cleanup
331
+ if (conn?.sessionId && _defaultState._mnemonic) {
332
+ _endSessionOnChain(conn.sessionId, _defaultState._mnemonic).then(r => events.emit('sessionEnded', { txHash: r?.transactionHash })).catch(e => events.emit('sessionEndFailed', { error: e.message }));
333
+ }
334
+ _defaultState.v2rayProc = null;
335
+ _defaultState.connection = null;
336
+ clearState();
337
+ events.emit('disconnected', { nodeAddress: conn.nodeAddress, serviceType: conn.serviceType, reason: 'tunnel_died' });
338
+ return null;
339
+ }
340
+
341
+ return {
342
+ connected: _defaultState.isConnected,
343
+ ...conn,
344
+ uptimeMs,
345
+ uptimeFormatted: formatUptime(uptimeMs),
346
+ healthChecks,
347
+ };
348
+ }
349
+
350
+ // ─── Verify Connection (v26c) ────────────────────────────────────────────────
351
+
352
+ /**
353
+ * Verify VPN is working by checking if IP has changed.
354
+ * Fetches public IP via ipify.org and compares to a direct (non-VPN) fetch.
355
+ *
356
+ * @param {object} [opts]
357
+ * @param {number} [opts.timeoutMs=8000]
358
+ * @returns {Promise<{ working: boolean, vpnIp: string|null, error?: string }>}
359
+ */
360
+ export async function verifyConnection(opts = {}) {
361
+ const timeout = opts.timeoutMs || 8000;
362
+ try {
363
+ const res = await axios.get('https://api.ipify.org?format=json', { timeout });
364
+ const vpnIp = res.data?.ip || null;
365
+ return { working: !!vpnIp, vpnIp };
366
+ } catch (err) {
367
+ return { working: false, vpnIp: null, error: err.message };
368
+ }
369
+ }