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/index.js
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
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 path from 'path';
|
|
24
|
+
import os from 'os';
|
|
25
|
+
|
|
26
|
+
// ── State file validation (prevents command injection via poisoned state.json) ──
|
|
27
|
+
const STATE_SCHEMA = {
|
|
28
|
+
sessionId: v => v == null || /^\d+$/.test(String(v)),
|
|
29
|
+
serviceType: v => v == null || v === 'wireguard' || v === 'v2ray',
|
|
30
|
+
v2rayPid: v => v == null || (Number.isInteger(Number(v)) && Number(v) > 0),
|
|
31
|
+
socksPort: v => v == null || (Number.isInteger(Number(v)) && Number(v) >= 1 && Number(v) <= 65535),
|
|
32
|
+
wgTunnelName: v => v == null || /^[a-zA-Z0-9_-]{1,64}$/.test(v),
|
|
33
|
+
systemProxySet: v => v == null || typeof v === 'boolean',
|
|
34
|
+
nodeAddress: v => v == null || /^sentnode1[a-z0-9]{38}$/.test(v),
|
|
35
|
+
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))),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function validateStateValues(state) {
|
|
39
|
+
for (const [field, validate] of Object.entries(STATE_SCHEMA)) {
|
|
40
|
+
if (state[field] !== undefined && !validate(state[field])) {
|
|
41
|
+
console.warn(`[sentinel-sdk] Corrupted state: invalid ${field} "${state[field]}" — skipping recovery`);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const STATE_DIR = path.join(os.homedir(), '.sentinel-sdk');
|
|
49
|
+
const STATE_FILE = path.join(STATE_DIR, 'state.json');
|
|
50
|
+
const SESSIONS_FILE = path.join(STATE_DIR, 'sessions.json');
|
|
51
|
+
const PID_FILE = path.join(STATE_DIR, 'app.pid');
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save current connection state to disk.
|
|
55
|
+
* Call this after a successful connection.
|
|
56
|
+
* @param {object} state
|
|
57
|
+
* @param {string} state.sessionId - Active session ID
|
|
58
|
+
* @param {string} state.serviceType - 'wireguard' | 'v2ray'
|
|
59
|
+
* @param {number} state.v2rayPid - V2Ray process PID (if v2ray)
|
|
60
|
+
* @param {number} state.socksPort - SOCKS5 port (if v2ray)
|
|
61
|
+
* @param {string} state.wgTunnelName - WireGuard tunnel service name (if wireguard)
|
|
62
|
+
* @param {boolean} state.systemProxySet - Whether Windows system proxy was set
|
|
63
|
+
* @param {string} state.nodeAddress - Connected node address
|
|
64
|
+
* @param {string} state.confPath - WireGuard config file path
|
|
65
|
+
*/
|
|
66
|
+
export function saveState(state) {
|
|
67
|
+
try {
|
|
68
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
69
|
+
// Strip unknown fields — only persist STATE_SCHEMA keys + metadata
|
|
70
|
+
const ALLOWED_KEYS = new Set([...Object.keys(STATE_SCHEMA), 'savedAt', 'pid']);
|
|
71
|
+
const cleaned = {};
|
|
72
|
+
for (const [k, v] of Object.entries(state)) {
|
|
73
|
+
if (ALLOWED_KEYS.has(k)) cleaned[k] = v;
|
|
74
|
+
}
|
|
75
|
+
const data = {
|
|
76
|
+
...cleaned,
|
|
77
|
+
savedAt: new Date().toISOString(),
|
|
78
|
+
pid: process.pid,
|
|
79
|
+
};
|
|
80
|
+
writeFileSync(STATE_FILE + '.tmp', JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
81
|
+
renameSync(STATE_FILE + '.tmp', STATE_FILE);
|
|
82
|
+
} catch (e) { console.warn('[sentinel-sdk] saveState warning:', e.message); }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Load saved state from disk.
|
|
87
|
+
* Returns null if no state file exists or it's corrupt.
|
|
88
|
+
*/
|
|
89
|
+
export function loadState() {
|
|
90
|
+
try {
|
|
91
|
+
if (!existsSync(STATE_FILE)) return null;
|
|
92
|
+
return JSON.parse(readFileSync(STATE_FILE, 'utf8'));
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Clear saved state (call after successful disconnect).
|
|
100
|
+
*/
|
|
101
|
+
export function clearState() {
|
|
102
|
+
try {
|
|
103
|
+
if (existsSync(STATE_FILE)) unlinkSync(STATE_FILE);
|
|
104
|
+
} catch (e) { console.warn('[sentinel-sdk] clearState warning:', e.message); }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Detect and clean up orphaned tunnels/processes from a previous crash.
|
|
109
|
+
* Call this at app startup after registerCleanupHandlers().
|
|
110
|
+
*
|
|
111
|
+
* Returns what was cleaned up (for logging).
|
|
112
|
+
*/
|
|
113
|
+
export function recoverOrphans() {
|
|
114
|
+
const state = loadState();
|
|
115
|
+
if (!state) return null;
|
|
116
|
+
|
|
117
|
+
// Validate state values before using them in shell commands (prevents command injection via poisoned state.json)
|
|
118
|
+
if (!validateStateValues(state)) {
|
|
119
|
+
clearState();
|
|
120
|
+
return { found: true, cleaned: ['Corrupted state file removed'] };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const recovered = { found: true, cleaned: [] };
|
|
124
|
+
|
|
125
|
+
// Check if the process that saved the state is still running
|
|
126
|
+
const savedPid = state.pid;
|
|
127
|
+
let processAlive = false;
|
|
128
|
+
if (savedPid) {
|
|
129
|
+
try {
|
|
130
|
+
process.kill(savedPid, 0); // signal 0 = check existence
|
|
131
|
+
processAlive = true;
|
|
132
|
+
} catch {
|
|
133
|
+
processAlive = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// If the original process is still running, don't touch anything
|
|
138
|
+
if (processAlive) {
|
|
139
|
+
return { found: true, cleaned: [], note: `Original process ${savedPid} still running` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Clean up orphaned V2Ray
|
|
143
|
+
if (state.serviceType === 'v2ray' && state.v2rayPid) {
|
|
144
|
+
try {
|
|
145
|
+
if (process.platform === 'win32') {
|
|
146
|
+
execFileSync('taskkill', ['/F', '/PID', String(state.v2rayPid)], { stdio: 'pipe', timeout: 5000 });
|
|
147
|
+
} else {
|
|
148
|
+
process.kill(state.v2rayPid, 'SIGKILL');
|
|
149
|
+
}
|
|
150
|
+
recovered.cleaned.push(`v2ray PID ${state.v2rayPid}`);
|
|
151
|
+
} catch {} // already dead — expected if process exited naturally
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Clean up orphaned WireGuard tunnel
|
|
155
|
+
if (state.serviceType === 'wireguard' && state.wgTunnelName) {
|
|
156
|
+
try {
|
|
157
|
+
if (process.platform === 'win32') {
|
|
158
|
+
// Check if WireGuard service exists
|
|
159
|
+
const out = execFileSync('sc', ['query', `WireGuardTunnel$${state.wgTunnelName}`], {
|
|
160
|
+
encoding: 'utf8', timeout: 5000, stdio: 'pipe',
|
|
161
|
+
});
|
|
162
|
+
if (out.includes('RUNNING') || out.includes('STOPPED')) {
|
|
163
|
+
// Find wireguard.exe
|
|
164
|
+
const wgExe = ['C:\\Program Files\\WireGuard\\wireguard.exe', 'C:\\Program Files (x86)\\WireGuard\\wireguard.exe']
|
|
165
|
+
.find(p => existsSync(p));
|
|
166
|
+
if (wgExe) {
|
|
167
|
+
execFileSync(wgExe, ['/uninstalltunnelservice', state.wgTunnelName], { timeout: 15000, stdio: 'pipe' });
|
|
168
|
+
recovered.cleaned.push(`WireGuard tunnel ${state.wgTunnelName}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
} catch (e) { console.warn('[sentinel-sdk] WG orphan cleanup warning:', e.message); }
|
|
173
|
+
|
|
174
|
+
// Linux/macOS: use wg-quick to remove stale tunnel
|
|
175
|
+
if (process.platform !== 'win32') {
|
|
176
|
+
try {
|
|
177
|
+
execFileSync('wg-quick', ['down', state.wgTunnelName], { timeout: 10000, stdio: 'pipe' });
|
|
178
|
+
recovered.cleaned.push(`WireGuard tunnel ${state.wgTunnelName} (wg-quick down)`);
|
|
179
|
+
} catch (e) { console.warn('[sentinel-sdk] wg-quick down warning:', e.message); }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Clean up orphaned system proxy
|
|
184
|
+
if (state.systemProxySet) {
|
|
185
|
+
try {
|
|
186
|
+
if (process.platform === 'win32') {
|
|
187
|
+
const REG = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings';
|
|
188
|
+
execFileSync('reg', ['add', REG, '/v', 'ProxyEnable', '/t', 'REG_DWORD', '/d', '0', '/f'], { stdio: 'pipe' });
|
|
189
|
+
execFileSync('reg', ['delete', REG, '/v', 'ProxyServer', '/f'], { stdio: 'pipe' });
|
|
190
|
+
recovered.cleaned.push('Windows system proxy');
|
|
191
|
+
} else if (process.platform === 'darwin') {
|
|
192
|
+
const services = execFileSync('networksetup', ['-listallnetworkservices'], { encoding: 'utf8', stdio: 'pipe' })
|
|
193
|
+
.split('\n').filter(s => s && !s.startsWith('*') && !s.startsWith('An asterisk'));
|
|
194
|
+
for (const svc of services) {
|
|
195
|
+
try { execFileSync('networksetup', ['-setsocksfirewallproxystate', svc, 'off'], { stdio: 'pipe' }); } catch {} // service may not have proxy enabled
|
|
196
|
+
}
|
|
197
|
+
recovered.cleaned.push('macOS system proxy');
|
|
198
|
+
} else {
|
|
199
|
+
execFileSync('gsettings', ['set', 'org.gnome.system.proxy', 'mode', 'none'], { stdio: 'pipe' });
|
|
200
|
+
recovered.cleaned.push('Linux system proxy (GNOME)');
|
|
201
|
+
}
|
|
202
|
+
} catch (e) { console.warn('[sentinel-sdk] proxy orphan cleanup warning:', e.message); }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Clean up stale config file
|
|
206
|
+
if (state.confPath && existsSync(state.confPath)) {
|
|
207
|
+
try { unlinkSync(state.confPath); } catch (e) { console.warn('[sentinel-sdk] conf cleanup warning:', e.message); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
clearState();
|
|
211
|
+
return recovered;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ─── Session Tracking ────────────────────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Load session history from disk.
|
|
218
|
+
* Returns { sessions: { [sessionId]: { status, nodeAddress, error?, timestamp } } }
|
|
219
|
+
*/
|
|
220
|
+
function loadSessions() {
|
|
221
|
+
try {
|
|
222
|
+
if (!existsSync(SESSIONS_FILE)) return { sessions: {} };
|
|
223
|
+
return JSON.parse(readFileSync(SESSIONS_FILE, 'utf8'));
|
|
224
|
+
} catch {
|
|
225
|
+
return { sessions: {} };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function saveSessions(data) {
|
|
230
|
+
try {
|
|
231
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
232
|
+
const tmpFile = SESSIONS_FILE + '.tmp';
|
|
233
|
+
writeFileSync(tmpFile, JSON.stringify(data, null, 2), { encoding: 'utf8', mode: 0o600 });
|
|
234
|
+
renameSync(tmpFile, SESSIONS_FILE);
|
|
235
|
+
} catch {} // best-effort session tracking — non-fatal if write fails
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Mark a session as poisoned (handshake failed).
|
|
240
|
+
* findExistingSession callers should skip poisoned sessions.
|
|
241
|
+
* @param {string} sessionId
|
|
242
|
+
* @param {string} nodeAddress
|
|
243
|
+
* @param {string} error - Why it was poisoned
|
|
244
|
+
*/
|
|
245
|
+
export function markSessionPoisoned(sessionId, nodeAddress, error) {
|
|
246
|
+
const data = loadSessions();
|
|
247
|
+
data.sessions[String(sessionId)] = {
|
|
248
|
+
status: 'poisoned',
|
|
249
|
+
nodeAddress,
|
|
250
|
+
error: error?.substring(0, 200),
|
|
251
|
+
timestamp: new Date().toISOString(),
|
|
252
|
+
};
|
|
253
|
+
// Prune old entries (keep last 200)
|
|
254
|
+
const entries = Object.entries(data.sessions);
|
|
255
|
+
if (entries.length > 200) {
|
|
256
|
+
const sorted = entries.sort((a, b) => new Date(b[1].timestamp) - new Date(a[1].timestamp));
|
|
257
|
+
data.sessions = Object.fromEntries(sorted.slice(0, 200));
|
|
258
|
+
}
|
|
259
|
+
saveSessions(data);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Mark a session as successfully connected.
|
|
264
|
+
* @param {string} sessionId
|
|
265
|
+
* @param {string} nodeAddress
|
|
266
|
+
*/
|
|
267
|
+
export function markSessionActive(sessionId, nodeAddress) {
|
|
268
|
+
const data = loadSessions();
|
|
269
|
+
data.sessions[String(sessionId)] = {
|
|
270
|
+
status: 'active',
|
|
271
|
+
nodeAddress,
|
|
272
|
+
timestamp: new Date().toISOString(),
|
|
273
|
+
};
|
|
274
|
+
saveSessions(data);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Check if a session was poisoned (handshake failed previously).
|
|
279
|
+
* @param {string} sessionId
|
|
280
|
+
* @returns {boolean}
|
|
281
|
+
*/
|
|
282
|
+
export function isSessionPoisoned(sessionId) {
|
|
283
|
+
const data = loadSessions();
|
|
284
|
+
return data.sessions[String(sessionId)]?.status === 'poisoned';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get full session history for debugging.
|
|
289
|
+
* @returns {{ [sessionId]: { status, nodeAddress, error?, timestamp } }}
|
|
290
|
+
*/
|
|
291
|
+
export function getSessionHistory() {
|
|
292
|
+
return loadSessions().sessions;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ─── PID File ────────────────────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Write a PID file for the current process.
|
|
299
|
+
* Use at server startup to enable clean restarts.
|
|
300
|
+
* @param {string} [name='app'] - App name (creates ~/.sentinel-sdk/{name}.pid)
|
|
301
|
+
* @returns {{ pidFile: string }}
|
|
302
|
+
*/
|
|
303
|
+
export function writePidFile(name = 'app') {
|
|
304
|
+
try {
|
|
305
|
+
mkdirSync(STATE_DIR, { recursive: true, mode: 0o700 });
|
|
306
|
+
const pidFile = path.join(STATE_DIR, `${name}.pid`);
|
|
307
|
+
writeFileSync(pidFile, JSON.stringify({ pid: process.pid, startedAt: new Date().toISOString() }), { encoding: 'utf8', mode: 0o600 });
|
|
308
|
+
return { pidFile };
|
|
309
|
+
} catch {
|
|
310
|
+
return { pidFile: null };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Check if a previous instance is running from a PID file.
|
|
316
|
+
* Returns { running: boolean, pid?: number } so the caller can decide what to do.
|
|
317
|
+
* @param {string} [name='app'] - App name
|
|
318
|
+
*/
|
|
319
|
+
export function checkPidFile(name = 'app') {
|
|
320
|
+
try {
|
|
321
|
+
const pidFile = path.join(STATE_DIR, `${name}.pid`);
|
|
322
|
+
if (!existsSync(pidFile)) return { running: false };
|
|
323
|
+
const data = JSON.parse(readFileSync(pidFile, 'utf8'));
|
|
324
|
+
const pid = data.pid;
|
|
325
|
+
try {
|
|
326
|
+
process.kill(pid, 0); // signal 0 = check existence
|
|
327
|
+
return { running: true, pid, startedAt: data.startedAt };
|
|
328
|
+
} catch {
|
|
329
|
+
// Process is dead — stale PID file
|
|
330
|
+
unlinkSync(pidFile);
|
|
331
|
+
return { running: false, stalePid: pid };
|
|
332
|
+
}
|
|
333
|
+
} catch {
|
|
334
|
+
return { running: false };
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Remove the PID file (call on clean shutdown).
|
|
340
|
+
* @param {string} [name='app'] - App name
|
|
341
|
+
*/
|
|
342
|
+
export function clearPidFile(name = 'app') {
|
|
343
|
+
try {
|
|
344
|
+
const pidFile = path.join(STATE_DIR, `${name}.pid`);
|
|
345
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
346
|
+
} catch {} // best-effort cleanup — non-fatal
|
|
347
|
+
}
|