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