-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathconnect.js
More file actions
236 lines (195 loc) · 9.65 KB
/
Copy pathconnect.js
File metadata and controls
236 lines (195 loc) · 9.65 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
* Agent Connect — Zero-Config VPN Connection
*
* One function call: await connect({ mnemonic }) -> connected
*
* This module wraps the full Sentinel SDK into the simplest possible
* interface for AI agents. No config files, no setup — just connect.
*
* AGENT FLOW (7 steps, each logged):
* STEP 1/7 Environment — check OS, V2Ray, WireGuard, admin
* STEP 2/7 Wallet — derive address, connect to chain
* STEP 3/7 Balance — verify sufficient P2P before paying
* STEP 4/7 Node — select + validate target node
* STEP 5/7 Session — broadcast TX, create on-chain session
* STEP 6/7 Tunnel — handshake + install WireGuard/V2Ray
* STEP 7/7 Verify — confirm IP changed, traffic flows
*
* ARCHITECTURE: Split into focused modules (2026-04-07):
* connect.js — This file: orchestrator + re-exports (~200 lines)
* connect-helpers.js — Constants, state, logging, IP check, error mapping
* connect-session.js — Stage 4-6: node selection, SDK options, tunnel setup
* connect-verify.js — Stage 7: verify(), verifySplitTunnel(), isVpnActive()
* connect-status.js — status(), disconnect(), onEvent()
*/
import { AiPathError, AiPathErrorCodes, NextActions } from './errors.js';
import {
createWallet as sdkCreateWallet,
formatP2P,
} from 'blue-js-sdk';
// ─── Import from split modules ──────────────────────────────────────────────
import {
agentLog,
ensureCleanup,
ensureAxiosAdapter,
checkVpnIp,
humanError,
preValidateBalance,
MIN_BALANCE_UDVPN,
setLastConnectResult,
setConnectedAt,
setConnectTimings,
} from './connect-helpers.js';
import {
resolveNode,
buildSdkOptions,
executeConnection,
buildConnectResult,
} from './connect-session.js';
// ─── Re-export public API from split modules ────────────────────────────────
export { isVpnActive, verify, verifySplitTunnel } from './connect-verify.js';
export { disconnect, status, onEvent } from './connect-status.js';
// ─── connect() ───────────────────────────────────────────────────────────────
/**
* Connect to Sentinel dVPN. The ONE function an AI agent needs.
*
* Every step is logged with numbered phases (STEP 1/7 through STEP 7/7)
* so an autonomous agent can track progress and diagnose failures.
*
* @param {object} opts
* @param {string} opts.mnemonic - BIP39 mnemonic (12 or 24 words)
* @param {string} [opts.country] - Preferred country code (e.g. 'US', 'DE')
* @param {string} [opts.nodeAddress] - Specific node (sentnode1...). Skips auto-pick.
* @param {string} [opts.dns] - DNS preset: 'google', 'cloudflare', 'hns'
* @param {string} [opts.protocol] - Preferred protocol: 'wireguard' or 'v2ray'
* @param {function} [opts.onProgress] - Progress callback: (stage, message) => void
* @param {number} [opts.timeout] - Connection timeout in ms (default: 120000 — 2 minutes)
* @param {boolean} [opts.silent] - If true, suppress step-by-step console output
* @returns {Promise<{
* sessionId: string,
* protocol: string,
* nodeAddress: string,
* country: string|null,
* city: string|null,
* moniker: string|null,
* socksPort: number|null,
* socksAuth: object|null,
* dryRun: boolean,
* ip: string|null,
* walletAddress: string,
* balance: { before: string, after: string|null },
* cost: { estimated: string },
* timing: { totalMs: number, phases: object },
* }>}
*/
export async function connect(opts = {}) {
if (!opts || typeof opts !== 'object') {
throw new AiPathError(AiPathErrorCodes.INVALID_OPTIONS, 'connect() requires an options object with at least { mnemonic }', null, NextActions.NONE);
}
if (!opts.mnemonic || typeof opts.mnemonic !== 'string') {
throw new AiPathError(AiPathErrorCodes.MISSING_MNEMONIC, 'connect() requires a mnemonic string (12 or 24 word BIP39 phrase)', null, NextActions.CREATE_WALLET);
}
const silent = opts.silent === true;
const log = silent ? () => {} : agentLog;
const totalSteps = 7;
const timings = {};
const connectStart = Date.now();
// ── STEP 1/7: Environment ─────────────────────────────────────────────────
let t0 = Date.now();
log(1, totalSteps, 'ENVIRONMENT', 'Checking OS, tunnel binaries, admin privileges...');
await ensureAxiosAdapter();
ensureCleanup();
// Detect environment for agent visibility
let envInfo = { os: process.platform, admin: false, v2ray: false, wireguard: false };
try {
const { getEnvironment } = await import('./environment.js');
const env = getEnvironment();
envInfo = {
os: env.os,
admin: env.admin,
v2ray: env.v2ray?.available || false,
wireguard: env.wireguard?.available || false,
v2rayPath: env.v2ray?.path || null,
};
} catch { /* environment detection failed */ }
log(1, totalSteps, 'ENVIRONMENT', `OS=${envInfo.os} | admin=${envInfo.admin} | v2ray=${envInfo.v2ray} | wireguard=${envInfo.wireguard}`);
timings.environment = Date.now() - t0;
// ── STEP 2/7: Wallet ──────────────────────────────────────────────────────
t0 = Date.now();
log(2, totalSteps, 'WALLET', 'Deriving wallet address from mnemonic...');
let walletAddress = null;
try {
const { account } = await sdkCreateWallet(opts.mnemonic);
walletAddress = account.address;
log(2, totalSteps, 'WALLET', `Address: ${walletAddress}`);
} catch (err) {
log(2, totalSteps, 'WALLET', `Failed: ${err.message}`);
throw new AiPathError(AiPathErrorCodes.INVALID_MNEMONIC, 'Invalid mnemonic — wallet derivation failed', null, NextActions.CREATE_WALLET);
}
timings.wallet = Date.now() - t0;
// ── STEP 3/7: Balance Pre-Check ───────────────────────────────────────────
t0 = Date.now();
log(3, totalSteps, 'BALANCE', `Checking balance for ${walletAddress}...`);
const balCheck = await preValidateBalance(opts.mnemonic);
log(3, totalSteps, 'BALANCE', `Balance: ${balCheck.p2p} | Sufficient: ${balCheck.sufficient}`);
if (!balCheck.sufficient && !opts.dryRun) {
throw new AiPathError(
AiPathErrorCodes.INSUFFICIENT_BALANCE,
`Insufficient balance: ${balCheck.p2p}. Need at least ${formatP2P(MIN_BALANCE_UDVPN)}. Fund address: ${walletAddress}`,
{ address: walletAddress, balance: balCheck.p2p, minimum: formatP2P(MIN_BALANCE_UDVPN) },
NextActions.FUND_WALLET,
);
}
timings.balance = Date.now() - t0;
// ── STEP 4/7: Node Selection ──────────────────────────────────────────────
t0 = Date.now();
const { resolvedNodeAddress, discoveredNode } = await resolveNode(opts, envInfo, log, totalSteps);
timings.nodeSelection = Date.now() - t0;
// ── STEP 5/7 + 6/7: Session + Tunnel ──────────────────────────────────────
t0 = Date.now();
log(5, totalSteps, 'SESSION', 'Broadcasting session transaction...');
const { sdkOpts, timeoutId } = buildSdkOptions(opts, envInfo, silent, totalSteps);
try {
const result = await executeConnection(sdkOpts, resolvedNodeAddress, discoveredNode);
timings.sessionAndTunnel = Date.now() - t0;
// ── STEP 7/7: Verify ──────────────────────────────────────────────────
t0 = Date.now();
log(7, totalSteps, 'VERIFY', 'Checking VPN IP through tunnel...');
const ip = await checkVpnIp(result.socksPort || null);
log(7, totalSteps, 'VERIFY', ip ? `VPN IP: ${ip}` : 'IP check failed (tunnel may still work)');
timings.verify = Date.now() - t0;
timings.total = Date.now() - connectStart;
// ── Build result ──────────────────────────────────────────────────────
const output = await buildConnectResult(
result, resolvedNodeAddress, discoveredNode,
walletAddress, balCheck, opts.mnemonic, timings, ip,
);
setLastConnectResult(output);
setConnectedAt(Date.now());
setConnectTimings(timings);
// ── Final summary ──────────────────────────────────────────────────
log(7, totalSteps, 'COMPLETE', [
`Session=${output.sessionId}`,
`Protocol=${output.protocol}`,
`Node=${output.nodeAddress}`,
output.country ? `Country=${output.country}` : null,
`IP=${output.ip || 'unknown'}`,
`Time=${output.timing.totalFormatted}`,
`Balance=${output.balance.before} → ${output.balance.after || '?'}`,
].filter(Boolean).join(' | '));
return output;
} catch (err) {
timings.total = Date.now() - connectStart;
const { message, nextAction } = humanError(err);
const wrapped = new AiPathError(
err?.code || 'CONNECT_FAILED',
message,
{ ...(err?.details || {}), timing: { totalMs: timings.total, phases: { ...timings } } },
nextAction,
);
log(5, totalSteps, 'FAILED', `${wrapped.code}: ${message} → nextAction: ${nextAction}`);
throw wrapped;
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
}