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,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Disconnect — clean up tunnels, system proxy, kill switch, and session state.
|
|
3
|
+
*
|
|
4
|
+
* Handles graceful and emergency disconnection, cleanup handler registration,
|
|
5
|
+
* and session recovery.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
events, _defaultState, _activeStates,
|
|
10
|
+
clearWalletCache, _endSessionOnChain,
|
|
11
|
+
markCleanupRegistered, isCleanupRegistered,
|
|
12
|
+
progress, checkAborted, cachedCreateWallet, _recordMetric,
|
|
13
|
+
setAbortConnect, setConnectLock,
|
|
14
|
+
} from './state.js';
|
|
15
|
+
import { disableKillSwitch, isKillSwitchEnabled, disableDnsLeakPrevention } from './security.js';
|
|
16
|
+
import { clearSystemProxy } from './proxy.js';
|
|
17
|
+
import { killV2RayProc, killOrphanV2Ray, performHandshake, validateTunnelRequirements } from './tunnel.js';
|
|
18
|
+
|
|
19
|
+
import { disconnectWireGuard, emergencyCleanupSync } from '../wireguard.js';
|
|
20
|
+
import { flushSpeedTestDnsCache } from '../speedtest.js';
|
|
21
|
+
import {
|
|
22
|
+
clearState, recoverOrphans, markSessionActive,
|
|
23
|
+
} from '../state.js';
|
|
24
|
+
import { ValidationError, ErrorCodes } from '../errors.js';
|
|
25
|
+
import { DEFAULT_LCD, DEFAULT_TIMEOUTS } from '../defaults.js';
|
|
26
|
+
import { nodeStatusV3 } from '../v3protocol.js';
|
|
27
|
+
import { queryNode, privKeyFromMnemonic } from '../cosmjs-setup.js';
|
|
28
|
+
import { createNodeHttpsAgent } from '../tls-trust.js';
|
|
29
|
+
|
|
30
|
+
// ─── Disconnect ──────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Clean up all active tunnels and system proxy.
|
|
34
|
+
* ALWAYS call this on exit — a stale WireGuard tunnel will kill your internet.
|
|
35
|
+
*/
|
|
36
|
+
/** Disconnect a specific state instance (internal). */
|
|
37
|
+
export async function disconnectState(state) {
|
|
38
|
+
// v30: Signal any running connectAuto() retry loop to abort, and release the
|
|
39
|
+
// connection lock so the user can reconnect after disconnect completes.
|
|
40
|
+
setAbortConnect(true);
|
|
41
|
+
setConnectLock(false);
|
|
42
|
+
|
|
43
|
+
const prev = state.connection;
|
|
44
|
+
// v29: try/finally ensures state.connection is ALWAYS cleared, even if
|
|
45
|
+
// disableKillSwitch() or clearSystemProxy() throw. Previously, an exception
|
|
46
|
+
// here left state.connection set → phantom "connected" status (IP leak).
|
|
47
|
+
try {
|
|
48
|
+
if (isKillSwitchEnabled()) {
|
|
49
|
+
try { disableKillSwitch(); } catch (e) { console.warn('[sentinel-sdk] Kill switch disable warning:', e.message); }
|
|
50
|
+
}
|
|
51
|
+
if (state.systemProxy) {
|
|
52
|
+
try { clearSystemProxy(); } catch (e) { console.warn('[sentinel-sdk] System proxy clear warning:', e.message); }
|
|
53
|
+
}
|
|
54
|
+
if (state.v2rayProc) {
|
|
55
|
+
killV2RayProc(state.v2rayProc);
|
|
56
|
+
state.v2rayProc = null;
|
|
57
|
+
}
|
|
58
|
+
if (state.wgTunnel) {
|
|
59
|
+
try { await disconnectWireGuard(); } catch (e) { console.warn('[sentinel-sdk] WireGuard disconnect warning:', e.message); }
|
|
60
|
+
state.wgTunnel = null;
|
|
61
|
+
// v34: Restore DNS to DHCP after WireGuard disconnect.
|
|
62
|
+
// WireGuard config sets DNS (10.8.0.1 or custom). When the adapter is removed,
|
|
63
|
+
// the system DNS may remain changed (observed: Cloudflare 1.1.1.1 persisted after
|
|
64
|
+
// split tunnel test). Always restore to DHCP to prevent DNS leak/persistence.
|
|
65
|
+
try { disableDnsLeakPrevention(); } catch (e) { console.warn('[sentinel-sdk] DNS restore warning:', e.message); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// End session on chain (best-effort, fire-and-forget — never blocks disconnect)
|
|
69
|
+
if (prev?.sessionId && state._mnemonic) {
|
|
70
|
+
_endSessionOnChain(prev.sessionId, state._mnemonic).catch(e => {
|
|
71
|
+
console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
// ALWAYS clear connection state — even if teardown threw
|
|
76
|
+
state._mnemonic = null;
|
|
77
|
+
state.connection = null;
|
|
78
|
+
clearState();
|
|
79
|
+
clearWalletCache(); // v34: Clear cached wallet objects (private keys) from memory
|
|
80
|
+
flushSpeedTestDnsCache(); // v25: Clear stale DNS cache between connections (#14)
|
|
81
|
+
if (prev) events.emit('disconnected', { nodeAddress: prev.nodeAddress, serviceType: prev.serviceType, reason: 'user' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function disconnect() {
|
|
86
|
+
return disconnectState(_defaultState);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Cleanup Registration ───────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Register exit handlers to clean up tunnels on crash/exit.
|
|
93
|
+
* Call this once at app startup.
|
|
94
|
+
*/
|
|
95
|
+
export function registerCleanupHandlers() {
|
|
96
|
+
if (isCleanupRegistered()) return; // prevent duplicate handler stacking
|
|
97
|
+
markCleanupRegistered();
|
|
98
|
+
const orphans = recoverOrphans(); // recover state-tracked orphans from crash
|
|
99
|
+
if (orphans?.cleaned?.length) console.log('[sentinel-sdk] Recovered orphans:', orphans.cleaned.join(', '));
|
|
100
|
+
emergencyCleanupSync(); // kill stale tunnels from previous crash
|
|
101
|
+
killOrphanV2Ray(); // kill orphaned v2ray from previous crash
|
|
102
|
+
process.on('exit', () => { if (isKillSwitchEnabled()) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); });
|
|
103
|
+
process.on('SIGINT', () => { if (isKillSwitchEnabled()) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); process.exit(130); });
|
|
104
|
+
process.on('SIGTERM', () => { if (isKillSwitchEnabled()) disableKillSwitch(); clearSystemProxy(); killOrphanV2Ray(); emergencyCleanupSync(); process.exit(143); });
|
|
105
|
+
process.on('uncaughtException', (err) => {
|
|
106
|
+
console.error('Uncaught exception:', err);
|
|
107
|
+
if (isKillSwitchEnabled()) disableKillSwitch();
|
|
108
|
+
clearSystemProxy();
|
|
109
|
+
killOrphanV2Ray();
|
|
110
|
+
emergencyCleanupSync();
|
|
111
|
+
process.exit(1);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─── Session Recovery (v25) ──────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Retry handshake on an already-paid session. Use when connect fails AFTER payment.
|
|
119
|
+
* The error.details from a failed connect contains { sessionId, nodeAddress } — pass those here.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} opts - Same as connectDirect (mnemonic, v2rayExePath, etc.)
|
|
122
|
+
* @param {string|bigint} opts.sessionId - Session ID from the failed connection error
|
|
123
|
+
* @param {string} opts.nodeAddress - Node address from the failed connection error
|
|
124
|
+
* @returns {Promise<ConnectResult>}
|
|
125
|
+
*/
|
|
126
|
+
export async function recoverSession(opts) {
|
|
127
|
+
if (!opts?.sessionId) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.sessionId');
|
|
128
|
+
if (!opts?.nodeAddress) throw new ValidationError(ErrorCodes.INVALID_OPTIONS, 'recoverSession requires opts.nodeAddress');
|
|
129
|
+
if (!opts?.mnemonic) throw new ValidationError(ErrorCodes.INVALID_MNEMONIC, 'recoverSession requires opts.mnemonic');
|
|
130
|
+
|
|
131
|
+
const logFn = opts.log || console.log;
|
|
132
|
+
const onProgress = opts.onProgress || null;
|
|
133
|
+
const sessionId = BigInt(opts.sessionId);
|
|
134
|
+
const timeouts = { ...DEFAULT_TIMEOUTS, ...opts.timeouts };
|
|
135
|
+
const tlsTrust = opts.tlsTrust || 'tofu';
|
|
136
|
+
const state = opts._state || _defaultState;
|
|
137
|
+
|
|
138
|
+
// Fetch node info
|
|
139
|
+
progress(onProgress, logFn, 'recover', `Recovering session ${sessionId} on ${opts.nodeAddress}...`);
|
|
140
|
+
const nodeAgent = createNodeHttpsAgent(opts.nodeAddress, tlsTrust);
|
|
141
|
+
|
|
142
|
+
// Get node status (we need serviceType and remote URL)
|
|
143
|
+
const lcdUrl = opts.lcdUrl || DEFAULT_LCD;
|
|
144
|
+
const nodeInfo = await queryNode(opts.nodeAddress, { lcdUrl });
|
|
145
|
+
|
|
146
|
+
const status = await nodeStatusV3(nodeInfo.remote_url, nodeAgent);
|
|
147
|
+
const resolvedV2rayPath = validateTunnelRequirements(status.type, opts.v2rayExePath);
|
|
148
|
+
|
|
149
|
+
const privKey = await privKeyFromMnemonic(opts.mnemonic);
|
|
150
|
+
const extremeDrift = status.type === 'v2ray' && status.clockDriftSec !== null && Math.abs(status.clockDriftSec) > 120;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const result = await performHandshake({
|
|
154
|
+
serviceType: status.type,
|
|
155
|
+
remoteUrl: nodeInfo.remote_url,
|
|
156
|
+
serverHost: new URL(nodeInfo.remote_url).hostname,
|
|
157
|
+
sessionId,
|
|
158
|
+
privKey,
|
|
159
|
+
v2rayExePath: resolvedV2rayPath,
|
|
160
|
+
fullTunnel: opts.fullTunnel !== false,
|
|
161
|
+
splitIPs: opts.splitIPs,
|
|
162
|
+
systemProxy: opts.systemProxy === true,
|
|
163
|
+
dns: opts.dns,
|
|
164
|
+
onProgress,
|
|
165
|
+
logFn,
|
|
166
|
+
extremeDrift,
|
|
167
|
+
clockDriftSec: status.clockDriftSec,
|
|
168
|
+
nodeAddress: opts.nodeAddress,
|
|
169
|
+
timeouts,
|
|
170
|
+
signal: opts.signal,
|
|
171
|
+
nodeAgent,
|
|
172
|
+
state,
|
|
173
|
+
});
|
|
174
|
+
markSessionActive(String(sessionId), opts.nodeAddress);
|
|
175
|
+
events.emit('connected', { sessionId, serviceType: status.type, nodeAddress: opts.nodeAddress });
|
|
176
|
+
return result;
|
|
177
|
+
} finally {
|
|
178
|
+
privKey.fill(0);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node Discovery — query, fetch, enrich, index, and score nodes.
|
|
3
|
+
*
|
|
4
|
+
* Handles LCD queries for online nodes, caching, quality scoring,
|
|
5
|
+
* and geographic indexing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
fetchActiveNodes, filterNodes, resolveNodeUrl,
|
|
10
|
+
} from '../cosmjs-setup.js';
|
|
11
|
+
import { nodeStatusV3 } from '../v3protocol.js';
|
|
12
|
+
import {
|
|
13
|
+
BROKEN_NODES, tryWithFallback, LCD_ENDPOINTS, LAST_VERIFIED,
|
|
14
|
+
} from '../defaults.js';
|
|
15
|
+
|
|
16
|
+
// ─── Node List Cache ─────────────────────────────────────────────────────────
|
|
17
|
+
// v21: Cache queryOnlineNodes results for 5 minutes. Returns cached results
|
|
18
|
+
// immediately on repeat calls and refreshes in background if stale.
|
|
19
|
+
// v25: Deduplicated concurrent refreshes + flushNodeCache() export.
|
|
20
|
+
|
|
21
|
+
const NODE_CACHE_TTL = 5 * 60_000; // 5 minutes
|
|
22
|
+
let _nodeCache = null; // { nodes, timestamp, key }
|
|
23
|
+
let _inflightRefresh = null; // Promise — prevents duplicate concurrent refreshes
|
|
24
|
+
|
|
25
|
+
/** Clear the node list cache. Next queryOnlineNodes() call will fetch fresh data. */
|
|
26
|
+
export function flushNodeCache() {
|
|
27
|
+
_nodeCache = null;
|
|
28
|
+
_inflightRefresh = null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Node Quality Scoring ───────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Score a node's expected connection quality (0-100).
|
|
35
|
+
* Based on real success rates from 400+ node tests.
|
|
36
|
+
* Higher = more likely to produce a working tunnel.
|
|
37
|
+
*/
|
|
38
|
+
export function scoreNode(status) {
|
|
39
|
+
let score = 50; // baseline
|
|
40
|
+
|
|
41
|
+
// WireGuard is simpler and more reliable than V2Ray
|
|
42
|
+
if (status.type === 'wireguard') score += 20;
|
|
43
|
+
|
|
44
|
+
// Clock drift penalty — VMess fails at >120s, VLess is immune.
|
|
45
|
+
// We can't know VMess vs VLess until handshake, but high drift is still risky.
|
|
46
|
+
if (status.clockDriftSec !== null) {
|
|
47
|
+
const drift = Math.abs(status.clockDriftSec);
|
|
48
|
+
if (drift > 120) score -= 40; // VMess will fail entirely (VLess OK but rare)
|
|
49
|
+
else if (drift > 60) score -= 15;
|
|
50
|
+
else if (drift > 30) score -= 5;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Peer count — fewer peers = less congestion
|
|
54
|
+
if (status.peers !== undefined) {
|
|
55
|
+
if (status.peers === 0) score += 10; // empty node = fast
|
|
56
|
+
else if (status.peers < 5) score += 5;
|
|
57
|
+
else if (status.peers > 20) score -= 10;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return Math.max(0, Math.min(100, score));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─── Query Nodes ─────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Fetch active nodes from LCD and check which are actually online.
|
|
67
|
+
* Returns array sorted by quality score (best first).
|
|
68
|
+
*
|
|
69
|
+
* Built-in quality scoring (from 400+ node tests):
|
|
70
|
+
* - WireGuard nodes scored higher than V2Ray (simpler tunnel, fewer failure modes)
|
|
71
|
+
* - V2Ray with grpc/tls deprioritized (0% success rate in testing)
|
|
72
|
+
* - High clock drift nodes penalized (VMess fails silently at >120s)
|
|
73
|
+
* - Nodes with fewer peers scored higher (less congestion)
|
|
74
|
+
*
|
|
75
|
+
* @param {object} options
|
|
76
|
+
* @param {string} options.lcdUrl - LCD endpoint (default: https://lcd.sentinel.co)
|
|
77
|
+
* @param {string} options.serviceType - Filter: 'wireguard' | 'v2ray' | null (both)
|
|
78
|
+
* @param {number} options.maxNodes - Max nodes to check online status (default: 100)
|
|
79
|
+
* @param {number} options.concurrency - Parallel online checks (default: 20)
|
|
80
|
+
* @param {boolean} options.sort - Sort by quality score, best first (default: true). Set false for random order.
|
|
81
|
+
*/
|
|
82
|
+
export async function queryOnlineNodes(options = {}) {
|
|
83
|
+
// v25: waitForFresh skips cache entirely
|
|
84
|
+
if (options.waitForFresh) {
|
|
85
|
+
const nodes = await _queryOnlineNodesImpl(options);
|
|
86
|
+
_nodeCache = { nodes, timestamp: Date.now(), key: `${options.lcdUrl || 'default'}_${options.serviceType || 'all'}_${options.maxNodes || 100}` };
|
|
87
|
+
return nodes;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// v21: Node cache — return cached results if fresh, background-refresh if stale
|
|
91
|
+
const cacheKey = `${options.lcdUrl || 'default'}_${options.serviceType || 'all'}_${options.maxNodes || 100}`;
|
|
92
|
+
if (!options.noCache && _nodeCache && _nodeCache.key === cacheKey && Date.now() - _nodeCache.timestamp < NODE_CACHE_TTL) {
|
|
93
|
+
// Cache hit — fire deduplicated background refresh but return instantly
|
|
94
|
+
if (!_inflightRefresh) {
|
|
95
|
+
_inflightRefresh = _queryOnlineNodesImpl(options).then(nodes => {
|
|
96
|
+
_nodeCache = { nodes, timestamp: Date.now(), key: cacheKey };
|
|
97
|
+
}).catch(e => {
|
|
98
|
+
if (typeof console !== 'undefined') console.warn('[sentinel-sdk] Node cache refresh failed:', e.message);
|
|
99
|
+
}).finally(() => { _inflightRefresh = null; });
|
|
100
|
+
}
|
|
101
|
+
return _nodeCache.nodes;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// No cache — deduplicate concurrent cold fetches
|
|
105
|
+
if (!_inflightRefresh) {
|
|
106
|
+
_inflightRefresh = _queryOnlineNodesImpl(options).then(nodes => {
|
|
107
|
+
_nodeCache = { nodes, timestamp: Date.now(), key: cacheKey };
|
|
108
|
+
return nodes;
|
|
109
|
+
}).finally(() => { _inflightRefresh = null; });
|
|
110
|
+
}
|
|
111
|
+
const nodes = await _inflightRefresh;
|
|
112
|
+
return nodes || _nodeCache?.nodes || [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function _queryOnlineNodesImpl(options = {}) {
|
|
116
|
+
const maxNodes = options.maxNodes || 5000; // v25b: raised from 100 — chain has 1000+ nodes
|
|
117
|
+
const concurrency = options.concurrency || 20;
|
|
118
|
+
const shouldSort = options.sort !== false; // default true
|
|
119
|
+
const logFn = options.log || null;
|
|
120
|
+
const brokenAddrs = new Set(BROKEN_NODES.map(n => n.address));
|
|
121
|
+
|
|
122
|
+
// 1. Fetch ALL active nodes from LCD — uses lcdPaginatedSafe (handles broken pagination)
|
|
123
|
+
let nodes = [];
|
|
124
|
+
if (options.lcdUrl) {
|
|
125
|
+
nodes = await fetchActiveNodes(options.lcdUrl);
|
|
126
|
+
} else {
|
|
127
|
+
const { result } = await tryWithFallback(LCD_ENDPOINTS, fetchActiveNodes, 'LCD node list');
|
|
128
|
+
nodes = result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Resolve remote_addrs → remote_url (LCD v3 returns "IP:PORT" array, not "https://..." string)
|
|
132
|
+
nodes = nodes.map(n => {
|
|
133
|
+
try { n.remote_url = resolveNodeUrl(n); } catch { n.remote_url = null; }
|
|
134
|
+
return n;
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Filter: must accept udvpn, must have URL, skip known broken nodes (verified ${LAST_VERIFIED})
|
|
138
|
+
nodes = nodes.filter(n =>
|
|
139
|
+
n.remote_url &&
|
|
140
|
+
!brokenAddrs.has(n.address) &&
|
|
141
|
+
(n.gigabyte_prices || []).some(p => p.denom === 'udvpn')
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Warn if maxNodes truncates results
|
|
145
|
+
if (maxNodes < nodes.length && logFn) {
|
|
146
|
+
logFn(`[queryOnlineNodes] Warning: ${nodes.length} nodes on chain, returning ${maxNodes} (capped by maxNodes)`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Shuffle and limit
|
|
150
|
+
// Fisher-Yates shuffle (unbiased)
|
|
151
|
+
for (let i = nodes.length - 1; i > 0; i--) {
|
|
152
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
153
|
+
[nodes[i], nodes[j]] = [nodes[j], nodes[i]];
|
|
154
|
+
}
|
|
155
|
+
nodes = nodes.slice(0, maxNodes);
|
|
156
|
+
|
|
157
|
+
// 2. Check online status in parallel batches
|
|
158
|
+
const online = [];
|
|
159
|
+
let probed = 0;
|
|
160
|
+
const onNodeProbed = options.onNodeProbed; // callback: ({ total, probed, online }) => void
|
|
161
|
+
for (let i = 0; i < nodes.length; i += concurrency) {
|
|
162
|
+
const batch = nodes.slice(i, i + concurrency);
|
|
163
|
+
const results = await Promise.allSettled(
|
|
164
|
+
batch.map(async (node) => {
|
|
165
|
+
const status = await nodeStatusV3(node.remote_url);
|
|
166
|
+
if (options.serviceType && status.type !== options.serviceType) return null;
|
|
167
|
+
return {
|
|
168
|
+
address: node.address,
|
|
169
|
+
remoteUrl: node.remote_url,
|
|
170
|
+
serviceType: status.type,
|
|
171
|
+
moniker: status.moniker,
|
|
172
|
+
country: status.location.country,
|
|
173
|
+
city: status.location.city,
|
|
174
|
+
peers: status.peers,
|
|
175
|
+
clockDriftSec: status.clockDriftSec,
|
|
176
|
+
gigabytePrices: node.gigabyte_prices,
|
|
177
|
+
hourlyPrices: node.hourly_prices,
|
|
178
|
+
qualityScore: scoreNode(status),
|
|
179
|
+
};
|
|
180
|
+
})
|
|
181
|
+
);
|
|
182
|
+
for (const r of results) {
|
|
183
|
+
if (r.status === 'fulfilled' && r.value) online.push(r.value);
|
|
184
|
+
}
|
|
185
|
+
probed += batch.length;
|
|
186
|
+
if (onNodeProbed) try { onNodeProbed({ total: nodes.length, probed, online: online.length }); } catch {}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// 3. Sort by quality score (best first) unless disabled
|
|
190
|
+
if (shouldSort) {
|
|
191
|
+
online.sort((a, b) => b.qualityScore - a.qualityScore);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return online;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ─── Full Node Catalog (LCD only, no per-node status checks) ────────────────
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Fetch ALL active nodes from the LCD. No per-node HTTP checks — instant.
|
|
201
|
+
*
|
|
202
|
+
* Returns every node that accepts udvpn, with LCD data only:
|
|
203
|
+
* address, remote_url, gigabyte_prices, hourly_prices.
|
|
204
|
+
*
|
|
205
|
+
* Use this for: building node lists/maps, country pickers, price comparisons.
|
|
206
|
+
* Use queryOnlineNodes() when you need verified online status + quality scores.
|
|
207
|
+
*
|
|
208
|
+
* @param {object} [options]
|
|
209
|
+
* @param {string} [options.lcdUrl] - LCD endpoint (uses fallback chain if omitted)
|
|
210
|
+
* @returns {Promise<Array>} All active nodes (900+)
|
|
211
|
+
*/
|
|
212
|
+
export async function fetchAllNodes(options = {}) {
|
|
213
|
+
let nodes;
|
|
214
|
+
if (options.lcdUrl) {
|
|
215
|
+
nodes = await fetchActiveNodes(options.lcdUrl);
|
|
216
|
+
} else {
|
|
217
|
+
const { result } = await tryWithFallback(
|
|
218
|
+
LCD_ENDPOINTS,
|
|
219
|
+
async (url) => fetchActiveNodes(url),
|
|
220
|
+
'LCD full node list',
|
|
221
|
+
);
|
|
222
|
+
nodes = result;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Filter: must accept udvpn, must have a resolvable URL
|
|
226
|
+
return nodes.filter(n =>
|
|
227
|
+
n.remote_url &&
|
|
228
|
+
(n.gigabyte_prices || []).some(p => p.denom === 'udvpn')
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Build a geographic index from a node list for instant country/city lookups.
|
|
234
|
+
*
|
|
235
|
+
* Requires enriched nodes (with country/city fields from nodeStatusV3).
|
|
236
|
+
* For LCD-only nodes, call enrichNodes() first.
|
|
237
|
+
*
|
|
238
|
+
* @param {Array} nodes - Array of node objects with country/city fields
|
|
239
|
+
* @returns {{ countries: Object, cities: Object, stats: Object }}
|
|
240
|
+
* - countries: { "Germany": [node, ...], "United States": [...] }
|
|
241
|
+
* - cities: { "Berlin": [node, ...], "New York": [...] }
|
|
242
|
+
* - stats: { totalNodes, totalCountries, totalCities, byCountry: [{country, count}] }
|
|
243
|
+
*/
|
|
244
|
+
export function buildNodeIndex(nodes) {
|
|
245
|
+
const countries = {};
|
|
246
|
+
const cities = {};
|
|
247
|
+
|
|
248
|
+
for (const node of nodes) {
|
|
249
|
+
const country = node.country || node.location?.country || 'Unknown';
|
|
250
|
+
const city = node.city || node.location?.city || 'Unknown';
|
|
251
|
+
|
|
252
|
+
if (!countries[country]) countries[country] = [];
|
|
253
|
+
countries[country].push(node);
|
|
254
|
+
|
|
255
|
+
const cityKey = city === 'Unknown' ? `${city} (${country})` : city;
|
|
256
|
+
if (!cities[cityKey]) cities[cityKey] = [];
|
|
257
|
+
cities[cityKey].push(node);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Stats sorted by node count (most nodes first)
|
|
261
|
+
const byCountry = Object.entries(countries)
|
|
262
|
+
.map(([country, nodes]) => ({ country, count: nodes.length }))
|
|
263
|
+
.sort((a, b) => b.count - a.count);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
countries,
|
|
267
|
+
cities,
|
|
268
|
+
stats: {
|
|
269
|
+
totalNodes: nodes.length,
|
|
270
|
+
totalCountries: Object.keys(countries).length,
|
|
271
|
+
totalCities: Object.keys(cities).length,
|
|
272
|
+
byCountry,
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Enrich LCD nodes with type/country/city by probing each node's status API.
|
|
279
|
+
*
|
|
280
|
+
* @param {Array} nodes - Raw LCD nodes from fetchAllNodes()
|
|
281
|
+
* @param {object} [options]
|
|
282
|
+
* @param {number} [options.concurrency=30] - Parallel probes
|
|
283
|
+
* @param {function} [options.onProgress] - Callback: ({ total, done, enriched }) => void
|
|
284
|
+
* @returns {Promise<Array>} Enriched nodes with serviceType, country, city, moniker, qualityScore
|
|
285
|
+
*/
|
|
286
|
+
export async function enrichNodes(nodes, options = {}) {
|
|
287
|
+
const concurrency = options.concurrency || 30;
|
|
288
|
+
const enriched = [];
|
|
289
|
+
let done = 0;
|
|
290
|
+
|
|
291
|
+
for (let i = 0; i < nodes.length; i += concurrency) {
|
|
292
|
+
const batch = nodes.slice(i, i + concurrency);
|
|
293
|
+
const results = await Promise.allSettled(
|
|
294
|
+
batch.map(async (node) => {
|
|
295
|
+
const status = await nodeStatusV3(node.remote_url);
|
|
296
|
+
return {
|
|
297
|
+
address: node.address,
|
|
298
|
+
remoteUrl: node.remote_url,
|
|
299
|
+
serviceType: status.type,
|
|
300
|
+
moniker: status.moniker,
|
|
301
|
+
country: status.location.country,
|
|
302
|
+
city: status.location.city,
|
|
303
|
+
peers: status.peers,
|
|
304
|
+
clockDriftSec: status.clockDriftSec,
|
|
305
|
+
gigabytePrices: node.gigabyte_prices,
|
|
306
|
+
hourlyPrices: node.hourly_prices,
|
|
307
|
+
qualityScore: scoreNode(status),
|
|
308
|
+
};
|
|
309
|
+
})
|
|
310
|
+
);
|
|
311
|
+
for (const r of results) {
|
|
312
|
+
if (r.status === 'fulfilled' && r.value) enriched.push(r.value);
|
|
313
|
+
}
|
|
314
|
+
done += batch.length;
|
|
315
|
+
if (options.onProgress) {
|
|
316
|
+
try { options.onProgress({ total: nodes.length, done, enriched: enriched.length }); } catch {}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return enriched;
|
|
321
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection Module — barrel re-export of all connection submodules.
|
|
3
|
+
*
|
|
4
|
+
* This is the single entry point for the connection/ directory.
|
|
5
|
+
* node-connect.js re-exports from here for backwards compatibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── State ───────────────────────────────────────────────────────────────────
|
|
9
|
+
export {
|
|
10
|
+
events,
|
|
11
|
+
ConnectionState,
|
|
12
|
+
_defaultState,
|
|
13
|
+
isConnecting,
|
|
14
|
+
clearWalletCache,
|
|
15
|
+
getConnectionMetrics,
|
|
16
|
+
isConnected,
|
|
17
|
+
getStatus,
|
|
18
|
+
verifyConnection,
|
|
19
|
+
} from './state.js';
|
|
20
|
+
|
|
21
|
+
// ─── Connect ─────────────────────────────────────────────────────────────────
|
|
22
|
+
export {
|
|
23
|
+
connectDirect,
|
|
24
|
+
connectAuto,
|
|
25
|
+
connectViaPlan,
|
|
26
|
+
connectViaSubscription,
|
|
27
|
+
quickConnect,
|
|
28
|
+
createConnectConfig,
|
|
29
|
+
} from './connect.js';
|
|
30
|
+
|
|
31
|
+
// ─── Disconnect ──────────────────────────────────────────────────────────────
|
|
32
|
+
export {
|
|
33
|
+
disconnect,
|
|
34
|
+
disconnectState,
|
|
35
|
+
registerCleanupHandlers,
|
|
36
|
+
recoverSession,
|
|
37
|
+
} from './disconnect.js';
|
|
38
|
+
|
|
39
|
+
// ─── Discovery ───────────────────────────────────────────────────────────────
|
|
40
|
+
export {
|
|
41
|
+
queryOnlineNodes,
|
|
42
|
+
fetchAllNodes,
|
|
43
|
+
enrichNodes,
|
|
44
|
+
buildNodeIndex,
|
|
45
|
+
flushNodeCache,
|
|
46
|
+
} from './discovery.js';
|
|
47
|
+
|
|
48
|
+
// ─── Security ────────────────────────────────────────────────────────────────
|
|
49
|
+
export {
|
|
50
|
+
enableKillSwitch,
|
|
51
|
+
disableKillSwitch,
|
|
52
|
+
isKillSwitchEnabled,
|
|
53
|
+
enableDnsLeakPrevention,
|
|
54
|
+
disableDnsLeakPrevention,
|
|
55
|
+
} from './security.js';
|
|
56
|
+
|
|
57
|
+
// ─── Resilience ──────────────────────────────────────────────────────────────
|
|
58
|
+
export {
|
|
59
|
+
resetCircuitBreaker,
|
|
60
|
+
configureCircuitBreaker,
|
|
61
|
+
getCircuitBreakerStatus,
|
|
62
|
+
autoReconnect,
|
|
63
|
+
tryFastReconnect,
|
|
64
|
+
} from './resilience.js';
|
|
65
|
+
|
|
66
|
+
// ─── Proxy ───────────────────────────────────────────────────────────────────
|
|
67
|
+
export {
|
|
68
|
+
setSystemProxy,
|
|
69
|
+
clearSystemProxy,
|
|
70
|
+
checkPortFree,
|
|
71
|
+
} from './proxy.js';
|
|
72
|
+
|
|
73
|
+
// ─── Tunnel ──────────────────────────────────────────────────────────────────
|
|
74
|
+
export {
|
|
75
|
+
verifyDependencies,
|
|
76
|
+
} from './tunnel.js';
|