blue-js-sdk 2.4.0 → 2.6.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.
@@ -28,13 +28,35 @@ import { queryNode, privKeyFromMnemonic } from '../cosmjs-setup.js';
28
28
  import { createNodeHttpsAgent } from '../tls-trust.js';
29
29
 
30
30
  // ─── Disconnect ──────────────────────────────────────────────────────────────
31
+ //
32
+ // TWO DISCONNECT PATHS — CHOOSE INTENT EXPLICITLY.
33
+ //
34
+ // Soft: disconnect() / disconnectState(state)
35
+ // - Tears down the local tunnel (WireGuard/V2Ray) and cleans up system state.
36
+ // - Leaves the on-chain session in status=1 (active).
37
+ // - Next connectDirect() to the SAME node reuses the session via
38
+ // findExistingSession — no new MsgStartSession TX, no new payment, bandwidth preserved.
39
+ // - Use when: user is pausing, network changed, closing the app temporarily.
40
+ //
41
+ // Hard: disconnectAndEndSession() / disconnectStateAndEndSession(state)
42
+ // - Tears down the tunnel AND broadcasts MsgCancelSession on chain.
43
+ // - Session moves status=1 → settling → refund after ~2h settlement window.
44
+ // - Use when: user is done with this node, switching nodes permanently,
45
+ // or wants the bandwidth deposit back.
46
+ //
47
+ // Internal: _disconnectInternal(state, { endSession })
48
+ // - Caller MUST pass endSession explicitly as true or false.
49
+ // - No default — forces intentional choice at every callsite.
31
50
 
32
51
  /**
33
- * Clean up all active tunnels and system proxy.
34
- * ALWAYS call this on exit — a stale WireGuard tunnel will kill your internet.
52
+ * Internal disconnect implementation. Caller must explicitly pass endSession.
53
+ * @param {object} state - ConnectionState instance
54
+ * @param {{ endSession: boolean }} opts
55
+ * endSession: true → broadcast MsgCancelSession (hard disconnect)
56
+ * endSession: false → preserve on-chain session for reuse (soft disconnect)
57
+ * @private
35
58
  */
36
- /** Disconnect a specific state instance (internal). */
37
- export async function disconnectState(state) {
59
+ async function _disconnectInternal(state, { endSession }) {
38
60
  // v30: Signal any running connectAuto() retry loop to abort, and release the
39
61
  // connection lock so the user can reconnect after disconnect completes.
40
62
  setAbortConnect(true);
@@ -65,11 +87,18 @@ export async function disconnectState(state) {
65
87
  try { disableDnsLeakPrevention(); } catch (e) { console.warn('[sentinel-sdk] DNS restore warning:', e.message); }
66
88
  }
67
89
 
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
- });
90
+ if (endSession) {
91
+ // Hard disconnect: broadcast MsgCancelSession — session settles and refunds deposit.
92
+ if (prev?.sessionId && state._mnemonic) {
93
+ _endSessionOnChain(prev.sessionId, state._mnemonic).catch(e => {
94
+ console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
95
+ });
96
+ }
97
+ } else {
98
+ // Soft disconnect: leave session on chain in status=1 for reuse.
99
+ if (prev?.sessionId) {
100
+ console.log(`[sentinel-sdk] Session ${prev.sessionId} preserved on chain (status=1) for future reuse — no MsgCancelSession broadcast.`);
101
+ }
73
102
  }
74
103
  } finally {
75
104
  // ALWAYS clear connection state — even if teardown threw
@@ -82,8 +111,55 @@ export async function disconnectState(state) {
82
111
  }
83
112
  }
84
113
 
