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/state.js
ADDED
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sentinel dVPN SDK — Local State Persistence
|
|
3
|
+
*
|
|
4
|
+
* Tracks active sessions, V2Ray PIDs, and WireGuard tunnel names across process restarts.
|
|
5
|
+
* Also tracks session history to avoid reusing poisoned (failed handshake) sessions.
|
|
6
|
+
* State is saved to ~/.sentinel-sdk/state.json.
|
|
7
|
+
* Session history is saved to ~/.sentinel-sdk/sessions.json.
|
|
8
|
+
* PID file at ~/.sentinel-sdk/app.pid for server process management.
|
|
9
|
+
*
|
|
10
|
+
* When the process crashes mid-connection:
|
|
11
|
+
* - In-memory state (activeV2RayProc, activeWgTunnel) is lost
|
|
12
|
+
* - The tunnel/proxy may still be running (WG service, v2ray.exe, system proxy)
|
|
13
|
+
* - On next startup, loadState() + recoverOrphans() detects and cleans up
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* import { saveState, loadState, clearState, recoverOrphans } from './state.js';
|
|
17
|
+
* import { markSessionPoisoned, isSessionPoisoned, getSessionHistory } from './state.js';
|
|
18
|
+
* import { writePidFile, checkPidFile, clearPidFile } from './state.js';
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, renameSync } from 'fs';
|
|
22
|
+
import { execSync, execFileSync } from 'child_process';
|
|
23
|
+
import { createCipheriv, createDecipheriv, randomBytes, createHash } from 'crypto';
|
|
24
|
+
import path from 'path';
|
|
25
|
+
import os from 'os';
|
|
26
|
+
|
|
27
|
+
// ── State file validation (prevents command injection via poisoned state.json) ──
|
|
28
|
+
const STATE_SCHEMA = {
|
|
29
|
+
sessionId: v => v == null || /^\d+$/.test(String(v)),
|
|
30
|
+
serviceType: v => v == null || v === 'wireguard' || v === 'v2ray',
|
|
31
|
+
v2rayPid: v => v == null || (Number.isInteger(Number(v)) && Number(v) > 0),
|
|
32
|
+
socksPort: v => v == null || (Number.isInteger(Number(v)) && Number(v) >= 1 && Number(v) <= 65535),
|
|
33
|
+
wgTunnelName: v => v == null || /^[a-zA-Z0-9_-]{1,64}$/.test(v),
|
|
34
|
+
systemProxySet: v => v == null || typeof v === 'boolean',
|
|
35
|
+
killSwitchEnabled: v => v == null || typeof v === 'boolean',
|
|
36
|
+
nodeAddress: v => v == null || /^sentnode1[a-z0-9]{38}$/.test(v),
|
|
37
|
+
confPath: v => v == null || (typeof v === 'string' && v.length <= 260 && (/^[a-zA-Z]:[\\\/][a-zA-Z0-9_.\-\\\/ ]+$/.test(v) || /^\/[a-zA-Z0-9_.\-\/ ]+$/.test(v))),
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function validateStateValues(state) {
|
|
41
|
+
for (const [field, validate] of Object.entries(STATE_SCHEMA)) {
|
|
42
|
+
if (state[field] !== undefined && !validate(state[field])) {
|
|
43
|
+
console.warn(`[sentinel-sdk] Corrupted state: invalid ${field} "${state[field]}" — skipping recovery`);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const STATE_DIR = path.join(os.homedir(), '.sentinel-sdk');
|
|
51
|
+
const STATE_FILE = path.join(STATE_DIR, 'state.json');
|
|
52
|
+
const SESSIONS_FILE = path.join(STATE_DIR, 'sessions.json');
|
|
53
|
+
const PID_FILE = path.join(STATE_DIR, 'app.pid');
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Save current connection state to disk.
|
|
57
|
+
* Call this after a successful connection.
|
|
58
|
+
* @param {object} state
|
|
59
|
+
* @param {string} state.sessionId - Active session ID
|
|
60
|
+
* @param {string} state.serviceType - 'wireguard' | 'v2ray'
|
|
61
|
+
* @param {number} state.v2rayPid - V2Ray process PID (if v2ray)
|
|
62
|
+
* @param {number} state.socksPort - SOCKS5 port (if v2ray)
|
|
63
|
+
* @param {string} state.wgTunnelName - WireGuard tunnel service name (if wireguard)
|
|
64
|
+
* @param {boolean} state.systemProxySet - Whether Windows system proxy was set
|
|
65
|
+
* @param {string} state.nodeAddress - Connected node address
|
|
66
|
+
* @param {string} state.confPath - WireGuard config file path
|
|
67
|
+
*/
|
|
68
|
+
export function saveState(state) {
|
|
69
|
+
try {
|
|
70
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
71
|
+
// Strip unknown fields — only persist STATE_SCHEMA keys + metadata
|
|
72
|
+
const ALLOWED_KEYS = new Set([...Object.keys(STATE_SCHEMA), 'savedAt', 'pid']);
|
|
73
|
+
const cleaned = {};
|
|
74
|
+
for (const [k, v] of Object.entries(state)) {
|
|
75
|
+
if (ALLOWED_KEYS.has(k)) cleaned[k] = v;
|
|
76
|
+
}
|
|
77
|
+
const data = {
|
|
78
|
+
...cleaned,
|
|
79
|
+
savedAt: new Date().toISOString(),
|
|
80
|
+
pid: process.pid,
|
|
81
|
+
};
|
|
82
|
+
writeFileSync(STATE_FILE + '.tmp', JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
83
|
+
renameSync(STATE_FILE + '.tmp', STATE_FILE);
|
|
84
|
+
} catch (e) { console.warn('[sentinel-sdk] saveState warning:', e.message); }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Load saved state from disk.
|
|
89
|
+
* Returns null if no state file exists or it's corrupt.
|
|
90
|
+
*/
|
|
91
|
+
export function loadState() {
|
|
92
|
+
try {
|
|
93
|
+
if (!existsSync(STATE_FILE)) return null;
|
|
94
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Clear saved state (call after successful disconnect).
|
|
102
|
+
*/
|
|
103
|
+
export function clearState() {
|
|
104
|
+
try {
|
|
105
|
+
if (existsSync(STATE_FILE)) unlinkSync(STATE_FILE);
|
|
106
|
+
} catch (e) { console.warn('[sentinel-sdk] clearState warning:', e.message); }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Detect and clean up orphaned tunnels/processes from a previous crash.
|
|
111
|
+
* Call this at app startup after registerCleanupHandlers().
|
|
112
|
+
*
|
|
113
|
+
* Returns what was cleaned up (for logging).
|
|
114
|
+
*/
|
|
115
|
+
export function recoverOrphans() {
|
|
116
|
+
const state = loadState();
|
|
117
|
+
if (!state) return null;
|
|
118
|
+
|
|
119
|
+
// Validate state values before using them in shell commands (prevents command injection via poisoned state.json)
|
|
120
|
+
if (!validateStateValues(state)) {
|
|
121
|
+
clearState();
|
|
122
|
+
return { found: true, cleaned: ['Corrupted state file removed'] };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const recovered = { found: true, cleaned: [] };
|
|
126
|
+
|
|
127
|
+
// Check if the process that saved the state is still running
|
|
128
|
+
const savedPid = state.pid;
|
|
129
|
+
let processAlive = false;
|
|
130
|
+
if (savedPid) {
|
|
131
|
+
try {
|
|
132
|
+
process.kill(savedPid, 0); // signal 0 = check existence
|
|
133
|
+
processAlive = true;
|
|
134
|
+
} catch {
|
|
135
|
+
processAlive = false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// If the original process is still running, don't touch anything
|
|
140
|
+
if (processAlive) {
|
|
141
|
+
return { found: true, cleaned: [], note: `Original process ${savedPid} still running` };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Clean up orphaned V2Ray
|
|
145
|
+
if (state.serviceType === 'v2ray' && state.v2rayPid) {
|
|
146
|
+
try {
|
|
147
|
+
if (process.platform === 'win32') {
|
|
148
|
+
execFileSync('taskkill', ['/F', '/PID', String(state.v2rayPid)], { stdio: 'pipe', timeout: 5000 });
|
|
149
|
+
} else {
|
|
150
|
+
process.kill(state.v2rayPid, 'SIGKILL');
|
|
151
|
+
}
|
|
152
|
+
recovered.cleaned.push(`v2ray PID ${state.v2rayPid}`);
|
|
153
|
+
} catch {} // already dead — expected if process exited naturally
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Clean up orphaned WireGuard tunnel
|
|
157
|
+
if (state.serviceType === 'wireguard' && state.wgTunnelName) {
|
|
158
|
+
try {
|
|
159
|
+
if (process.platform === 'win32') {
|
|
160
|
+
// Check if WireGuard service exists
|
|
161
|
+
const out = execFileSync('sc', ['query', `WireGuardTunnel$${state.wgTunnelName}`], {
|
|
162
|
+
encoding: 'utf8', timeout: 5000, stdio: 'pipe',
|
|
163
|
+
});
|
|
164
|
+
if (out.includes('RUNNING') || out.includes('STOPPED')) {
|
|
165
|
+
// Find wireguard.exe
|
|
166
|
+
const wgExe = ['C:\\Program Files\\WireGuard\\wireguard.exe', 'C:\\Program Files (x86)\\WireGuard\\wireguard.exe']
|
|
167
|
+
.find(p => existsSync(p));
|
|
168
|
+
if (wgExe) {
|
|
169
|
+
execFileSync(wgExe, ['/uninstalltunnelservice', state.wgTunnelName], { timeout: 15000, stdio: 'pipe' });
|
|
170
|
+
recovered.cleaned.push(`WireGuard tunnel ${state.wgTunnelName}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
} catch (e) { console.warn('[sentinel-sdk] WG orphan cleanup warning:', e.message); }
|
|
175
|
+
|
|
176
|
+
// Linux/macOS: use wg-quick to remove stale tunnel
|
|
177
|
+
if (process.platform !== 'win32') {
|
|
178
|
+
try {
|
|
179
|
+
execFileSync('wg-quick', ['down', state.wgTunnelName], { timeout: 10000, stdio: 'pipe' });
|
|
180
|
+
recovered.cleaned.push(`WireGuard tunnel ${state.wgTunnelName} (wg-quick down)`);
|
|
181
|
+
} catch (e) { console.warn('[sentinel-sdk] wg-quick down warning:', e.message); }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clean up orphaned system proxy
|
|
186
|
+
if (state.systemProxySet) {
|
|
187
|
+
try {
|
|
188
|
+
if (process.platform === 'win32') {
|
|
189
|
+
const REG = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
|
|
190
|
+
execFileSync('reg', ['add', REG, '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', '0', '/f'], { stdio: 'pipe' });
|
|
191
|
+
execFileSync('reg', ['delete', REG, '/v', 'ProxyServer', '/f'], { stdio: 'pipe' });
|
|
192
|
+
recovered.cleaned.push('Windows system proxy');
|
|
193
|
+
} else if (process.platform === 'darwin') {
|
|
194
|
+
const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8', stdio: 'pipe' })
|
|
195
|
+
.split('\n').filter(s => s && !s.startsWith('*') && !s.startsWith('An asterisk'));
|
|
196
|
+
for (const svc of services) {
|
|
197
|
+
try { execFileSync('networksetup', ['-setsocksfirewallproxystate', svc, 'off'], { stdio: 'pipe' }); } catch {} // service may not have proxy enabled
|
|
198
|
+
}
|
|
199
|
+
recovered.cleaned.push('macOS system proxy');
|
|
200
|
+
} else {
|
|
201
|
+
execFileSync('gsettings', ['set', 'org.gnome.system.proxy', 'mode', 'none'], { stdio: 'pipe' });
|
|
202
|
+
recovered.cleaned.push('Linux system proxy (GNOME)');
|
|
203
|
+
}
|
|
204
|
+
} catch (e) { console.warn('[sentinel-sdk] proxy orphan cleanup warning:', e.message); }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Clean up orphaned kill switch (firewall rules persist across reboots — permanent internet loss)
|
|
208
|
+
if (state.killSwitchEnabled) {
|
|
209
|
+
try {
|
|
210
|
+
if (process.platform === 'win32') {
|
|
211
|
+
const rules = [
|
|
212
|
+
'SentinelVPN-Allow-Tunnel', 'SentinelVPN-Allow-WG-Endpoint',
|
|
213
|
+
'SentinelVPN-Allow-Loopback', 'SentinelVPN-Allow-DHCP',
|
|
214
|
+
'SentinelVPN-Allow-DNS-Tunnel', 'SentinelVPN-Block-IPv6',
|
|
215
|
+
];
|
|
216
|
+
for (const rule of rules) {
|
|
217
|
+
try { execFileSync('netsh', ['advfirewall', 'firewall', 'delete', 'rule', `name=${rule}`], { stdio: 'pipe' }); } catch {} // rule may not exist
|
|
218
|
+
}
|
|
219
|
+
execFileSync('netsh', ['advfirewall', 'set', 'allprofiles', 'firewallpolicy', 'blockinbound,allowoutbound'], { stdio: 'pipe' });
|
|
220
|
+
} else if (process.platform === 'darwin') {
|
|
221
|
+
try { execFileSync('pfctl', ['-d'], { stdio: 'pipe' }); } catch {} // pf may already be disabled
|
|
222
|
+
try { unlinkSync('/tmp/sentinel-killswitch.conf'); } catch {} // file may not exist
|
|
223
|
+
} else {
|
|
224
|
+
let hasRules = true;
|
|
225
|
+
while (hasRules) {
|
|
226
|
+
try { execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'ACCEPT'], { stdio: 'pipe' }); } catch { hasRules = false; }
|
|
227
|
+
}
|
|
228
|
+
try { execFileSync('iptables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch {} // rule may not exist
|
|
229
|
+
try { execFileSync('ip6tables', ['-D', 'OUTPUT', '-m', 'comment', '--comment', 'sentinel-vpn', '-j', 'DROP'], { stdio: 'pipe' }); } catch {} // rule may not exist
|
|
230
|
+
}
|
|
231
|
+
recovered.cleaned.push('Kill switch firewall rules');
|
|
232
|
+
} catch (e) { console.warn('[sentinel-sdk] kill switch orphan cleanup warning:', e.message); }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Clean up stale config file
|
|
236
|
+
if (state.confPath && existsSync(state.confPath)) {
|
|
237
|
+
try { unlinkSync(state.confPath); } catch (e) { console.warn('[sentinel-sdk] conf cleanup warning:', e.message); }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
clearState();
|
|
241
|
+
return recovered;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Session Tracking ────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Load session history from disk.
|
|
248
|
+
* Returns { sessions: { [sessionId]: { status, nodeAddress, error?, timestamp } } }
|
|
249
|
+
*/
|
|
250
|
+
function loadSessions() {
|
|
251
|
+
try {
|
|
252
|
+
if (!existsSync(SESSIONS_FILE)) return { sessions: {} };
|
|
253
|
+
return JSON.parse(readFileSync(SESSIONS_FILE, 'utf8'));
|
|
254
|
+
} catch {
|
|
255
|
+
return { sessions: {} };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function saveSessions(data) {
|
|
260
|
+
try {
|
|
261
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
262
|
+
const tmpFile = SESSIONS_FILE + '.tmp';
|
|
263
|
+
writeFileSync(tmpFile, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
264
|
+
renameSync(tmpFile, SESSIONS_FILE);
|
|
265
|
+
} catch {} // best-effort session tracking — non-fatal if write fails
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Mark a session as poisoned (handshake failed).
|
|
270
|
+
* findExistingSession callers should skip poisoned sessions.
|
|
271
|
+
* @param {string} sessionId
|
|
272
|
+
* @param {string} nodeAddress
|
|
273
|
+
* @param {string} error - Why it was poisoned
|
|
274
|
+
*/
|
|
275
|
+
export function markSessionPoisoned(sessionId, nodeAddress, error) {
|
|
276
|
+
const data = loadSessions();
|
|
277
|
+
data.sessions[String(sessionId)] = {
|
|
278
|
+
status: 'poisoned',
|
|
279
|
+
nodeAddress,
|
|
280
|
+
error: error?.substring(0, 200),
|
|
281
|
+
timestamp: new Date().toISOString(),
|
|
282
|
+
};
|
|
283
|
+
// Prune old entries (keep last 200)
|
|
284
|
+
const entries = Object.entries(data.sessions);
|
|
285
|
+
if (entries.length > 200) {
|
|
286
|
+
const sorted = entries.sort((a, b) => new Date(b[1].timestamp) - new Date(a[1].timestamp));
|
|
287
|
+
data.sessions = Object.fromEntries(sorted.slice(0, 200));
|
|
288
|
+
}
|
|
289
|
+
saveSessions(data);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Mark a session as successfully connected.
|
|
294
|
+
* @param {string} sessionId
|
|
295
|
+
* @param {string} nodeAddress
|
|
296
|
+
*/
|
|
297
|
+
export function markSessionActive(sessionId, nodeAddress) {
|
|
298
|
+
const data = loadSessions();
|
|
299
|
+
data.sessions[String(sessionId)] = {
|
|
300
|
+
status: 'active',
|
|
301
|
+
nodeAddress,
|
|
302
|
+
timestamp: new Date().toISOString(),
|
|
303
|
+
};
|
|
304
|
+
saveSessions(data);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Check if a session was poisoned (handshake failed previously).
|
|
309
|
+
* @param {string} sessionId
|
|
310
|
+
* @returns {boolean}
|
|
311
|
+
*/
|
|
312
|
+
export function isSessionPoisoned(sessionId) {
|
|
313
|
+
const data = loadSessions();
|
|
314
|
+
return data.sessions[String(sessionId)]?.status === 'poisoned';
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Get full session history for debugging.
|
|
319
|
+
* @returns {{ [sessionId]: { status, nodeAddress, error?, timestamp } }}
|
|
320
|
+
*/
|
|
321
|
+
export function getSessionHistory() {
|
|
322
|
+
return loadSessions().sessions;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Load poisoned session keys from disk.
|
|
327
|
+
* Returns an array of "nodeAddr:sessionId" strings.
|
|
328
|
+
* @returns {string[]}
|
|
329
|
+
*/
|
|
330
|
+
export function loadPoisonedKeys() {
|
|
331
|
+
const data = loadSessions();
|
|
332
|
+
return Array.isArray(data.poisoned) ? data.poisoned : [];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Save poisoned session keys to disk.
|
|
337
|
+
* @param {string[]} keys - Array of "nodeAddr:sessionId" strings
|
|
338
|
+
*/
|
|
339
|
+
export function savePoisonedKeys(keys) {
|
|
340
|
+
const data = loadSessions();
|
|
341
|
+
// Keep last 500 poisoned keys to prevent unbounded growth
|
|
342
|
+
data.poisoned = keys.length > 500 ? keys.slice(-500) : keys;
|
|
343
|
+
saveSessions(data);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── PID File ────────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Write a PID file for the current process.
|
|
350
|
+
* Use at server startup to enable clean restarts.
|
|
351
|
+
* @param {string} [name='app'] - App name (creates ~/.sentinel-sdk/{name}.pid)
|
|
352
|
+
* @returns {{ pidFile: string }}
|
|
353
|
+
*/
|
|
354
|
+
export function writePidFile(name = 'app') {
|
|
355
|
+
try {
|
|
356
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
357
|
+
const pidFile = path.join(STATE_DIR, `${name}.pid`);
|
|
358
|
+
writeFileSync(pidFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }), { encoding: 'utf8', mode: 0o600 });
|
|
359
|
+
return { pidFile };
|
|
360
|
+
} catch {
|
|
361
|
+
return { pidFile: null };
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Check if a previous instance is running from a PID file.
|
|
367
|
+
* Returns { running: boolean, pid?: number } so the caller can decide what to do.
|
|
368
|
+
* @param {string} [name='app'] - App name
|
|
369
|
+
*/
|
|
370
|
+
export function checkPidFile(name = 'app') {
|
|
371
|
+
try {
|
|
372
|
+
const pidFile = path.join(STATE_DIR, `${name}.pid`);
|
|
373
|
+
if (!existsSync(pidFile)) return { running: false };
|
|
374
|
+
const data = JSON.parse(readFileSync(pidFile, 'utf8'));
|
|
375
|
+
const pid = data.pid;
|
|
376
|
+
try {
|
|
377
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
378
|
+
return { running: true, pid, startedAt: data.startedAt };
|
|
379
|
+
} catch {
|
|
380
|
+
// Process is dead — stale PID file
|
|
381
|
+
unlinkSync(pidFile);
|
|
382
|
+
return { running: false, stalePid: pid };
|
|
383
|
+
}
|
|
384
|
+
} catch {
|
|
385
|
+
return { running: false };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Remove the PID file (call on clean shutdown).
|
|
391
|
+
* @param {string} [name='app'] - App name
|
|
392
|
+
*/
|
|
393
|
+
export function clearPidFile(name = 'app') {
|
|
394
|
+
try {
|
|
395
|
+
const pidFile = path.join(STATE_DIR, `${name}.pid`);
|
|
396
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
397
|
+
} catch {} // best-effort cleanup — non-fatal
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ─── Credential Cache ────────────────────────────────────────────────────────
|
|
401
|
+
// ─── Credential Encryption ──────────────────────────────────────────────────
|
|
402
|
+
// Credentials contain WireGuard private keys and V2Ray UUIDs.
|
|
403
|
+
// Encrypted at rest with AES-256-GCM using a machine-local key derived from
|
|
404
|
+
// hostname + username + SDK install path. Not a replacement for OS keyring,
|
|
405
|
+
// but prevents trivial plaintext exposure if the file is copied off-machine.
|
|
406
|
+
|
|
407
|
+
const CRED_FILE = path.join(STATE_DIR, 'credentials.enc.json');
|
|
408
|
+
const CRED_FILE_LEGACY = path.join(STATE_DIR, 'credentials.json');
|
|
409
|
+
|
|
410
|
+
function _credKey() {
|
|
411
|
+
const material = `sentinel-sdk:${os.hostname()}:${os.userInfo().username}:${STATE_DIR}`;
|
|
412
|
+
return createHash('sha256').update(material).digest();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function _encrypt(plaintext) {
|
|
416
|
+
const key = _credKey();
|
|
417
|
+
const iv = randomBytes(12);
|
|
418
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
419
|
+
const enc = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
|
|
420
|
+
const tag = cipher.getAuthTag();
|
|
421
|
+
return JSON.stringify({
|
|
422
|
+
v: 1,
|
|
423
|
+
iv: iv.toString('base64'),
|
|
424
|
+
tag: tag.toString('base64'),
|
|
425
|
+
data: enc.toString('base64'),
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function _decrypt(envelope) {
|
|
430
|
+
const { v, iv, tag, data } = JSON.parse(envelope);
|
|
431
|
+
if (v !== 1) throw new Error('Unknown credential encryption version');
|
|
432
|
+
const key = _credKey();
|
|
433
|
+
const decipher = createDecipheriv('aes-256-gcm', key, Buffer.from(iv, 'base64'));
|
|
434
|
+
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
|
435
|
+
return Buffer.concat([decipher.update(Buffer.from(data, 'base64')), decipher.final()]).toString('utf8');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Save handshake credentials for a node+session pair.
|
|
440
|
+
* @param {string} nodeAddress - sentnode1...
|
|
441
|
+
* @param {string} sessionId - chain session ID
|
|
442
|
+
* @param {object} credentials - { serviceType, wgPrivateKey?, wgServerPubKey?, wgAssignedAddrs?, wgServerEndpoint?, v2rayUuid?, v2rayConfig?, confPath? }
|
|
443
|
+
*/
|
|
444
|
+
export function saveCredentials(nodeAddress, sessionId, credentials) {
|
|
445
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
446
|
+
const store = loadCredentialStore();
|
|
447
|
+
store[nodeAddress] = {
|
|
448
|
+
sessionId: String(sessionId),
|
|
449
|
+
...credentials,
|
|
450
|
+
savedAt: new Date().toISOString(),
|
|
451
|
+
};
|
|
452
|
+
// Prune old entries (keep max 100)
|
|
453
|
+
const entries = Object.entries(store);
|
|
454
|
+
if (entries.length > 100) {
|
|
455
|
+
entries.sort((a, b) => new Date(b[1].savedAt) - new Date(a[1].savedAt));
|
|
456
|
+
const pruned = Object.fromEntries(entries.slice(0, 100));
|
|
457
|
+
_writeCredentialStore(pruned);
|
|
458
|
+
} else {
|
|
459
|
+
_writeCredentialStore(store);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function _writeCredentialStore(store) {
|
|
464
|
+
const plaintext = JSON.stringify(store, null, 2);
|
|
465
|
+
const encrypted = _encrypt(plaintext);
|
|
466
|
+
const tmpPath = CRED_FILE + '.tmp';
|
|
467
|
+
writeFileSync(tmpPath, encrypted, { mode: 0o600 });
|
|
468
|
+
renameSync(tmpPath, CRED_FILE);
|
|
469
|
+
// Remove legacy plaintext file if it exists
|
|
470
|
+
try { if (existsSync(CRED_FILE_LEGACY)) unlinkSync(CRED_FILE_LEGACY); } catch { /* best effort */ }
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Load saved credentials for a node.
|
|
475
|
+
* @param {string} nodeAddress
|
|
476
|
+
* @returns {{ sessionId: string, serviceType: string, wgPrivateKey?: string, wgServerPubKey?: string, wgAssignedAddrs?: string[], wgServerEndpoint?: string, v2rayUuid?: string, v2rayConfig?: string, savedAt: string } | null}
|
|
477
|
+
*/
|
|
478
|
+
export function loadCredentials(nodeAddress) {
|
|
479
|
+
const store = loadCredentialStore();
|
|
480
|
+
return store[nodeAddress] || null;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/** Clear saved credentials for a node. */
|
|
484
|
+
export function clearCredentials(nodeAddress) {
|
|
485
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
486
|
+
const store = loadCredentialStore();
|
|
487
|
+
delete store[nodeAddress];
|
|
488
|
+
_writeCredentialStore(store);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Clear all saved credentials. */
|
|
492
|
+
export function clearAllCredentials() {
|
|
493
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
494
|
+
_writeCredentialStore({});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function loadCredentialStore() {
|
|
498
|
+
try {
|
|
499
|
+
// Try encrypted file first
|
|
500
|
+
if (existsSync(CRED_FILE)) {
|
|
501
|
+
const raw = readFileSync(CRED_FILE, 'utf-8');
|
|
502
|
+
const decrypted = _decrypt(raw);
|
|
503
|
+
return JSON.parse(decrypted);
|
|
504
|
+
}
|
|
505
|
+
// Migrate legacy plaintext credentials
|
|
506
|
+
if (existsSync(CRED_FILE_LEGACY)) {
|
|
507
|
+
const store = JSON.parse(readFileSync(CRED_FILE_LEGACY, 'utf-8'));
|
|
508
|
+
_writeCredentialStore(store); // Re-save encrypted + delete legacy
|
|
509
|
+
return store;
|
|
510
|
+
}
|
|
511
|
+
return {};
|
|
512
|
+
} catch {
|
|
513
|
+
// Decryption failed (different machine, corrupted) — start fresh
|
|
514
|
+
return {};
|
|
515
|
+
}
|
|
516
|
+
}
|