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,251 @@
1
+ /**
2
+ * Sentinel SDK — Instantiable Client Class
3
+ *
4
+ * Wraps the functional API with per-instance state, dependency injection,
5
+ * and EventEmitter. Addresses the "global singleton" finding from Meta/Telegram audits.
6
+ *
7
+ * v21 — 2026-03-09
8
+ *
9
+ * CHANGE LOG (for debugging — if bugs appear, check these changes):
10
+ * - NEW FILE: Wraps connectDirect/connectViaPlan/disconnect/queryOnlineNodes
11
+ * - Each instance has its own EventEmitter (independent from module-level `events`)
12
+ * - Constructor accepts DI options: logger, rpcUrl, lcdUrl, tlsTrust, v2rayExePath
13
+ * - Instance methods merge constructor defaults with per-call options
14
+ * - State (connected, sessionId, etc.) tracked per-instance
15
+ * - LIMITATION: WireGuard and V2Ray tunnels are OS-level singletons.
16
+ * Only one SentinelClient can have an active tunnel at a time.
17
+ * Multiple instances can query nodes, check balances, and broadcast TXs concurrently.
18
+ *
19
+ * Usage:
20
+ * import { SentinelClient } from './client.js';
21
+ * const client = new SentinelClient({ rpcUrl, lcdUrl, logger: myLogger });
22
+ * client.on('connected', ({ sessionId }) => updateUI());
23
+ * const conn = await client.connect({ mnemonic, nodeAddress });
24
+ * await client.disconnect();
25
+ */
26
+
27
+ import { EventEmitter } from 'events';
28
+ import {
29
+ connectDirect, connectViaPlan, connectAuto, queryOnlineNodes,
30
+ disconnect as sdkDisconnect,
31
+ isConnected as sdkIsConnected, getStatus as sdkGetStatus,
32
+ registerCleanupHandlers, setSystemProxy, clearSystemProxy,
33
+ events as sdkEvents, ConnectionState,
34
+ } from '../connection/index.js';
35
+ import {
36
+ createWallet, privKeyFromMnemonic, createClient, broadcast,
37
+ createSafeBroadcaster, getBalance, findExistingSession, fetchActiveNodes,
38
+ discoverPlanIds, resolveNodeUrl, lcd, MSG_TYPES,
39
+ } from '../chain/index.js';
40
+ import { nodeStatusV3 } from '../protocol/index.js';
41
+ import { createNodeHttpsAgent, clearKnownNode, clearAllKnownNodes, getKnownNode } from '../security/index.js';
42
+ import { SentinelError, ErrorCodes } from '../errors/index.js';
43
+
44
+ export class SentinelClient extends EventEmitter {
45
+ /**
46
+ * Create a new SentinelClient instance.
47
+ *
48
+ * @param {object} opts - Default options applied to all operations
49
+ * @param {string} opts.rpcUrl - Default RPC URL (overridable per-call)
50
+ * @param {string} opts.lcdUrl - Default LCD URL (overridable per-call)
51
+ * @param {string} opts.mnemonic - Default mnemonic (overridable per-call)
52
+ * @param {string} opts.v2rayExePath - Default V2Ray binary path
53
+ * @param {function} opts.logger - Logger function (default: console.log). Set to null to suppress.
54
+ * @param {'tofu'|'none'} opts.tlsTrust - TLS trust mode (default: 'tofu')
55
+ * @param {object} opts.timeouts - Default timeout overrides
56
+ * @param {boolean} opts.fullTunnel - Default fullTunnel setting
57
+ * @param {boolean} opts.systemProxy - Default systemProxy setting
58
+ */
59
+ constructor(opts = {}) {
60
+ super();
61
+ this._defaults = { ...opts };
62
+ this._logger = opts.logger !== undefined ? opts.logger : console.log;
63
+ this._connection = null; // last connection result
64
+ this._wallet = null; // cached wallet
65
+ this._client = null; // cached RPC client
66
+ this._rpc = null; // which RPC the cached client is connected to
67
+ this._state = new ConnectionState(); // per-instance tunnel state (v22)
68
+
69
+ // Forward module-level events to this instance's emitter
70
+ this._forwarder = (event) => (...args) => this.emit(event, ...args);
71
+ this._boundForwarders = {};
72
+ for (const event of ['connecting', 'connected', 'disconnected', 'error', 'progress']) {
73
+ this._boundForwarders[event] = this._forwarder(event);
74
+ sdkEvents.on(event, this._boundForwarders[event]);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Merge instance defaults with per-call options.
80
+ * Per-call values override instance defaults.
81
+ */
82
+ _mergeOpts(callOpts = {}) {
83
+ const merged = { ...this._defaults, ...callOpts };
84
+ // Logger: use instance logger unless per-call provides one
85
+ if (!callOpts.log && this._logger) merged.log = this._logger;
86
+ // Inject per-instance state so tunnels are isolated
87
+ merged._state = this._state;
88
+ return merged;
89
+ }
90
+
91
+ // ─── Connection ──────────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Connect to a node by paying directly per GB.
95
+ * @param {object} opts - Options (merged with constructor defaults)
96
+ * @returns {Promise<object>} Connection result with cleanup()
97
+ */
98
+ async connect(opts = {}) {
99
+ const merged = this._mergeOpts(opts);
100
+ this._connection = await connectDirect(merged);
101
+ return this._connection;
102
+ }
103
+
104
+ /**
105
+ * Connect with auto-fallback: picks best node, retries on failure.
106
+ * Recommended for most apps.
107
+ * @param {object} opts - Options (merged with constructor defaults)
108
+ * @returns {Promise<object>} Connection result with cleanup()
109
+ */
110
+ async autoConnect(opts = {}) {
111
+ const merged = this._mergeOpts(opts);
112
+ this._connection = await connectAuto(merged);
113
+ return this._connection;
114
+ }
115
+
116
+ /**
117
+ * Connect via a plan subscription.
118
+ * @param {object} opts - Options including planId (merged with constructor defaults)
119
+ * @returns {Promise<object>} Connection result with cleanup()
120
+ */
121
+ async connectPlan(opts = {}) {
122
+ const merged = this._mergeOpts(opts);
123
+ this._connection = await connectViaPlan(merged);
124
+ return this._connection;
125
+ }
126
+
127
+ /**
128
+ * Disconnect current VPN tunnel.
129
+ */
130
+ async disconnect() {
131
+ await sdkDisconnect();
132
+ this._connection = null;
133
+ }
134
+
135
+ /**
136
+ * Check if a VPN tunnel is currently active.
137
+ */
138
+ isConnected() {
139
+ return this._state.isConnected;
140
+ }
141
+
142
+ /**
143
+ * Get current connection status (null if not connected).
144
+ */
145
+ getStatus() {
146
+ if (!this._state.connection) return null;
147
+ return {
148
+ connected: this._state.isConnected,
149
+ ...this._state.connection,
150
+ uptimeMs: Date.now() - this._state.connection.connectedAt,
151
+ };
152
+ }
153
+
154
+ // ─── Node Discovery ──────────────────────────────────────────────────────
155
+
156
+ /**
157
+ * List online nodes, sorted by quality score.
158
+ * Uses node cache (5min TTL) for instant results on repeat calls.
159
+ * @param {object} options - Query options (merged with constructor defaults for lcdUrl)
160
+ */
161
+ async listNodes(options = {}) {
162
+ const merged = { ...options };
163
+ if (!merged.lcdUrl && this._defaults.lcdUrl) merged.lcdUrl = this._defaults.lcdUrl;
164
+ return queryOnlineNodes(merged);
165
+ }
166
+
167
+ /**
168
+ * Get status of a specific node.
169
+ * @param {string} remoteUrl - Node's remote URL (https://...)
170
+ * @param {string} nodeAddress - sentnode1... address (for TOFU TLS)
171
+ */
172
+ async nodeStatus(remoteUrl, nodeAddress) {
173
+ const agent = nodeAddress
174
+ ? createNodeHttpsAgent(nodeAddress, this._defaults.tlsTrust || 'tofu')
175
+ : undefined;
176
+ return nodeStatusV3(remoteUrl, agent);
177
+ }
178
+
179
+ // ─── Wallet & Chain ──────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * Create or return cached wallet from mnemonic.
183
+ * @param {string} mnemonic - Override mnemonic (or uses instance default)
184
+ */
185
+ async getWallet(mnemonic) {
186
+ const m = mnemonic || this._defaults.mnemonic;
187
+ if (!m) throw new SentinelError(ErrorCodes.INVALID_MNEMONIC, 'No mnemonic provided');
188
+ if (this._wallet) return this._wallet;
189
+ this._wallet = await createWallet(m);
190
+ return this._wallet;
191
+ }
192
+
193
+ /**
194
+ * Get or create a cached RPC client.
195
+ * @param {string} rpcUrl - Override RPC URL (or uses instance default)
196
+ */
197
+ async getClient(rpcUrl) {
198
+ const url = rpcUrl || this._defaults.rpcUrl;
199
+ if (!url) throw new SentinelError(ErrorCodes.INVALID_URL, 'No rpcUrl provided');
200
+ if (this._client && this._rpc === url) return this._client;
201
+ const { wallet } = await this.getWallet();
202
+ this._client = await createClient(url, wallet);
203
+ this._rpc = url;
204
+ return this._client;
205
+ }
206
+
207
+ /**
208
+ * Get P2P token balance for the instance wallet.
209
+ */
210
+ async getBalance() {
211
+ const { account } = await this.getWallet();
212
+ const client = await this.getClient();
213
+ return getBalance(client, account.address);
214
+ }
215
+
216
+ // ─── TLS Trust Management ────────────────────────────────────────────────
217
+
218
+ /** Clear stored TLS fingerprint for a node */
219
+ clearKnownNode(nodeAddress) { clearKnownNode(nodeAddress); }
220
+
221
+ /** Clear all stored TLS fingerprints */
222
+ clearAllKnownNodes() { clearAllKnownNodes(); }
223
+
224
+ /** Get stored cert info for a node */
225
+ getKnownNode(nodeAddress) { return getKnownNode(nodeAddress); }
226
+
227
+ // ─── Lifecycle ───────────────────────────────────────────────────────────
228
+
229
+ /**
230
+ * Register process exit handlers for clean tunnel shutdown.
231
+ * Call once at app startup.
232
+ */
233
+ registerCleanup() {
234
+ registerCleanupHandlers();
235
+ }
236
+
237
+ /**
238
+ * Clean up event forwarding. Call when discarding the instance.
239
+ */
240
+ destroy() {
241
+ for (const [event, fn] of Object.entries(this._boundForwarders)) {
242
+ sdkEvents.removeListener(event, fn);
243
+ }
244
+ this._boundForwarders = {};
245
+ this._connection = null;
246
+ this._wallet = null;
247
+ this._client = null;
248
+ this._state.destroy(); // remove from global cleanup registry
249
+ this.removeAllListeners();
250
+ }
251
+ }
package/client.js ADDED
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Sentinel SDK — Instantiable Client Class
3
+ *
4
+ * Wraps the functional API with per-instance state, dependency injection,
5
+ * and EventEmitter. Addresses the "global singleton" finding from Meta/Telegram audits.
6
+ *
7
+ * v21 — 2026-03-09
8
+ *
9
+ * CHANGE LOG (for debugging — if bugs appear, check these changes):
10
+ * - NEW FILE: Wraps connectDirect/connectViaPlan/disconnect/queryOnlineNodes
11
+ * - Each instance has its own EventEmitter (independent from module-level `events`)
12
+ * - Constructor accepts DI options: logger, rpcUrl, lcdUrl, tlsTrust, v2rayExePath
13
+ * - Instance methods merge constructor defaults with per-call options
14
+ * - State (connected, sessionId, etc.) tracked per-instance
15
+ * - LIMITATION: WireGuard and V2Ray tunnels are OS-level singletons.
16
+ * Only one SentinelClient can have an active tunnel at a time.
17
+ * Multiple instances can query nodes, check balances, and broadcast TXs concurrently.
18
+ *
19
+ * Usage:
20
+ * import { SentinelClient } from './client.js';
21
+ * const client = new SentinelClient({ rpcUrl, lcdUrl, logger: myLogger });
22
+ * client.on('connected', ({ sessionId }) => updateUI());
23
+ * const conn = await client.connect({ mnemonic, nodeAddress });
24
+ * await client.disconnect();
25
+ */
26
+
27
+ import { EventEmitter } from 'events';
28
+ import {
29
+ connectDirect, connectViaPlan, connectAuto, queryOnlineNodes,
30
+ disconnect as sdkDisconnect, disconnectState,
31
+ isConnected as sdkIsConnected, getStatus as sdkGetStatus,
32
+ registerCleanupHandlers, setSystemProxy, clearSystemProxy,
33
+ events as sdkEvents, ConnectionState,
34
+ } from './node-connect.js';
35
+ import {
36
+ createWallet, privKeyFromMnemonic, createClient, broadcast,
37
+ createSafeBroadcaster, getBalance, findExistingSession, fetchActiveNodes,
38
+ discoverPlanIds, resolveNodeUrl, lcd, MSG_TYPES,
39
+ } from './cosmjs-setup.js';
40
+ import { nodeStatusV3 } from './v3protocol.js';
41
+ import { createNodeHttpsAgent, clearKnownNode, clearAllKnownNodes, getKnownNode } from './tls-trust.js';
42
+ import { SentinelError, ErrorCodes } from './errors.js';
43
+
44
+ export class SentinelClient extends EventEmitter {
45
+ /**
46
+ * Create a new SentinelClient instance.
47
+ *
48
+ * @param {object} opts - Default options applied to all operations
49
+ * @param {string} opts.rpcUrl - Default RPC URL (overridable per-call)
50
+ * @param {string} opts.lcdUrl - Default LCD URL (overridable per-call)
51
+ * @param {string} opts.mnemonic - Default mnemonic (overridable per-call)
52
+ * @param {string} opts.v2rayExePath - Default V2Ray binary path
53
+ * @param {function} opts.logger - Logger function (default: console.log). Set to null to suppress.
54
+ * @param {'tofu'|'none'} opts.tlsTrust - TLS trust mode (default: 'tofu')
55
+ * @param {object} opts.timeouts - Default timeout overrides
56
+ * @param {boolean} opts.fullTunnel - Default fullTunnel setting
57
+ * @param {boolean} opts.systemProxy - Default systemProxy setting
58
+ */
59
+ constructor(opts = {}) {
60
+ super();
61
+ this._defaults = { ...opts };
62
+ this._logger = opts.logger !== undefined ? opts.logger : console.log;
63
+ this._connection = null; // last connection result
64
+ this._wallet = null; // cached wallet
65
+ this._client = null; // cached RPC client
66
+ this._rpc = null; // which RPC the cached client is connected to
67
+ this._state = new ConnectionState(); // per-instance tunnel state (v22)
68
+
69
+ // Forward module-level events to this instance's emitter
70
+ this._forwarder = (event) => (...args) => this.emit(event, ...args);
71
+ this._boundForwarders = {};
72
+ for (const event of ['connecting', 'connected', 'disconnected', 'error', 'progress']) {
73
+ this._boundForwarders[event] = this._forwarder(event);
74
+ sdkEvents.on(event, this._boundForwarders[event]);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Merge instance defaults with per-call options.
80
+ * Per-call values override instance defaults.
81
+ */
82
+ _mergeOpts(callOpts = {}) {
83
+ const merged = { ...this._defaults, ...callOpts };
84
+ // Logger: use instance logger unless per-call provides one
85
+ if (!callOpts.log && this._logger) merged.log = this._logger;
86
+ // Inject per-instance state so tunnels are isolated
87
+ merged._state = this._state;
88
+ return merged;
89
+ }
90
+
91
+ // ─── Connection ──────────────────────────────────────────────────────────
92
+
93
+ /**
94
+ * Connect to a node by paying directly per GB.
95
+ * @param {object} opts - Options (merged with constructor defaults)
96
+ * @returns {Promise<object>} Connection result with cleanup()
97
+ */
98
+ async connect(opts = {}) {
99
+ const merged = this._mergeOpts(opts);
100
+ this._connection = await connectDirect(merged);
101
+ return this._connection;
102
+ }
103
+
104
+ /**
105
+ * Connect with auto-fallback: picks best node, retries on failure.
106
+ * Recommended for most apps.
107
+ * @param {object} opts - Options (merged with constructor defaults)
108
+ * @returns {Promise<object>} Connection result with cleanup()
109
+ */
110
+ async autoConnect(opts = {}) {
111
+ const merged = this._mergeOpts(opts);
112
+ this._connection = await connectAuto(merged);
113
+ return this._connection;
114
+ }
115
+
116
+ /**
117
+ * Connect via a plan subscription.
118
+ * @param {object} opts - Options including planId (merged with constructor defaults)
119
+ * @returns {Promise<object>} Connection result with cleanup()
120
+ */
121
+ async connectPlan(opts = {}) {
122
+ const merged = this._mergeOpts(opts);
123
+ this._connection = await connectViaPlan(merged);
124
+ return this._connection;
125
+ }
126
+
127
+ /**
128
+ * Disconnect current VPN tunnel.
129
+ */
130
+ async disconnect() {
131
+ await disconnectState(this._state);
132
+ this._connection = null;
133
+ }
134
+
135
+ /**
136
+ * Check if a VPN tunnel is currently active.
137
+ */
138
+ isConnected() {
139
+ return this._state.isConnected;
140
+ }
141
+
142
+ /**
143
+ * Get current connection status (null if not connected).
144
+ * v29: Cross-checks tunnel liveness to prevent phantom connected state.
145
+ */
146
+ getStatus() {
147
+ if (!this._state.connection) return null;
148
+ // v29: If connection object exists but tunnel handles are gone, state is phantom.
149
+ // Clear it and return disconnected to prevent IP leak.
150
+ if (!this._state.wgTunnel && !this._state.v2rayProc) {
151
+ const stale = this._state.connection;
152
+ this._state.connection = null;
153
+ this.emit('disconnected', { nodeAddress: stale.nodeAddress, serviceType: stale.serviceType, reason: 'phantom_state' });
154
+ return null;
155
+ }
156
+ const uptimeMs = Date.now() - this._state.connection.connectedAt;
157
+ const secs = Math.floor(uptimeMs / 1000);
158
+ const m = Math.floor(secs / 60), s = secs % 60, h = Math.floor(m / 60);
159
+ const uptimeFormatted = h > 0 ? `${h}h ${m % 60}m ${s}s` : m > 0 ? `${m}m ${s}s` : `${s}s`;
160
+ return {
161
+ connected: this._state.isConnected,
162
+ ...this._state.connection,
163
+ uptimeMs,
164
+ uptimeFormatted,
165
+ };
166
+ }
167
+
168
+ // ─── Node Discovery ──────────────────────────────────────────────────────
169
+
170
+ /**
171
+ * List online nodes, sorted by quality score.
172
+ * Uses node cache (5min TTL) for instant results on repeat calls.
173
+ * @param {object} options - Query options (merged with constructor defaults for lcdUrl)
174
+ */
175
+ async listNodes(options = {}) {
176
+ const merged = { ...options };
177
+ if (!merged.lcdUrl && this._defaults.lcdUrl) merged.lcdUrl = this._defaults.lcdUrl;
178
+ return queryOnlineNodes(merged);
179
+ }
180
+
181
+ /**
182
+ * Get status of a specific node.
183
+ * @param {string} remoteUrl - Node's remote URL (https://...)
184
+ * @param {string} nodeAddress - sentnode1... address (for TOFU TLS)
185
+ */
186
+ async nodeStatus(remoteUrl, nodeAddress) {
187
+ const agent = nodeAddress
188
+ ? createNodeHttpsAgent(nodeAddress, this._defaults.tlsTrust || 'tofu')
189
+ : undefined;
190
+ return nodeStatusV3(remoteUrl, agent);
191
+ }
192
+
193
+ // ─── Wallet & Chain ──────────────────────────────────────────────────────
194
+
195
+ /**
196
+ * Create or return cached wallet from mnemonic.
197
+ * @param {string} mnemonic - Override mnemonic (or uses instance default)
198
+ */
199
+ async getWallet(mnemonic) {
200
+ const m = mnemonic || this._defaults.mnemonic;
201
+ if (!m) throw new SentinelError(ErrorCodes.INVALID_MNEMONIC, 'No mnemonic provided');
202
+ // Invalidate cache if mnemonic changed
203
+ if (this._wallet && this._walletMnemonic !== m) {
204
+ this._wallet = null;
205
+ this._client = null; // client depends on wallet
206
+ }
207
+ if (this._wallet) return this._wallet;
208
+ this._wallet = await createWallet(m);
209
+ this._walletMnemonic = m;
210
+ return this._wallet;
211
+ }
212
+
213
+ /**
214
+ * Get or create a cached RPC client.
215
+ * @param {string} rpcUrl - Override RPC URL (or uses instance default)
216
+ */
217
+ async getClient(rpcUrl) {
218
+ const url = rpcUrl || this._defaults.rpcUrl;
219
+ if (!url) throw new SentinelError(ErrorCodes.INVALID_URL, 'No rpcUrl provided');
220
+ if (this._client && this._rpc === url) return this._client;
221
+ const { wallet } = await this.getWallet();
222
+ this._client = await createClient(url, wallet);
223
+ this._rpc = url;
224
+ return this._client;
225
+ }
226
+
227
+ /**
228
+ * Get P2P balance for the instance wallet.
229
+ */
230
+ async getBalance() {
231
+ const { account } = await this.getWallet();
232
+ const client = await this.getClient();
233
+ return getBalance(client, account.address);
234
+ }
235
+
236
+ // ─── TLS Trust Management ────────────────────────────────────────────────
237
+
238
+ /** Clear stored TLS fingerprint for a node */
239
+ clearKnownNode(nodeAddress) { clearKnownNode(nodeAddress); }
240
+
241
+ /** Clear all stored TLS fingerprints */
242
+ clearAllKnownNodes() { clearAllKnownNodes(); }
243
+
244
+ /** Get stored cert info for a node */
245
+ getKnownNode(nodeAddress) { return getKnownNode(nodeAddress); }
246
+
247
+ // ─── Lifecycle ───────────────────────────────────────────────────────────
248
+
249
+ /**
250
+ * Register process exit handlers for clean tunnel shutdown.
251
+ * Call once at app startup.
252
+ */
253
+ registerCleanup() {
254
+ registerCleanupHandlers();
255
+ }
256
+
257
+ /**
258
+ * Clean up event forwarding. Call when discarding the instance.
259
+ */
260
+ destroy() {
261
+ for (const [event, fn] of Object.entries(this._boundForwarders)) {
262
+ sdkEvents.removeListener(event, fn);
263
+ }
264
+ this._boundForwarders = {};
265
+ this._connection = null;
266
+ this._wallet = null;
267
+ this._client = null;
268
+ this._state.destroy(); // remove from global cleanup registry
269
+ this.removeAllListeners();
270
+ }
271
+ }