114
+ /**
115
+ * Soft disconnect — tear down the local tunnel, leave the on-chain session active.
116
+ *
117
+ * A subsequent connectDirect() to the SAME node will reuse the session via
118
+ * findExistingSession — no new MsgStartSession TX, no new payment, remaining
119
+ * bandwidth is preserved.
120
+ *
121
+ * Use when: user is pausing, network changed, or closing the app temporarily.
122
+ * To settle the session on-chain and reclaim the unused deposit, use
123
+ * disconnectAndEndSession() instead.
124
+ *
125
+ * @param {object} [state] - ConnectionState instance (defaults to _defaultState)
126
+ */
127
+ export async function disconnectState(state) {
128
+ return _disconnectInternal(state, { endSession: false });
129
+ }
130
+
131
+ /**
132
+ * Soft disconnect (default state) — tear down the tunnel, leave on-chain session active.
133
+ *
134
+ * @see disconnectState
135
+ */
85
136
  export async function disconnect() {
86
- return disconnectState(_defaultState);
137
+ return _disconnectInternal(_defaultState, { endSession: false });
138
+ }
139
+
140
+ /**
141
+ * Hard disconnect — tear down the tunnel AND broadcast MsgCancelSession on chain.
142
+ *
143
+ * The session settles after the ~2h inactive_pending window. The node refunds
144
+ * the unused portion of the bandwidth deposit (for peer-to-peer sessions).
145
+ * For plan-based sessions, this stops metering against the plan allocation.
146
+ *
147
+ * Use when: user is done with this node (switching nodes permanently, ending
148
+ * their session, or wants the deposit back).
149
+ *
150
+ * @param {object} [state] - ConnectionState instance (defaults to _defaultState)
151
+ */
152
+ export async function disconnectStateAndEndSession(state) {
153
+ return _disconnectInternal(state, { endSession: true });
154
+ }
155
+
156
+ /**
157
+ * Hard disconnect (default state) — tear down the tunnel AND broadcast MsgCancelSession.
158
+ *
159
+ * @see disconnectStateAndEndSession
160
+ */
161
+ export async function disconnectAndEndSession() {
162
+ return _disconnectInternal(_defaultState, { endSession: true });
87
163
  }
88
164
 
89
165
  // ─── Cleanup Registration ───────────────────────────────────────────────────
