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
package/audit.js ADDED
@@ -0,0 +1,847 @@
1
+ /**
2
+ * Sentinel dVPN SDK -- Network Audit & Node Testing
3
+ *
4
+ * Utility/operator functions for testing individual nodes and auditing the
5
+ * network. These use the SDK's own consumer path (connectDirect/disconnect)
6
+ * internally -- they do NOT reimplement handshake, tunnel, or payment logic.
7
+ *
8
+ * WARNING: OPERATOR TOOL -- NOT FOR CONSUMER APPS
9
+ * These functions start sessions across many nodes, costing real P2P tokens.
10
+ * Consumer apps should use connectAuto() or connectDirect() instead.
11
+ *
12
+ * Usage:
13
+ * import { testNode, auditNetwork } from './audit.js';
14
+ *
15
+ * // Test a single node
16
+ * const result = await testNode({ mnemonic, nodeAddress: 'sentnode1...' });
17
+ *
18
+ * // Audit the whole network
19
+ * const { results, stats } = await auditNetwork({
20
+ * mnemonic, concurrency: 30, onProgress: (r) => console.log(r),
21
+ * });
22
+ */
23
+
24
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
25
+ import path from 'path';
26
+ import os from 'os';
27
+ import axios from 'axios';
28
+
29
+ import {
30
+ connectDirect,
31
+ disconnect,
32
+ queryOnlineNodes,
33
+ fetchAllNodes,
34
+ registerCleanupHandlers,
35
+ events,
36
+ ConnectionState,
37
+ disconnectState,
38
+ } from './node-connect.js';
39
+
40
+ import {
41
+ speedtestDirect,
42
+ speedtestViaSocks5,
43
+ } from './speedtest.js';
44
+
45
+ import {
46
+ createWallet,
47
+ getBalance,
48
+ createClient,
49
+ } from './cosmjs-setup.js';
50
+
51
+ import { nodeStatusV3 } from './v3protocol.js';
52
+
53
+ import {
54
+ DEFAULT_RPC,
55
+ DEFAULT_LCD,
56
+ RPC_ENDPOINTS,
57
+ LCD_ENDPOINTS,
58
+ BROKEN_NODES,
59
+ tryWithFallback,
60
+ sleep,
61
+ } from './defaults.js';
62
+
63
+ import {
64
+ SentinelError,
65
+ ValidationError,
66
+ NodeError,
67
+ ChainError,
68
+ TunnelError,
69
+ ErrorCodes,
70
+ isRetryable,
71
+ } from './errors.js';
72
+
73
+ // ─── Constants ────────────────────────────────────────────────────────────────
74
+
75
+ const TRANSPORT_CACHE_DIR = path.join(os.homedir(), '.sentinel-sdk');
76
+ const TRANSPORT_CACHE_FILE = path.join(TRANSPORT_CACHE_DIR, 'transport-cache.json');
77
+ const TRANSPORT_CACHE_TTL = 14 * 24 * 60 * 60_000; // 14 days
78
+
79
+ const GOOGLE_CHECK_TARGETS = [
80
+ 'https://www.google.com',
81
+ 'https://www.google.com/generate_204',
82
+ ];
83
+
84
+ const GOOGLE_CHECK_TIMEOUT = 10_000;
85
+
86
+ // ─── Transport Cache ──────────────────────────────────────────────────────────
87
+ //
88
+ // Learns which V2Ray transports work per node. Persists to disk at
89
+ // ~/.sentinel-sdk/transport-cache.json with TTL eviction.
90
+ //
91
+ // Structure:
92
+ // {
93
+ // perNode: { "sentnode1...": { protocol, network, security, port, successCount, failCount, lastSeen } },
94
+ // global: { "grpc/none": { success, fail, updatedAt }, ... },
95
+ // }
96
+
97
+ /** @type {{ perNode: Record<string, object>, global: Record<string, object> } | null} */
98
+ let _transportCache = null;
99
+
100
+ /**
101
+ * Load the transport cache from disk. Creates an empty cache if none exists.
102
+ * Evicts entries older than TRANSPORT_CACHE_TTL.
103
+ *
104
+ * @param {string} [cachePath] - Custom path (default: ~/.sentinel-sdk/transport-cache.json)
105
+ * @returns {{ perNode: Record<string, object>, global: Record<string, object> }}
106
+ */
107
+ export function loadTransportCache(cachePath) {
108
+ const filePath = cachePath || TRANSPORT_CACHE_FILE;
109
+ try {
110
+ if (existsSync(filePath)) {
111
+ const raw = JSON.parse(readFileSync(filePath, 'utf-8'));
112
+ const now = Date.now();
113
+ const perNode = {};
114
+ const global = {};
115
+
116
+ // Evict stale per-node entries
117
+ for (const [addr, entry] of Object.entries(raw.perNode || {})) {
118
+ if (entry.lastSeen && now - entry.lastSeen < TRANSPORT_CACHE_TTL) {
119
+ perNode[addr] = entry;
120
+ }
121
+ }
122
+
123
+ // Evict stale global entries
124
+ for (const [key, entry] of Object.entries(raw.global || {})) {
125
+ if (entry.updatedAt && now - entry.updatedAt < TRANSPORT_CACHE_TTL) {
126
+ global[key] = entry;
127
+ }
128
+ }
129
+
130
+ _transportCache = { perNode, global };
131
+ } else {
132
+ _transportCache = { perNode: {}, global: {} };
133
+ }
134
+ } catch {
135
+ _transportCache = { perNode: {}, global: {} };
136
+ }
137
+ return _transportCache;
138
+ }
139
+
140
+ /**
141
+ * Save the current transport cache to disk.
142
+ *
143
+ * @param {string} [cachePath] - Custom path (default: ~/.sentinel-sdk/transport-cache.json)
144
+ */
145
+ export function saveTransportCache(cachePath) {
146
+ if (!_transportCache) return;
147
+ const filePath = cachePath || TRANSPORT_CACHE_FILE;
148
+ try {
149
+ const dir = path.dirname(filePath);
150
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
151
+ writeFileSync(filePath, JSON.stringify(_transportCache, null, 2), { mode: 0o600 });
152
+ } catch {
153
+ // Disk write failed -- cache stays in memory only
154
+ }
155
+ }
156
+
157
+ /** Ensure cache is loaded (lazy init). */
158
+ function _ensureCache() {
159
+ if (!_transportCache) loadTransportCache();
160
+ return _transportCache;
161
+ }
162
+
163
+ /**
164
+ * Record a successful transport for a node. Updates both per-node and global stats.
165
+ *
166
+ * @param {string} nodeAddr - Node address (sentnode1...)
167
+ * @param {{ protocol: string, network: string, security: string, port: number }} transport - Working transport details
168
+ */
169
+ export function recordTransportSuccess(nodeAddr, transport) {
170
+ const cache = _ensureCache();
171
+ const { protocol, network, security, port } = transport;
172
+ const globalKey = security && security !== 'none' ? `${network}/${security}` : network;
173
+
174
+ // Per-node: store the working transport
175
+ cache.perNode[nodeAddr] = {
176
+ protocol,
177
+ network,
178
+ security: security || 'none',
179
+ port,
180
+ successCount: (cache.perNode[nodeAddr]?.successCount || 0) + 1,
181
+ failCount: cache.perNode[nodeAddr]?.failCount || 0,
182
+ lastSeen: Date.now(),
183
+ };
184
+
185
+ // Global: increment success
186
+ if (!cache.global[globalKey]) cache.global[globalKey] = { success: 0, fail: 0, updatedAt: 0 };
187
+ cache.global[globalKey].success++;
188
+ cache.global[globalKey].updatedAt = Date.now();
189
+
190
+ saveTransportCache();
191
+ }
192
+
193
+ /**
194
+ * Record a transport failure. Updates global stats only (per-node keeps last success).
195
+ *
196
+ * @param {{ protocol: string, network: string, security: string }} transport - Failed transport details
197
+ */
198
+ export function recordTransportFailure(transport) {
199
+ const cache = _ensureCache();
200
+ const { network, security } = transport;
201
+ const globalKey = security && security !== 'none' ? `${network}/${security}` : network;
202
+
203
+ if (!cache.global[globalKey]) cache.global[globalKey] = { success: 0, fail: 0, updatedAt: 0 };
204
+ cache.global[globalKey].fail++;
205
+ cache.global[globalKey].updatedAt = Date.now();
206
+
207
+ saveTransportCache();
208
+ }
209
+
210
+ /**
211
+ * Reorder V2Ray outbounds based on cached intelligence.
212
+ * Cached per-node hit goes first, then sorted by global success rate (descending).
213
+ *
214
+ * @param {string} nodeAddr - Node address
215
+ * @param {Array<object>} outbounds - V2Ray outbound configs (from buildV2RayClientConfig)
216
+ * @returns {Array<object>} Reordered outbounds (new array, original unchanged)
217
+ */
218
+ export function reorderOutbounds(nodeAddr, outbounds) {
219
+ const cache = _ensureCache();
220
+ const nodeEntry = cache.perNode[nodeAddr];
221
+
222
+ // Extract transport key from outbound
223
+ function outboundKey(ob) {
224
+ const network = ob.streamSettings?.network;
225
+ const security = ob.streamSettings?.security || 'none';
226
+ if (!network) return null;
227
+ return security !== 'none' ? `${network}/${security}` : network;
228
+ }
229
+
230
+ // Get global success rate for a transport key
231
+ function globalRate(key) {
232
+ if (!key) return 0;
233
+ const entry = cache.global[key];
234
+ if (!entry) return 0.5; // unknown -- neutral
235
+ const total = entry.success + entry.fail;
236
+ if (total < 2) return 0.5;
237
+ return entry.success / total;
238
+ }
239
+
240
+ const sorted = [...outbounds];
241
+
242
+ sorted.sort((a, b) => {
243
+ // Cached per-node hit gets priority
244
+ if (nodeEntry) {
245
+ const aMatch = a.streamSettings?.network === nodeEntry.network &&
246
+ (a.streamSettings?.security || 'none') === nodeEntry.security;
247
+ const bMatch = b.streamSettings?.network === nodeEntry.network &&
248
+ (b.streamSettings?.security || 'none') === nodeEntry.security;
249
+ if (aMatch && !bMatch) return -1;
250
+ if (!aMatch && bMatch) return 1;
251
+ }
252
+
253
+ // Then sort by global success rate
254
+ const aRate = globalRate(outboundKey(a));
255
+ const bRate = globalRate(outboundKey(b));
256
+ return bRate - aRate;
257
+ });
258
+
259
+ return sorted;
260
+ }
261
+
262
+ /**
263
+ * Get transport cache statistics.
264
+ *
265
+ * @returns {{ nodesCached: number, transportStats: Array<{ transport: string, success: number, fail: number, rate: number }> }}
266
+ */
267
+ export function getCacheStats() {
268
+ const cache = _ensureCache();
269
+ const transportStats = [];
270
+
271
+ for (const [key, entry] of Object.entries(cache.global)) {
272
+ const total = entry.success + entry.fail;
273
+ transportStats.push({
274
+ transport: key,
275
+ success: entry.success,
276
+ fail: entry.fail,
277
+ rate: total > 0 ? parseFloat((entry.success / total).toFixed(3)) : 0,
278
+ });
279
+ }
280
+
281
+ // Sort by sample size descending
282
+ transportStats.sort((a, b) => (b.success + b.fail) - (a.success + a.fail));
283
+
284
+ return {
285
+ nodesCached: Object.keys(cache.perNode).length,
286
+ transportStats,
287
+ };
288
+ }
289
+
290
+ // ─── Google Accessibility Check ───────────────────────────────────────────────
291
+
292
+ /**
293
+ * Check if Google is reachable through the active tunnel.
294
+ * For WireGuard: direct HTTPS (all traffic is tunneled).
295
+ * For V2Ray: routes through SOCKS5 proxy.
296
+ *
297
+ * @param {'wireguard'|'v2ray'} serviceType
298
+ * @param {number} [socksPort] - SOCKS5 port (V2Ray only)
299
+ * @returns {Promise<boolean>}
300
+ * @private
301
+ */
302
+ async function _checkGoogleAccessible(serviceType, socksPort) {
303
+ for (const target of GOOGLE_CHECK_TARGETS) {
304
+ try {
305
+ if (serviceType === 'wireguard') {
306
+ // WireGuard: all traffic goes through tunnel
307
+ await axios.get(target, {
308
+ timeout: GOOGLE_CHECK_TIMEOUT,
309
+ maxRedirects: 2,
310
+ validateStatus: (s) => s < 500,
311
+ });
312
+ return true;
313
+ } else if (serviceType === 'v2ray' && socksPort) {
314
+ // V2Ray: route through SOCKS5
315
+ const { SocksProxyAgent } = await import('socks-proxy-agent');
316
+ const agent = new SocksProxyAgent(`socks5://127.0.0.1:${socksPort}`);
317
+ try {
318
+ await axios.get(target, {
319
+ httpAgent: agent,
320
+ httpsAgent: agent,
321
+ timeout: GOOGLE_CHECK_TIMEOUT,
322
+ maxRedirects: 2,
323
+ validateStatus: (s) => s < 500,
324
+ });
325
+ return true;
326
+ } finally {
327
+ agent.destroy();
328
+ }
329
+ }
330
+ } catch {
331
+ // Try next target
332
+ }
333
+ }
334
+ return false;
335
+ }
336
+
337
+ // ─── testNode ─────────────────────────────────────────────────────────────────
338
+
339
+ /**
340
+ * Test a single Sentinel dVPN node using the SDK's consumer connection path.
341
+ *
342
+ * Flow:
343
+ * 1. connectDirect() -- real wallet, real session, real tunnel
344
+ * 2. Speed test (WG: speedtestDirect, V2Ray: speedtestViaSocks5)
345
+ * 3. Google accessibility check
346
+ * 4. disconnect() -- clean teardown
347
+ *
348
+ * Returns a structured result with pass/fail, speed, accessibility, and diagnostics.
349
+ *
350
+ * @param {object} options
351
+ * @param {string} options.mnemonic - BIP39 wallet mnemonic (required)
352
+ * @param {string} options.nodeAddress - Node address sentnode1... (required)
353
+ * @param {string} [options.rpcUrl] - RPC endpoint URL
354
+ * @param {string} [options.lcdUrl] - LCD endpoint URL
355
+ * @param {string} [options.v2rayExePath] - Path to v2ray binary (required for V2Ray nodes)
356
+ * @param {number} [options.gigabytes=1] - GB to allocate for session
357
+ * @param {number} [options.testMb=5] - Download size for speed test (MB)
358
+ * @param {number} [options.baselineMbps=null] - Baseline speed for scoring
359
+ * @param {function} [options.onLog=null] - Log callback (msg) => {}
360
+ * @param {function} [options.onProgress=null] - Progress callback (step, detail) => {}
361
+ * @param {AbortSignal} [options.signal=null] - AbortSignal for cancellation
362
+ * @returns {Promise<{
363
+ * pass: boolean,
364
+ * address: string,
365
+ * type: string,
366
+ * moniker: string,
367
+ * country: string,
368
+ * city: string,
369
+ * actualMbps: number,
370
+ * googleAccessible: boolean,
371
+ * diag: string,
372
+ * timestamp: string,
373
+ * }>}
374
+ */
375
+ export async function testNode(options) {
376
+ // ── Validate inputs ──
377
+ if (!options || typeof options !== 'object') {
378
+ throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'testNode() requires an options object');
379
+ }
380
+ if (typeof options.mnemonic !== 'string' || options.mnemonic.trim().split(/\s+/).length < 12) {
381
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'mnemonic must be a 12+ word BIP39 string');
382
+ }
383
+ if (!options.nodeAddress || !options.nodeAddress.startsWith('sentnode')) {
384
+ throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS, 'nodeAddress must be a valid sentnode1... address');
385
+ }
386
+
387
+ const {
388
+ mnemonic,
389
+ nodeAddress,
390
+ rpcUrl,
391
+ lcdUrl,
392
+ v2rayExePath,
393
+ gigabytes = 1,
394
+ testMb = 5,
395
+ baselineMbps = null,
396
+ onLog = null,
397
+ onProgress = null,
398
+ signal = null,
399
+ } = options;
400
+
401
+ // Ensure cleanup handlers are registered (connectDirect requires it)
402
+ registerCleanupHandlers();
403
+
404
+ const log = (msg) => { if (onLog) onLog(msg); };
405
+ const progress = (step, detail) => { if (onProgress) onProgress(step, detail); };
406
+
407
+ const timestamp = new Date().toISOString();
408
+ let connResult = null;
409
+ let serviceType = null;
410
+ let moniker = '';
411
+ let country = '';
412
+ let city = '';
413
+ let actualMbps = 0;
414
+ let googleAccessible = false;
415
+ let diag = '';
416
+ let pass = false;
417
+
418
+ try {
419
+ // ── Step 1: Connect via the SDK's consumer path ──
420
+ progress('connect', `Connecting to ${nodeAddress}...`);
421
+ log(`[testNode] Connecting to ${nodeAddress}`);
422
+
423
+ connResult = await connectDirect({
424
+ mnemonic,
425
+ nodeAddress,
426
+ rpcUrl,
427
+ lcdUrl,
428
+ v2rayExePath,
429
+ gigabytes,
430
+ fullTunnel: serviceType === 'wireguard', // WG: full tunnel for speedtest, V2Ray: SOCKS5
431
+ systemProxy: false, // Never set system proxy during testing
432
+ onProgress: (step, detail) => progress(`connect:${step}`, detail),
433
+ signal,
434
+ _skipLock: true, // Allow concurrent tests (audit mode)
435
+ });
436
+
437
+ serviceType = connResult.serviceType;
438
+ log(`[testNode] Connected: ${serviceType} (session ${connResult.sessionId})`);
439
+
440
+ // Extract node metadata from the connection result if available
441
+ if (connResult.nodeMoniker) moniker = connResult.nodeMoniker;
442
+ if (connResult.nodeLocation) {
443
+ country = connResult.nodeLocation.country || '';
444
+ city = connResult.nodeLocation.city || '';
445
+ }
446
+
447
+ // If we don't have moniker/location from connResult, fetch node status
448
+ if (!moniker || !country) {
449
+ try {
450
+ const lcdBase = lcdUrl || DEFAULT_LCD;
451
+ const { queryNode } = await import('./cosmjs-setup.js');
452
+ const nodeInfo = await queryNode(nodeAddress, { lcdUrl: lcdBase });
453
+ if (nodeInfo.remote_url) {
454
+ const status = await nodeStatusV3(nodeInfo.remote_url);
455
+ moniker = moniker || status.moniker;
456
+ country = country || status.location.country;
457
+ city = city || status.location.city;
458
+ }
459
+ } catch {
460
+ // Metadata fetch failed -- non-fatal, continue with empty fields
461
+ }
462
+ }
463
+
464
+ // ── Step 2: Speed test ──
465
+ progress('speedtest', `Running speed test (${testMb}MB)...`);
466
+ log(`[testNode] Running speed test (${serviceType})`);
467
+
468
+ try {
469
+ let speedResult;
470
+ if (serviceType === 'wireguard') {
471
+ speedResult = await speedtestDirect();
472
+ } else if (serviceType === 'v2ray' && connResult.socksPort) {
473
+ speedResult = await speedtestViaSocks5(testMb, connResult.socksPort);
474
+ }
475
+ if (speedResult) {
476
+ actualMbps = speedResult.mbps || 0;
477
+ log(`[testNode] Speed: ${actualMbps} Mbps (${speedResult.adaptive || 'unknown'})`);
478
+ }
479
+ } catch (speedErr) {
480
+ diag += `speedtest_failed: ${speedErr.message}; `;
481
+ log(`[testNode] Speed test failed: ${speedErr.message}`);
482
+ }
483
+
484
+ // ── Step 3: Google accessibility ──
485
+ progress('google', 'Checking Google accessibility...');
486
+ log('[testNode] Checking Google accessibility');
487
+
488
+ try {
489
+ googleAccessible = await _checkGoogleAccessible(
490
+ serviceType,
491
+ connResult.socksPort,
492
+ );
493
+ log(`[testNode] Google accessible: ${googleAccessible}`);
494
+ } catch (googleErr) {
495
+ diag += `google_check_failed: ${googleErr.message}; `;
496
+ log(`[testNode] Google check failed: ${googleErr.message}`);
497
+ }
498
+
499
+ // ── Determine pass/fail ──
500
+ // Pass: connected + has some measurable speed + Google is accessible
501
+ pass = actualMbps > 0 && googleAccessible;
502
+ if (pass) {
503
+ diag = diag || 'ok';
504
+ } else if (!googleAccessible) {
505
+ diag += 'google_unreachable; ';
506
+ }
507
+
508
+ } catch (connectErr) {
509
+ // Connection failed entirely
510
+ diag = `connect_failed: ${connectErr.code || connectErr.name}: ${connectErr.message}`;
511
+ log(`[testNode] Connection failed: ${diag}`);
512
+
513
+ // Try to extract node metadata from the error or via status probe
514
+ if (!moniker || !country) {
515
+ try {
516
+ const lcdBase = lcdUrl || DEFAULT_LCD;
517
+ const { queryNode } = await import('./cosmjs-setup.js');
518
+ const nodeInfo = await queryNode(nodeAddress, { lcdUrl: lcdBase });
519
+ if (nodeInfo.remote_url) {
520
+ const status = await nodeStatusV3(nodeInfo.remote_url);
521
+ serviceType = serviceType || status.type;
522
+ moniker = moniker || status.moniker;
523
+ country = country || status.location.country;
524
+ city = city || status.location.city;
525
+ }
526
+ } catch {
527
+ // Can't reach node at all
528
+ }
529
+ }
530
+ } finally {
531
+ // ── Step 4: Disconnect (always) ──
532
+ progress('disconnect', 'Disconnecting...');
533
+ try {
534
+ if (connResult?.cleanup) {
535
+ await connResult.cleanup();
536
+ } else {
537
+ await disconnect();
538
+ }
539
+ log('[testNode] Disconnected');
540
+ } catch (dcErr) {
541
+ log(`[testNode] Disconnect warning: ${dcErr.message}`);
542
+ }
543
+ }
544
+
545
+ const result = {
546
+ pass,
547
+ address: nodeAddress,
548
+ type: serviceType || 'unknown',
549
+ moniker,
550
+ country,
551
+ city,
552
+ actualMbps: parseFloat(actualMbps.toFixed(2)),
553
+ googleAccessible,
554
+ diag: diag.replace(/;\s*$/, '') || 'unknown',
555
+ timestamp,
556
+ };
557
+
558
+ // Record transport cache data for V2Ray nodes
559
+ if (serviceType === 'v2ray' && connResult?.outbound) {
560
+ try {
561
+ const obTag = connResult.outbound;
562
+ // Parse transport info from outbound tag (format: "proto-network-security-port")
563
+ const parts = obTag.split('-');
564
+ if (parts.length >= 2) {
565
+ const transport = {
566
+ protocol: parts[0] || 'vmess',
567
+ network: parts[1] || 'tcp',
568
+ security: parts[2] || 'none',
569
+ port: parseInt(parts[3]) || 0,
570
+ };
571
+ if (pass) {
572
+ recordTransportSuccess(nodeAddress, transport);
573
+ } else {
574
+ recordTransportFailure(transport);
575
+ }
576
+ }
577
+ } catch {
578
+ // Transport cache update failed -- non-fatal
579
+ }
580
+ }
581
+
582
+ return result;
583
+ }
584
+
585
+ // ─── auditNetwork ─────────────────────────────────────────────────────────────
586
+
587
+ /**
588
+ * Audit the Sentinel dVPN network by testing all (or some) active nodes.
589
+ *
590
+ * Flow:
591
+ * 1. Wallet setup + balance check
592
+ * 2. Fetch all active nodes from LCD
593
+ * 3. Parallel online scan (probe each node's status endpoint)
594
+ * 4. For each viable node: testNode() with retry
595
+ * 5. Emit progress events, return results + stats
596
+ *
597
+ * @param {object} options
598
+ * @param {string} options.mnemonic - BIP39 wallet mnemonic (required)
599
+ * @param {string} [options.rpcUrl] - RPC endpoint URL
600
+ * @param {string} [options.lcdUrl] - LCD endpoint URL
601
+ * @param {string} [options.v2rayExePath] - Path to v2ray binary
602
+ * @param {number} [options.concurrency=30] - Parallel status scan concurrency
603
+ * @param {number} [options.batchSize=5] - Payment batching (for display only; testNode pays individually)
604
+ * @param {number} [options.gigabytesPerNode=1] - GB per session
605
+ * @param {number} [options.testMb=5] - Speed test download size (MB)
606
+ * @param {number} [options.maxNodes=0] - 0 = test all viable nodes
607
+ * @param {Array} [options.resume=null] - Previous results array to skip already-tested nodes
608
+ * @param {function} [options.onProgress=null] - Called with each test result: (result) => {}
609
+ * @param {function} [options.onLog=null] - Log callback: (msg) => {}
610
+ * @param {function} [options.onBatchPayment=null] - Called per batch: (batchNum, total) => {}
611
+ * @param {AbortSignal} [options.signal=null] - AbortSignal for cancellation
612
+ * @returns {Promise<{
613
+ * results: Array<object>,
614
+ * stats: {
615
+ * total: number,
616
+ * tested: number,
617
+ * passed: number,
618
+ * failed: number,
619
+ * skipped: number,
620
+ * avgMbps: number,
621
+ * googleAccessiblePct: number,
622
+ * durationMs: number,
623
+ * },
624
+ * }>}
625
+ */
626
+ export async function auditNetwork(options) {
627
+ // ── Validate inputs ──
628
+ if (!options || typeof options !== 'object') {
629
+ throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'auditNetwork() requires an options object');
630
+ }
631
+ if (typeof options.mnemonic !== 'string' || options.mnemonic.trim().split(/\s+/).length < 12) {
632
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'mnemonic must be a 12+ word BIP39 string');
633
+ }
634
+
635
+ const {
636
+ mnemonic,
637
+ rpcUrl,
638
+ lcdUrl,
639
+ v2rayExePath,
640
+ concurrency = 30,
641
+ batchSize = 5,
642
+ gigabytesPerNode = 1,
643
+ testMb = 5,
644
+ maxNodes = 0,
645
+ resume = null,
646
+ onProgress = null,
647
+ onLog = null,
648
+ onBatchPayment = null,
649
+ signal = null,
650
+ } = options;
651
+
652
+ const log = (msg) => { if (onLog) onLog(msg); };
653
+ const auditStart = Date.now();
654
+ const results = [];
655
+
656
+ // Ensure cleanup handlers are registered (idempotent)
657
+ registerCleanupHandlers();
658
+
659
+ // Build skip set from resume data
660
+ const skipSet = new Set();
661
+ if (resume && Array.isArray(resume)) {
662
+ for (const r of resume) {
663
+ if (r.address) skipSet.add(r.address);
664
+ }
665
+ log(`[audit] Resuming: ${skipSet.size} nodes already tested, will skip`);
666
+ }
667
+
668
+ // ── Step 1: Wallet setup + balance check ──
669
+ log('[audit] Setting up wallet...');
670
+ _checkAborted(signal);
671
+
672
+ let walletAddress = '';
673
+ let balanceDvpn = 0;
674
+ try {
675
+ const { wallet, account } = await createWallet(mnemonic);
676
+ walletAddress = account.address;
677
+
678
+ const clientResult = rpcUrl
679
+ ? { result: await createClient(rpcUrl, wallet) }
680
+ : await tryWithFallback(RPC_ENDPOINTS, async (url) => createClient(url, wallet), 'RPC connect');
681
+ const client = clientResult.result || clientResult;
682
+ const bal = await getBalance(client, walletAddress);
683
+ balanceDvpn = bal.dvpn;
684
+ log(`[audit] Wallet: ${walletAddress} | ${balanceDvpn.toFixed(1)} P2P`);
685
+
686
+ if (balanceDvpn < 1) {
687
+ throw new ChainError(
688
+ ErrorCodes.INSUFFICIENT_BALANCE,
689
+ `Wallet has ${balanceDvpn.toFixed(2)} P2P -- need at least 1 P2P for network audit. Fund ${walletAddress}.`,
690
+ { balance: bal, address: walletAddress },
691
+ );
692
+ }
693
+ } catch (err) {
694
+ if (err.code === ErrorCodes.INSUFFICIENT_BALANCE) throw err;
695
+ log(`[audit] Wallet setup warning: ${err.message}`);
696
+ }
697
+
698
+ // ── Step 2: Fetch all active nodes ──
699
+ log('[audit] Fetching active nodes from chain...');
700
+ _checkAborted(signal);
701
+
702
+ let allNodes;
703
+ try {
704
+ allNodes = await queryOnlineNodes({
705
+ lcdUrl,
706
+ maxNodes: maxNodes > 0 ? maxNodes * 3 : 5000, // Fetch extra for filtering
707
+ concurrency,
708
+ noCache: true,
709
+ sort: true,
710
+ });
711
+ log(`[audit] Found ${allNodes.length} online nodes`);
712
+ } catch (err) {
713
+ throw new ChainError(
714
+ ErrorCodes.LCD_ERROR,
715
+ `Failed to fetch nodes: ${err.message}`,
716
+ { original: err.message },
717
+ );
718
+ }
719
+
720
+ // ── Step 3: Filter and prepare test queue ──
721
+ const brokenAddrs = new Set(BROKEN_NODES.map(n => n.address));
722
+ let viableNodes = allNodes.filter(n =>
723
+ !brokenAddrs.has(n.address) &&
724
+ !skipSet.has(n.address),
725
+ );
726
+
727
+ if (maxNodes > 0 && viableNodes.length > maxNodes) {
728
+ viableNodes = viableNodes.slice(0, maxNodes);
729
+ }
730
+
731
+ const totalToTest = viableNodes.length;
732
+ log(`[audit] Testing ${totalToTest} nodes (${skipSet.size} skipped from resume, ${brokenAddrs.size} known broken)`);
733
+
734
+ // ── Step 4: Test each node sequentially ──
735
+ // Sequential testing is required because:
736
+ // - connectDirect uses system-level tunnels (WireGuard) that conflict when parallel
737
+ // - Each test creates/tears down a tunnel -- cannot have multiple active tunnels
738
+ let testedCount = 0;
739
+ let passedCount = 0;
740
+ let failedCount = 0;
741
+ let totalMbps = 0;
742
+ let googleCount = 0;
743
+
744
+ for (let i = 0; i < viableNodes.length; i++) {
745
+ _checkAborted(signal);
746
+
747
+ const node = viableNodes[i];
748
+ const batchNum = Math.floor(i / batchSize) + 1;
749
+ const totalBatches = Math.ceil(viableNodes.length / batchSize);
750
+
751
+ if (i % batchSize === 0 && onBatchPayment) {
752
+ onBatchPayment(batchNum, totalBatches);
753
+ }
754
+
755
+ log(`[audit] [${i + 1}/${totalToTest}] Testing ${node.address} (${node.moniker || 'unknown'}, ${node.serviceType || 'unknown'})...`);
756
+
757
+ let result;
758
+ try {
759
+ result = await testNode({
760
+ mnemonic,
761
+ nodeAddress: node.address,
762
+ rpcUrl,
763
+ lcdUrl,
764
+ v2rayExePath,
765
+ gigabytes: gigabytesPerNode,
766
+ testMb,
767
+ onLog,
768
+ signal,
769
+ });
770
+ } catch (err) {
771
+ // testNode should not throw (it catches internally), but handle just in case
772
+ result = {
773
+ pass: false,
774
+ address: node.address,
775
+ type: node.serviceType || 'unknown',
776
+ moniker: node.moniker || '',
777
+ country: node.country || '',
778
+ city: node.city || '',
779
+ actualMbps: 0,
780
+ googleAccessible: false,
781
+ diag: `unexpected_error: ${err.message}`,
782
+ timestamp: new Date().toISOString(),
783
+ };
784
+ }
785
+
786
+ // Merge node metadata from scan if missing from result
787
+ if (!result.moniker && node.moniker) result.moniker = node.moniker;
788
+ if (!result.country && node.country) result.country = node.country;
789
+ if (!result.city && node.city) result.city = node.city;
790
+ if (result.type === 'unknown' && node.serviceType) result.type = node.serviceType;
791
+
792
+ results.push(result);
793
+ testedCount++;
794
+
795
+ if (result.pass) {
796
+ passedCount++;
797
+ totalMbps += result.actualMbps;
798
+ } else {
799
+ failedCount++;
800
+ }
801
+ if (result.googleAccessible) googleCount++;
802
+
803
+ // Emit progress
804
+ if (onProgress) {
805
+ try {
806
+ onProgress(result);
807
+ } catch {
808
+ // Progress callback error -- non-fatal
809
+ }
810
+ }
811
+
812
+ log(`[audit] [${i + 1}/${totalToTest}] ${result.pass ? 'PASS' : 'FAIL'} ${node.address} | ${result.actualMbps} Mbps | Google: ${result.googleAccessible}`);
813
+ }
814
+
815
+ // ── Step 5: Compute stats ──
816
+ const durationMs = Date.now() - auditStart;
817
+ const stats = {
818
+ total: totalToTest + skipSet.size,
819
+ tested: testedCount,
820
+ passed: passedCount,
821
+ failed: failedCount,
822
+ skipped: skipSet.size,
823
+ avgMbps: passedCount > 0 ? parseFloat((totalMbps / passedCount).toFixed(2)) : 0,
824
+ googleAccessiblePct: testedCount > 0
825
+ ? parseFloat(((googleCount / testedCount) * 100).toFixed(1))
826
+ : 0,
827
+ durationMs,
828
+ };
829
+
830
+ log(`[audit] Complete: ${passedCount}/${testedCount} passed, avg ${stats.avgMbps} Mbps, ${stats.googleAccessiblePct}% Google accessible, ${(durationMs / 1000 / 60).toFixed(1)} min`);
831
+
832
+ return { results, stats };
833
+ }
834
+
835
+ // ─── Internal Helpers ─────────────────────────────────────────────────────────
836
+
837
+ /**
838
+ * Check if an AbortSignal has been triggered.
839
+ * @param {AbortSignal|null} signal
840
+ * @throws {SentinelError} if aborted
841
+ * @private
842
+ */
843
+ function _checkAborted(signal) {
844
+ if (signal?.aborted) {
845
+ throw new SentinelError(ErrorCodes.ABORTED, 'Audit was cancelled');
846
+ }
847
+ }