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