@@ -32,6 +32,8 @@ export {
32
32
  export {
33
33
  disconnect,
34
34
  disconnectState,
35
+ disconnectAndEndSession,
36
+ disconnectStateAndEndSession,
35
37
  registerCleanupHandlers,
36
38
  recoverSession,
37
39
  } from './disconnect.js';
package/cosmjs-setup.js CHANGED
@@ -87,6 +87,8 @@ import {
87
87
  getExpiringGrants as _getExpiringGrants,
88
88
  renewExpiringGrants as _renewExpiringGrants,
89
89
  monitorFeeGrants as _monitorFeeGrants,
90
+ streamGrantPlanSubscribers as _streamGrantPlanSubscribers,
91
+ computeFeeGrantGasCosts as _computeFeeGrantGasCosts,
90
92
  } from './chain/fee-grants.js';
91
93
 
92
94
  // ─── Input Validation Helpers ────────────────────────────────────────────────
@@ -485,9 +487,15 @@ export async function getBalance(client, address) {
485
487
  * Returns session ID (BigInt) or null. Use this to avoid double-paying.
486
488
  *
487
489
  * Note: Sessions have a nested base_session object containing the actual data.
490
+ *
491
+ * @param {string} lcdUrl - LCD endpoint URL
492
+ * @param {string} walletAddr - sent1... wallet address
493
+ * @param {string} nodeAddr - sentnode1... node address
494
+ * @param {object} [opts] - Optional. Pass `{ onStaleDuplicate: (BigInt) => void }` to
495
+ * receive fire-and-forget cancellation callbacks for stale duplicate sessions.
488
496
  */
489
- export async function findExistingSession(lcdUrl, walletAddr, nodeAddr) {
490
- return _findExistingSession(lcdUrl, walletAddr, nodeAddr);
497
+ export async function findExistingSession(lcdUrl, walletAddr, nodeAddr, opts) {
498
+ return _findExistingSession(lcdUrl, walletAddr, nodeAddr, opts);
491
499
  }
492
500
 
493
501
  /**
@@ -1006,6 +1014,22 @@ export function monitorFeeGrants(opts = {}) {
1006
1014
  return _monitorFeeGrants(opts);
1007
1015
  }
1008
1016
 
1017
+ /**
1018
+ * Stream progress as we grant fee allowances to all plan subscribers in batches.
1019
+ * Async generator. See chain/fee-grants.js for event shapes.
1020
+ */
1021
+ export function streamGrantPlanSubscribers(planId, opts = {}) {
1022
+ return _streamGrantPlanSubscribers(planId, opts);
1023
+ }
1024
+
1025
+ /**
1026
+ * Sum udvpn fees the granter has paid on behalf of a plan's subscribers.
1027
+ * Iterates subscribers, pulls TX history, filters on fee.granter.
1028
+ */
1029
+ export async function computeFeeGrantGasCosts(planId, opts = {}) {
1030
+ return _computeFeeGrantGasCosts(planId, opts);
1031
+ }
1032
+
1009
1033
  // ─── Query Helpers (v25c) ────────────────────────────────────────────────────
1010
1034
 
1011
1035
  /**
package/index.js CHANGED
@@ -27,6 +27,9 @@ export {
27
27
  enrichNodes,
28
28
  buildNodeIndex,
29
29
  disconnect,
30
+ disconnectAndEndSession,
31
+ disconnectState,
32
+ disconnectStateAndEndSession,
30
33
  isConnected,
31
34
  getStatus,
32
35
  registerCleanupHandlers,
@@ -52,7 +55,6 @@ export {
52
55
  disableDnsLeakPrevention,
53
56
  events,
54
57
  ConnectionState,
55
- disconnectState,
56
58
  tryFastReconnect,
57
59
  } from './node-connect.js';
58
60
 
@@ -311,6 +313,7 @@ export {
311
313
  rpcQueryFeeGrantsIssued,
312
314
  rpcQueryAuthzGrants,
313
315
  rpcQueryProvider,
316
+ rpcGetTxByHash,
314
317
  } from './chain/rpc.js';
315
318
 
316
319
  // ─── Subscription Sharing (plan operator → user onboarding) ────────────────
@@ -323,6 +326,7 @@ export {
323
326
 
324
327
  export {
325
328
  querySubscriptionAllocations,
329
+ getTxByHash,
326
330
  } from './chain/queries.js';
327
331
 
328
332
  // ─── TypeScript Client (extends CosmJS SigningStargateClient) ───────────────
package/node-connect.js CHANGED
@@ -1011,7 +1011,17 @@ export async function connectDirect(opts) {
1011
1011
  if (!forceNewSession) {
1012
1012
  progress(onProgress, logFn, 'session', 'Checking for existing session...');
1013
1013
  checkAborted(signal);
1014
- sessionId = await findExistingSession(lcd, account.address, opts.nodeAddress);
1014
+ sessionId = await findExistingSession(lcd, account.address, opts.nodeAddress, {
1015
+ // Dedup: if multiple active sessions exist for this node (stale duplicates from
1016
+ // crashes or multi-client wallets), keep the highest-ID one. Silently cancel stale
1017
+ // lower-ID sessions before MsgStartSession — mirrors C# SessionManager dedup pattern.
1018
+ onStaleDuplicate: (staleId) => {
1019
+ logFn?.(`[connect] Cancelling stale duplicate session ${staleId} on ${opts.nodeAddress}...`);
1020
+ _endSessionOnChain(staleId, opts.mnemonic, opts.feeGranter || null).catch(e => {
1021
+ logFn?.(`[connect] Failed to cancel stale session ${staleId}: ${e.message}`);
1022
+ });
1023
+ },
1024
+ });
1015
1025
  if (sessionId && isSessionPoisoned(String(sessionId))) {
1016
1026
  progress(onProgress, logFn, 'session', `Session ${sessionId} previously failed — skipping`);
1017
1027
  sessionId = null;
@@ -1460,8 +1470,10 @@ async function connectInternal(opts, paymentStrategy, retryStrategy, state = _de
1460
1470
  { nodeAddress: state.connection?.nodeAddress });
1461
1471
  }
1462
1472
  const prev = state.connection;
1463
- await disconnectState(state);
1464
- if (opts.log || defaultLog) (opts.log || defaultLog)(`[connect] Disconnected from ${prev?.nodeAddress || 'previous node'}`);
1473
+ // Hard disconnect: user is actively connecting to a different node,
1474
+ // so the old session should be settled and the deposit refunded.
1475
+ await disconnectStateAndEndSession(state);
1476
+ if (opts.log || defaultLog) (opts.log || defaultLog)(`[connect] Ended session on ${prev?.nodeAddress || 'previous node'} — connecting to new node`);
1465
1477
  }
1466
1478
 
1467
1479
  const onProgress = opts.onProgress || null;
@@ -2267,13 +2279,35 @@ function formatUptime(ms) {
2267
2279
  }
2268
2280
 
2269
2281
  // ─── Disconnect ──────────────────────────────────────────────────────────────
2282
+ //
2283
+ // TWO DISCONNECT PATHS — CHOOSE INTENT EXPLICITLY.
2284
+ //
2285
+ // Soft: disconnect() / disconnectState(state)
2286
+ // - Tears down the local tunnel (WireGuard/V2Ray) and cleans up system state.
2287
+ // - Leaves the on-chain session in status=1 (active).
2288
+ // - Next connectDirect() to the SAME node reuses the session via
2289
+ // findExistingSession — no new MsgStartSession TX, no new payment, bandwidth preserved.
2290
+ // - Use when: user is pausing, network changed, closing the app temporarily.
2291
+ //
2292
+ // Hard: disconnectAndEndSession() / disconnectStateAndEndSession(state)
2293
+ // - Tears down the tunnel AND broadcasts MsgCancelSession on chain.
2294
+ // - Session moves status=1 → settling → refund after ~2h settlement window.
2295
+ // - Use when: user is done with this node, switching nodes permanently,
2296
+ // or wants the bandwidth deposit back.
2297
+ //
2298
+ // Internal: _disconnectInternal(state, { endSession })
2299
+ // - Caller MUST pass endSession explicitly as true or false.
2300
+ // - No default — forces intentional choice at every callsite.
2270
2301
 
2271
2302
  /**
2272
- * Clean up all active tunnels and system proxy.
2273
- * ALWAYS call this on exit — a stale WireGuard tunnel will kill your internet.
2303
+ * Internal disconnect implementation. Caller must explicitly pass endSession.
2304
+ * @param {object} state - ConnectionState instance
2305
+ * @param {{ endSession: boolean }} opts
2306
+ * endSession: true → broadcast MsgCancelSession (hard disconnect)
2307
+ * endSession: false → preserve on-chain session for reuse (soft disconnect)
2308
+ * @private
2274
2309
  */
2275
- /** Disconnect a specific state instance (internal). */
2276
- export async function disconnectState(state) {
2310
+ async function _disconnectInternal(state, { endSession }) {
2277
2311
  // v30: Signal any running connectAuto() retry loop to abort, and release the
2278
2312
  // connection lock so the user can reconnect after disconnect completes.
2279
2313
  _abortConnect = true;
@@ -2299,11 +2333,18 @@ export async function disconnectState(state) {
2299
2333
  state.wgTunnel = null;
2300
2334
  }
2301
2335
 
2302
- // End session on chain (best-effort, fire-and-forget — never blocks disconnect)
2303
- if (prev?.sessionId && state._mnemonic) {
2304
- _endSessionOnChain(prev.sessionId, state._mnemonic, state._feeGranter).catch(e => {
2305
- console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
2306
- });
2336
+ if (endSession) {
2337
+ // Hard disconnect: broadcast MsgCancelSession — session settles and refunds deposit.
2338
+ if (prev?.sessionId && state._mnemonic) {
2339
+ _endSessionOnChain(prev.sessionId, state._mnemonic, state._feeGranter).catch(e => {
2340
+ console.warn(`[sentinel-sdk] Failed to end session ${prev.sessionId} on chain: ${e.message}`);
2341
+ });
2342
+ }
2343
+ } else {
2344
+ // Soft disconnect: leave session on chain in status=1 for reuse.
2345
+ if (prev?.sessionId) {
2346
+ console.log(`[sentinel-sdk] Session ${prev.sessionId} preserved on chain (status=1) for future reuse — no MsgCancelSession broadcast.`);
2347
+ }
2307
2348
  }
2308
2349
  } finally {
2309
2350
  // ALWAYS clear connection state — even if teardown threw
@@ -2317,8 +2358,55 @@ export async function disconnectState(state) {
2317
2358
  }
2318
2359
  }
2319
2360
 
2361
+ /**
2362
+ * Soft disconnect — tear down the local tunnel, leave the on-chain session active.
2363
+ *
2364
+ * A subsequent connectDirect() to the SAME node will reuse the session via
2365
+ * findExistingSession — no new MsgStartSession TX, no new payment, remaining
2366
+ * bandwidth is preserved.
2367
+ *
2368
+ * Use when: user is pausing, network changed, or closing the app temporarily.
2369
+ * To settle the session on-chain and reclaim the unused deposit, use
2370
+ * disconnectAndEndSession() instead.
2371
+ *
2372
+ * @param {object} state - ConnectionState instance
2373
+ */
2374
+ export async function disconnectState(state) {
2375
+ return _disconnectInternal(state, { endSession: false });
2376
+ }
2377
+
2378
+ /**
2379
+ * Soft disconnect (default state) — tear down the tunnel, leave on-chain session active.
2380
+ *
2381
+ * @see disconnectState
2382
+ */
2320
2383
  export async function disconnect() {
2321
- return disconnectState(_defaultState);
2384
+ return _disconnectInternal(_defaultState, { endSession: false });
2385
+ }
2386
+
2387
+ /**
2388
+ * Hard disconnect — tear down the tunnel AND broadcast MsgCancelSession on chain.
2389
+ *
2390
+ * The session settles after the ~2h inactive_pending window. The node refunds
2391
+ * the unused portion of the bandwidth deposit (for peer-to-peer sessions).
2392
+ * For plan-based sessions, this stops metering against the plan allocation.
2393
+ *
2394
+ * Use when: user is done with this node (switching nodes permanently, ending
2395
+ * their session, or wants the deposit back).
2396
+ *
2397
+ * @param {object} state - ConnectionState instance
2398
+ */
2399
+ export async function disconnectStateAndEndSession(state) {
2400
+ return _disconnectInternal(state, { endSession: true });
2401
+ }
2402
+
2403
+ /**
2404
+ * Hard disconnect (default state) — tear down the tunnel AND broadcast MsgCancelSession.
2405
+ *
2406
+ * @see disconnectStateAndEndSession
2407
+ */
2408
+ export async function disconnectAndEndSession() {
2409
+ return _disconnectInternal(_defaultState, { endSession: true });
2322
2410
  }
2323
2411
 
2324
2412
  // ─── Session End (on-chain cleanup) ──────────────────────────────────────────
package/operator.js CHANGED
@@ -77,6 +77,8 @@ export {
77
77
  grantPlanSubscribers,
78
78
  renewExpiringGrants,
79
79
  monitorFeeGrants,
80
+ streamGrantPlanSubscribers,
81
+ computeFeeGrantGasCosts,
80
82
  } from './cosmjs-setup.js';
81
83
 
82
84
  // ─── Authz ──────────────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "blue-js-sdk",
3
- "version": "2.4.0",
3
+ "version": "2.6.0",
4
4
  "description": "Decentralized VPN SDK for the Sentinel P2P bandwidth network. WireGuard + V2Ray tunnels, Cosmos blockchain, 900+ nodes. Tested on Windows. macOS/Linux support included but untested.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "types": "types/index.d.ts",
8
8
  "bin": {
9
- "sentinel": "./cli/index.js",
10
- "sentinel-ai": "./ai-path/cli.js"
9
+ "sentinel": "./cli/index.js"
11
10
  },
12
11
  "exports": {
13
12
  ".": {
@@ -16,7 +15,6 @@
16
15
  },
17
16
  "./consumer": "./consumer.js",
18
17
  "./operator": "./operator.js",
19
- "./ai-path": "./ai-path/index.js",
20
18
  "./blue": {
21
19
  "types": "./dist/index.d.ts",
22
20
  "default": "./dist/index.js"
@@ -49,7 +47,6 @@
49
47
  "errors/",
50
48
  "examples/",
51
49
  "docs/",
52
- "ai-path/",
53
50
  "bin/setup.js",
54
51
  "dist/",
55
52
  "src/",
package/types/index.d.ts CHANGED
@@ -469,8 +469,8 @@ export function isMnemonicValid(mnemonic: string): boolean;
469
469
  /** Get current P2P price in USD */
470
470
  export function getDvpnPrice(): Promise<number>;
471
471
 
472
- /** Find existing active session for wallet+node pair */
473
- export function findExistingSession(lcdUrl: string, walletAddr: string, nodeAddr: string): Promise<bigint | null>;
472
+ /** Find existing active session for wallet+node pair. Deduplicates stale sessions via onStaleDuplicate callback. */
473
+ export function findExistingSession(lcdUrl: string, walletAddr: string, nodeAddr: string, opts?: { onStaleDuplicate?: (sessionId: bigint) => void }): Promise<bigint | null>;
474
474
 
475
475
  /** Fetch active nodes from LCD with pagination */
476
476
  export function fetchActiveNodes(lcdUrl: string, limit?: number, maxPages?: number): Promise<unknown[]>;
@@ -1,116 +0,0 @@
1
- # Admin Elevation Guide — Sentinel AI Path
2
-
3
- ## Why Admin is Required
4
-
5
- WireGuard tunnel operations require system-level access:
6
- - **Windows:** Installing/removing WireGuard tunnel services (wireguard.exe /installtunnelservice)
7
- - **macOS:** Creating utun interfaces (wg-quick up)
8
- - **Linux:** Creating WireGuard interfaces (ip link add wg0)
9
-
10
- Without admin, you can only use V2Ray nodes (~70% of the network). With admin, you access 100% of nodes including the faster, more reliable WireGuard nodes.
11
-
12
- **The SDK checks admin BEFORE payment.** If you're not admin and select a WireGuard node, the SDK rejects the connection before any P2P tokens are spent. No money is wasted.
13
-
14
- ## Windows — Using run-admin.vbs
15
-
16
- The `run-admin.vbs` script triggers a single UAC prompt, then runs your Node.js script with full Administrator privileges. One prompt per session.
17
-
18
- ```bash
19
- # Setup (downloads V2Ray + installs WireGuard silently)
20
- cscript run-admin.vbs setup.js
21
-
22
- # Connect via CLI
23
- cscript run-admin.vbs cli.js connect
24
-
25
- # Run any custom script
26
- cscript run-admin.vbs my-agent.mjs
27
-
28
- # Test WireGuard specifically
29
- cscript run-admin.vbs test-wireguard.mjs
30
- ```
31
-
32
- ### How run-admin.vbs works
33
- 1. Calls `Shell.Application.ShellExecute` with verb `"runas"` → triggers UAC
34
- 2. Opens an elevated cmd.exe window
35
- 3. cd's to the script directory
36
- 4. Runs `node <your-script>`
37
- 5. Keeps the window open (so you can see output)
38
-
39
- ### For AI agents running unattended
40
- If the AI agent runs as a Windows Service or scheduled task, configure it to run as a user with admin rights (e.g., SYSTEM or a dedicated admin account). No UAC prompt needed for services.
41
-
42
- ## macOS — Using sudo
43
-
44
- ```bash
45
- sudo node setup.js # Install WireGuard via brew
46
- sudo node cli.js connect # Connect with WireGuard access
47
- sudo node my-agent.mjs # Run agent elevated
48
- ```
49
-
50
- For unattended agents, add to sudoers:
51
- ```
52
- agent-user ALL=(ALL) NOPASSWD: /usr/local/bin/node
53
- ```
54
-
55
- ## Linux — Using sudo
56
-
57
- ```bash
58
- sudo node setup.js # Install wireguard-tools via apt/dnf
59
- sudo node cli.js connect # Connect with WireGuard access
60
- sudo node my-agent.mjs # Run agent elevated
61
- ```
62
-
63
- For unattended agents in systemd:
64
- ```ini
65
- [Service]
66
- User=root
67
- ExecStart=/usr/local/bin/node /path/to/my-agent.mjs
68
- ```
69
-
70
- Or use capabilities instead of full root:
71
- ```bash
72
- sudo setcap cap_net_admin+ep $(which node)
73
- ```
74
-
75
- ## V2Ray-Only Mode (No Admin Needed)
76
-
77
- If admin is not available, the SDK automatically falls back to V2Ray nodes:
78
-
79
- ```js
80
- const vpn = await connect({
81
- mnemonic: process.env.MNEMONIC,
82
- protocol: 'v2ray', // Explicitly request V2Ray only
83
- });
84
- ```
85
-
86
- V2Ray runs as a userspace SOCKS5 proxy — no system-level access needed. It connects to ~630 nodes (70% of the network). This is the recommended mode for:
87
- - CI/CD pipelines
88
- - Docker containers without --privileged
89
- - Cloud VMs where root is restricted
90
- - Development/testing
91
-
92
- ## Detection in Code
93
-
94
- The SDK exports `IS_ADMIN` for checking:
95
-
96
- ```js
97
- import { IS_ADMIN, WG_AVAILABLE } from 'sentinel-dvpn-sdk';
98
-
99
- if (WG_AVAILABLE && IS_ADMIN) {
100
- console.log('Full network access (WireGuard + V2Ray)');
101
- } else if (WG_AVAILABLE && !IS_ADMIN) {
102
- console.log('WireGuard installed but not admin — V2Ray only');
103
- console.log('Run: cscript run-admin.vbs your-script.mjs');
104
- } else {
105
- console.log('V2Ray only (WireGuard not installed)');
106
- }
107
- ```
108
-
109
- The `getEnvironment()` function reports this:
110
- ```js
111
- import { getEnvironment } from 'sentinel-ai-connect';
112
- const env = getEnvironment();
113
- // env.admin: true/false
114
- // env.capabilities: ['v2ray', 'wireguard'] or ['v2ray', 'wireguard-needs-admin'] or ['v2ray']
115
- // env.recommended: ['run as admin to use WireGuard nodes (faster, more reliable)']
116
- ```