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.
Files changed (215) hide show
  1. package/CHANGELOG.md +446 -0
  2. package/LICENSE +21 -0
  3. package/README.md +75 -0
  4. package/ai-path/ADMIN-ELEVATION.md +116 -0
  5. package/ai-path/AI-MANIFESTO.md +185 -0
  6. package/ai-path/BREAKING.md +74 -0
  7. package/ai-path/CHECKLIST.md +619 -0
  8. package/ai-path/CONNECTION-STEPS.md +724 -0
  9. package/ai-path/DECISION-TREE.md +378 -0
  10. package/ai-path/DEPENDENCIES.md +459 -0
  11. package/ai-path/E2E-FLOW.md +1555 -0
  12. package/ai-path/FAILURES.md +403 -0
  13. package/ai-path/GUIDE.md +1217 -0
  14. package/ai-path/README.md +558 -0
  15. package/ai-path/SPLIT-TUNNEL.md +266 -0
  16. package/ai-path/cli.js +535 -0
  17. package/ai-path/connect.js +884 -0
  18. package/ai-path/discover.js +178 -0
  19. package/ai-path/environment.js +266 -0
  20. package/ai-path/errors.js +86 -0
  21. package/ai-path/examples/autonomous-agent.mjs +220 -0
  22. package/ai-path/examples/multi-region.mjs +174 -0
  23. package/ai-path/examples/one-shot.mjs +31 -0
  24. package/ai-path/index.js +60 -0
  25. package/ai-path/pricing.js +136 -0
  26. package/ai-path/recommend.js +413 -0
  27. package/ai-path/run-admin.vbs +25 -0
  28. package/ai-path/setup.js +291 -0
  29. package/ai-path/wallet.js +137 -0
  30. package/app-helpers.js +363 -0
  31. package/app-settings.js +95 -0
  32. package/app-types.js +267 -0
  33. package/audit.js +847 -0
  34. package/batch.js +293 -0
  35. package/bin/setup.js +376 -0
  36. package/chain/authz.js +109 -0
  37. package/chain/broadcast.js +472 -0
  38. package/chain/client.js +160 -0
  39. package/chain/fee-grants.js +305 -0
  40. package/chain/index.js +891 -0
  41. package/chain/lcd.js +313 -0
  42. package/chain/queries.js +547 -0
  43. package/chain/rpc.js +408 -0
  44. package/chain/wallet.js +141 -0
  45. package/cli/config.js +143 -0
  46. package/cli/index.js +463 -0
  47. package/cli/output.js +182 -0
  48. package/cli.js +491 -0
  49. package/client/index.js +251 -0
  50. package/client.js +271 -0
  51. package/config/index.js +255 -0
  52. package/connection/connect.js +849 -0
  53. package/connection/disconnect.js +180 -0
  54. package/connection/discovery.js +321 -0
  55. package/connection/index.js +76 -0
  56. package/connection/proxy.js +148 -0
  57. package/connection/resilience.js +428 -0
  58. package/connection/security.js +232 -0
  59. package/connection/state.js +369 -0
  60. package/connection/tunnel.js +691 -0
  61. package/consumer.js +132 -0
  62. package/cosmjs-setup.js +1884 -0
  63. package/defaults.js +366 -0
  64. package/disk-cache.js +107 -0
  65. package/dist/client.d.ts +108 -0
  66. package/dist/client.d.ts.map +1 -0
  67. package/dist/client.js +400 -0
  68. package/dist/client.js.map +1 -0
  69. package/dist/index.d.ts +8 -0
  70. package/dist/index.d.ts.map +1 -0
  71. package/dist/index.js +8 -0
  72. package/dist/index.js.map +1 -0
  73. package/errors/index.js +112 -0
  74. package/errors.js +218 -0
  75. package/examples/README.md +64 -0
  76. package/examples/connect-direct.mjs +106 -0
  77. package/examples/connect-plan.mjs +125 -0
  78. package/examples/error-handling.mjs +109 -0
  79. package/examples/query-nodes.mjs +94 -0
  80. package/examples/wallet-basics.mjs +61 -0
  81. package/generated/amino/amino.ts +9 -0
  82. package/generated/cosmos/base/v1beta1/coin.ts +365 -0
  83. package/generated/cosmos_proto/cosmos.ts +323 -0
  84. package/generated/gogoproto/gogo.ts +9 -0
  85. package/generated/google/protobuf/descriptor.ts +7601 -0
  86. package/generated/google/protobuf/duration.ts +208 -0
  87. package/generated/google/protobuf/timestamp.ts +238 -0
  88. package/generated/sentinel/lease/v1/events.ts +924 -0
  89. package/generated/sentinel/lease/v1/lease.ts +292 -0
  90. package/generated/sentinel/lease/v1/msg.ts +949 -0
  91. package/generated/sentinel/lease/v1/params.ts +164 -0
  92. package/generated/sentinel/node/v3/events.ts +881 -0
  93. package/generated/sentinel/node/v3/msg.ts +1002 -0
  94. package/generated/sentinel/node/v3/node.ts +263 -0
  95. package/generated/sentinel/node/v3/params.ts +183 -0
  96. package/generated/sentinel/plan/v3/events.ts +675 -0
  97. package/generated/sentinel/plan/v3/msg.ts +1191 -0
  98. package/generated/sentinel/plan/v3/plan.ts +283 -0
  99. package/generated/sentinel/provider/v2/events.ts +171 -0
  100. package/generated/sentinel/provider/v2/msg.ts +480 -0
  101. package/generated/sentinel/provider/v2/params.ts +131 -0
  102. package/generated/sentinel/provider/v2/provider.ts +246 -0
  103. package/generated/sentinel/session/v3/events.ts +480 -0
  104. package/generated/sentinel/session/v3/msg.ts +616 -0
  105. package/generated/sentinel/session/v3/params.ts +260 -0
  106. package/generated/sentinel/session/v3/proof.ts +180 -0
  107. package/generated/sentinel/session/v3/session.ts +384 -0
  108. package/generated/sentinel/subscription/v3/events.ts +1181 -0
  109. package/generated/sentinel/subscription/v3/msg.ts +1305 -0
  110. package/generated/sentinel/subscription/v3/params.ts +167 -0
  111. package/generated/sentinel/subscription/v3/subscription.ts +315 -0
  112. package/generated/sentinel/types/v1/bandwidth.ts +124 -0
  113. package/generated/sentinel/types/v1/price.ts +149 -0
  114. package/generated/sentinel/types/v1/renewal.ts +87 -0
  115. package/generated/sentinel/types/v1/status.ts +54 -0
  116. package/generated/typeRegistry.ts +27 -0
  117. package/index.js +486 -0
  118. package/node-connect.js +3015 -0
  119. package/operator.js +134 -0
  120. package/package.json +113 -0
  121. package/plan-operations.js +199 -0
  122. package/preflight.js +352 -0
  123. package/pricing/index.js +262 -0
  124. package/proto/amino/amino.proto +84 -0
  125. package/proto/cosmos/base/v1beta1/coin.proto +61 -0
  126. package/proto/cosmos_proto/cosmos.proto +112 -0
  127. package/proto/gogoproto/gogo.proto +145 -0
  128. package/proto/google/api/annotations.proto +31 -0
  129. package/proto/google/api/http.proto +370 -0
  130. package/proto/google/protobuf/any.proto +106 -0
  131. package/proto/google/protobuf/duration.proto +115 -0
  132. package/proto/google/protobuf/timestamp.proto +145 -0
  133. package/proto/sentinel/lease/v1/events.proto +52 -0
  134. package/proto/sentinel/lease/v1/genesis.proto +15 -0
  135. package/proto/sentinel/lease/v1/lease.proto +25 -0
  136. package/proto/sentinel/lease/v1/msg.proto +62 -0
  137. package/proto/sentinel/lease/v1/params.proto +17 -0
  138. package/proto/sentinel/node/v3/events.proto +50 -0
  139. package/proto/sentinel/node/v3/genesis.proto +15 -0
  140. package/proto/sentinel/node/v3/msg.proto +63 -0
  141. package/proto/sentinel/node/v3/node.proto +27 -0
  142. package/proto/sentinel/node/v3/params.proto +21 -0
  143. package/proto/sentinel/node/v3/querier.proto +63 -0
  144. package/proto/sentinel/plan/v3/events.proto +41 -0
  145. package/proto/sentinel/plan/v3/genesis.proto +21 -0
  146. package/proto/sentinel/plan/v3/msg.proto +83 -0
  147. package/proto/sentinel/plan/v3/plan.proto +32 -0
  148. package/proto/sentinel/plan/v3/querier.proto +53 -0
  149. package/proto/sentinel/provider/v2/events.proto +16 -0
  150. package/proto/sentinel/provider/v2/genesis.proto +15 -0
  151. package/proto/sentinel/provider/v2/msg.proto +35 -0
  152. package/proto/sentinel/provider/v2/params.proto +17 -0
  153. package/proto/sentinel/provider/v2/provider.proto +24 -0
  154. package/proto/sentinel/provider/v3/genesis.proto +15 -0
  155. package/proto/sentinel/provider/v3/params.proto +13 -0
  156. package/proto/sentinel/session/v3/events.proto +30 -0
  157. package/proto/sentinel/session/v3/genesis.proto +15 -0
  158. package/proto/sentinel/session/v3/msg.proto +50 -0
  159. package/proto/sentinel/session/v3/params.proto +25 -0
  160. package/proto/sentinel/session/v3/proof.proto +25 -0
  161. package/proto/sentinel/session/v3/querier.proto +100 -0
  162. package/proto/sentinel/session/v3/session.proto +50 -0
  163. package/proto/sentinel/subscription/v2/allocation.proto +21 -0
  164. package/proto/sentinel/subscription/v2/payout.proto +22 -0
  165. package/proto/sentinel/subscription/v3/events.proto +65 -0
  166. package/proto/sentinel/subscription/v3/genesis.proto +17 -0
  167. package/proto/sentinel/subscription/v3/msg.proto +83 -0
  168. package/proto/sentinel/subscription/v3/params.proto +21 -0
  169. package/proto/sentinel/subscription/v3/subscription.proto +33 -0
  170. package/proto/sentinel/types/v1/bandwidth.proto +19 -0
  171. package/proto/sentinel/types/v1/price.proto +21 -0
  172. package/proto/sentinel/types/v1/renewal.proto +21 -0
  173. package/proto/sentinel/types/v1/status.proto +16 -0
  174. package/protocol/encoding.js +341 -0
  175. package/protocol/events.js +361 -0
  176. package/protocol/handshake.js +297 -0
  177. package/protocol/index.js +15 -0
  178. package/protocol/messages.js +346 -0
  179. package/protocol/plans.js +199 -0
  180. package/protocol/v2ray.js +268 -0
  181. package/protocol/v3.js +723 -0
  182. package/protocol/wireguard.js +125 -0
  183. package/security/index.js +132 -0
  184. package/session-manager.js +329 -0
  185. package/session-tracker.js +80 -0
  186. package/setup.js +376 -0
  187. package/speedtest/index.js +528 -0
  188. package/speedtest.js +567 -0
  189. package/src/client.ts +502 -0
  190. package/src/index.ts +20 -0
  191. package/state/index.js +347 -0
  192. package/state.js +516 -0
  193. package/test-all-chain-ops.js +493 -0
  194. package/test-all-logic.js +199 -0
  195. package/test-all-msg-types.js +292 -0
  196. package/test-every-connection.js +208 -0
  197. package/test-feegrant-connect.js +98 -0
  198. package/test-logic.js +148 -0
  199. package/test-mainnet.js +176 -0
  200. package/test-plan-lifecycle.js +335 -0
  201. package/tls-trust.js +132 -0
  202. package/tsconfig.build.json +20 -0
  203. package/tsconfig.json +34 -0
  204. package/types/chain.d.ts +746 -0
  205. package/types/connection.d.ts +425 -0
  206. package/types/errors.d.ts +174 -0
  207. package/types/index.d.ts +1380 -0
  208. package/types/nodes.d.ts +187 -0
  209. package/types/pricing.d.ts +156 -0
  210. package/types/protocol.d.ts +332 -0
  211. package/types/session.d.ts +236 -0
  212. package/types/settings.d.ts +192 -0
  213. package/v3protocol.js +1053 -0
  214. package/wallet/index.js +153 -0
  215. package/wireguard.js +307 -0
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Sentinel SDK — Wallet Module
3
+ *
4
+ * Wallet creation, mnemonic validation, key derivation, and address prefix
5
+ * conversion for the Sentinel dVPN chain.
6
+ *
7
+ * Usage:
8
+ * import { createWallet, generateWallet, privKeyFromMnemonic } from './wallet/index.js';
9
+ * import { Wallet } from './wallet/index.js';
10
+ *
11
+ * const { wallet, account } = await createWallet(mnemonic);
12
+ * const { mnemonic, wallet, account } = await Wallet.generate();
13
+ * const provAddr = Wallet.toProvider(account.address);
14
+ */
15
+
16
+ import { Bip39, EnglishMnemonic, Slip10, Slip10Curve, Random } from '@cosmjs/crypto';
17
+ import { makeCosmoshubPath } from '@cosmjs/amino';
18
+ import { DirectSecp256k1HdWallet } from '@cosmjs/proto-signing';
19
+ import { fromBech32, toBech32 } from '@cosmjs/encoding';
20
+ import { ValidationError, ErrorCodes } from '../errors/index.js';
21
+
22
+ // ─── Input Validation Helpers ────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Validate a BIP39 mnemonic string. Returns true if valid, false if not.
26
+ * Use this to enable/disable a "Connect" button in your UI.
27
+ *
28
+ * @param {string} mnemonic - The mnemonic to validate
29
+ * @returns {boolean} True if the mnemonic is a valid 12+ word string
30
+ *
31
+ * @example
32
+ * if (isMnemonicValid(userInput)) showConnectButton();
33
+ */
34
+ export function isMnemonicValid(mnemonic) {
35
+ return typeof mnemonic === 'string' && mnemonic.trim().split(/\s+/).length >= 12;
36
+ }
37
+
38
+ /**
39
+ * Validate a mnemonic and throw ValidationError if invalid.
40
+ * Used internally by createWallet, privKeyFromMnemonic, etc.
41
+ * Also exported for use by the chain module.
42
+ *
43
+ * @param {string} mnemonic - The mnemonic to validate
44
+ * @param {string} fnName - Calling function name (for error messages)
45
+ * @throws {ValidationError} If mnemonic is not a 12+ word string
46
+ */
47
+ export function validateMnemonic(mnemonic, fnName) {
48
+ if (!isMnemonicValid(mnemonic)) {
49
+ throw new ValidationError(ErrorCodes.INVALID_MNEMONIC,
50
+ `${fnName}(): mnemonic must be a 12+ word BIP39 string`,
51
+ { wordCount: typeof mnemonic === 'string' ? mnemonic.trim().split(/\s+/).length : 0 });
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Validate a bech32 address has the expected prefix.
57
+ * Used internally by address conversion functions.
58
+ * Also exported for use by the chain module.
59
+ *
60
+ * @param {string} addr - The address to validate
61
+ * @param {string} prefix - Expected bech32 prefix (e.g. 'sent', 'sentprov')
62
+ * @param {string} fnName - Calling function name (for error messages)
63
+ * @throws {ValidationError} If address doesn't start with expected prefix
64
+ */
65
+ export function validateAddress(addr, prefix, fnName) {
66
+ if (typeof addr !== 'string' || !addr.startsWith(prefix)) {
67
+ throw new ValidationError(ErrorCodes.INVALID_NODE_ADDRESS,
68
+ `${fnName}(): address must be a valid ${prefix}... bech32 string`,
69
+ { value: addr });
70
+ }
71
+ }
72
+
73
+ // ─── Wallet ──────────────────────────────────────────────────────────────────
74
+
75
+ /**
76
+ * Create a Sentinel wallet from a BIP39 mnemonic.
77
+ * Returns { wallet, account } where account.address is the sent1... address.
78
+ */
79
+ export async function createWallet(mnemonic) {
80
+ validateMnemonic(mnemonic, 'createWallet');
81
+ const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'sent' });
82
+ const [account] = await wallet.getAccounts();
83
+ return { wallet, account };
84
+ }
85
+
86
+ /**
87
+ * Generate a new wallet with a fresh random BIP39 mnemonic.
88
+ * @param {number} strength - 128 for 12 words, 256 for 24 words (default: 128)
89
+ * @returns {{ mnemonic: string, wallet: DirectSecp256k1HdWallet, account: { address: string } }}
90
+ */
91
+ export async function generateWallet(strength = 128) {
92
+ const entropy = Random.getBytes(strength / 8);
93
+ const mnemonic = Bip39.encode(entropy).toString();
94
+ const wallet = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, { prefix: 'sent' });
95
+ const [account] = await wallet.getAccounts();
96
+ return { mnemonic, wallet, account };
97
+ }
98
+
99
+ /**
100
+ * Derive the raw secp256k1 private key from a mnemonic.
101
+ * Needed for handshake signatures (node-handshake protocol).
102
+ */
103
+ export async function privKeyFromMnemonic(mnemonic) {
104
+ validateMnemonic(mnemonic, 'privKeyFromMnemonic');
105
+ const seed = await Bip39.mnemonicToSeed(new EnglishMnemonic(mnemonic));
106
+ const { privkey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, makeCosmoshubPath(0));
107
+ return Buffer.from(privkey);
108
+ }
109
+
110
+ // ─── Address Prefix Conversion ───────────────────────────────────────────────
111
+ // Same key, different bech32 prefix. See address-prefixes.md.
112
+
113
+ export function sentToSentprov(sentAddr) {
114
+ validateAddress(sentAddr, 'sent', 'sentToSentprov');
115
+ const { data } = fromBech32(sentAddr);
116
+ return toBech32('sentprov', data);
117
+ }
118
+
119
+ export function sentToSentnode(sentAddr) {
120
+ validateAddress(sentAddr, 'sent', 'sentToSentnode');
121
+ const { data } = fromBech32(sentAddr);
122
+ return toBech32('sentnode', data);
123
+ }
124
+
125
+ export function sentprovToSent(provAddr) {
126
+ validateAddress(provAddr, 'sentprov', 'sentprovToSent');
127
+ const { data } = fromBech32(provAddr);
128
+ return toBech32('sent', data);
129
+ }
130
+
131
+ // ─── Wallet Class (convenience wrapper) ─────────────────────────────────────
132
+
133
+ /**
134
+ * Static utility class wrapping all wallet functions.
135
+ *
136
+ * @example
137
+ * const { wallet, account } = await Wallet.create(mnemonic);
138
+ * const { mnemonic, wallet, account } = await Wallet.generate();
139
+ * const privKey = await Wallet.derivePrivKey(mnemonic);
140
+ * if (Wallet.isValid(input)) { ... }
141
+ * const provAddr = Wallet.toProvider(sentAddr);
142
+ * const nodeAddr = Wallet.toNode(sentAddr);
143
+ * const sentAddr = Wallet.toAccount(provAddr);
144
+ */
145
+ export class Wallet {
146
+ static async create(mnemonic) { return createWallet(mnemonic); }
147
+ static async generate(strength) { return generateWallet(strength); }
148
+ static async derivePrivKey(mnemonic) { return privKeyFromMnemonic(mnemonic); }
149
+ static isValid(mnemonic) { return isMnemonicValid(mnemonic); }
150
+ static toProvider(addr) { return sentToSentprov(addr); }
151
+ static toNode(addr) { return sentToSentnode(addr); }
152
+ static toAccount(addr) { return sentprovToSent(addr); }
153
+ }
package/wireguard.js ADDED
@@ -0,0 +1,307 @@
1
+ // Cross-platform WireGuard tunnel management
2
+ // Windows: wireguard.exe /installtunnelservice (requires admin OR elevation)
3
+ // Linux/macOS: wg-quick up/down (requires root/sudo)
4
+
5
+ import { execSync, execFileSync, spawnSync } from 'child_process';
6
+ import { existsSync, writeFileSync, unlinkSync, mkdirSync, statSync } from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import os from 'os';
10
+ import { sleep } from './defaults.js';
11
+
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+
14
+ // ─── Admin detection ──────────────────────────────────────────────────────────
15
+ function checkIsAdmin() {
16
+ if (process.platform === 'win32') {
17
+ try {
18
+ // "net session" works on all Windows locales (unlike "whoami /groups" which depends on group name language)
19
+ execSync('net session', { stdio: 'ignore', timeout: 3000 });
20
+ return true;
21
+ } catch {
22
+ // Fallback: fsutil requires admin — works on non-English Windows too
23
+ try {
24
+ execSync('fsutil dirty query %systemdrive%', { stdio: 'ignore', timeout: 3000 });
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+ }
31
+ // Linux/macOS: root = UID 0
32
+ try {
33
+ return process.getuid() === 0;
34
+ } catch {
35
+ return false;
36
+ }
37
+ }
38
+
39
+ export const IS_ADMIN = checkIsAdmin();
40
+
41
+ // ─── WireGuard binary detection ───────────────────────────────────────────────
42
+ const WG_PATHS = [
43
+ 'C:\\Program Files\\WireGuard\\wireguard.exe',
44
+ 'C:\\Program Files (x86)\\WireGuard\\wireguard.exe',
45
+ process.env.WIREGUARD_PATH || '',
46
+ ].filter(Boolean);
47
+
48
+ function findWireGuardExe() {
49
+ for (const p of WG_PATHS) {
50
+ if (existsSync(p)) return p;
51
+ }
52
+ try {
53
+ const result = execSync('where wireguard.exe', { encoding: 'utf8', stdio: 'pipe' }).trim();
54
+ if (result) return result.split('\n')[0].trim();
55
+ } catch {}
56
+ return null;
57
+ }
58
+
59
+ function findWgQuick() {
60
+ // wg-quick has no --version flag. Use `which` (Unix) or `where` (Windows) to find it.
61
+ try {
62
+ const cmd = process.platform === 'win32' ? 'where wg-quick' : 'which wg-quick';
63
+ const result = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }).trim();
64
+ if (result) return result.split('\n')[0].trim();
65
+ } catch {}
66
+ // Also check common Linux paths directly
67
+ const paths = ['/usr/bin/wg-quick', '/usr/local/bin/wg-quick', '/usr/sbin/wg-quick'];
68
+ for (const p of paths) {
69
+ if (existsSync(p)) return p;
70
+ }
71
+ return null;
72
+ }
73
+
74
+ export const WG_EXE = findWireGuardExe();
75
+ export const WG_QUICK = findWgQuick();
76
+ export const WG_AVAILABLE = !!(WG_EXE || WG_QUICK);
77
+
78
+ let activeTunnelName = null;
79
+ let activeTunnelConf = null;
80
+ let tunnelInstalledAt = 0; // timestamp when tunnel was installed
81
+
82
+ // ─── Emergency cleanup (exported for process exit handlers) ──────────────────
83
+ /**
84
+ * Force-kill ALL WireGuard tunnels matching "wgsent*".
85
+ * Safe to call multiple times. Does NOT throw.
86
+ * Uses sync APIs only (safe in process exit handlers).
87
+ */
88
+ export function emergencyCleanupSync() {
89
+ // NOTE: Empty catches in this function are intentional.
90
+ // This runs in process exit handlers where console output is unreliable.
91
+ // Best-effort cleanup — each step must not block subsequent steps.
92
+
93
+ // Windows: use wireguard.exe /uninstalltunnelservice + sc query
94
+ if (WG_EXE && process.platform === 'win32') {
95
+ for (const name of ['wgsent0', activeTunnelName].filter(Boolean)) {
96
+ try {
97
+ execFileSync(WG_EXE, ['/uninstalltunnelservice', name], { timeout: 10_000, stdio: 'pipe' });
98
+ } catch {} // service may not exist
99
+ }
100
+ try {
101
+ const services = execFileSync('sc', ['query', 'type=', 'service', 'state=', 'all'], { encoding: 'utf8', timeout: 5000 });
102
+ const matches = services.match(/WireGuardTunnel\$wgsent\S*/g) || [];
103
+ for (const svc of matches) {
104
+ try { execFileSync('sc', ['stop', svc], { timeout: 5000, stdio: 'pipe' }); } catch {} // may already be stopped
105
+ try { execFileSync('sc', ['delete', svc], { timeout: 5000, stdio: 'pipe' }); } catch {} // may already be deleted
106
+ }
107
+ } catch {} // sc query may fail — no services installed
108
+ }
109
+
110
+ // Linux/macOS: use wg-quick down for known tunnel configs
111
+ if (WG_QUICK && process.platform !== 'win32') {
112
+ for (const name of ['wgsent0', activeTunnelName].filter(Boolean)) {
113
+ // Try wg-quick down with the tunnel name or config path
114
+ const confPath = activeTunnelConf || `/tmp/sentinel-wg/${name}.conf`;
115
+ try { execFileSync(WG_QUICK, ['down', confPath], { timeout: 10_000, stdio: 'pipe' }); } catch {} // conf may not exist
116
+ // Also try by interface name directly
117
+ try { execFileSync(WG_QUICK, ['down', name], { timeout: 10_000, stdio: 'pipe' }); } catch {} // interface may not exist
118
+ }
119
+ }
120
+
121
+ activeTunnelName = null;
122
+ activeTunnelConf = null;
123
+ tunnelInstalledAt = 0;
124
+ }
125
+
126
+ /**
127
+ * Check if a tunnel is currently active.
128
+ * Returns { active, name, uptimeMs } or { active: false }.
129
+ * Does NOT kill the tunnel — tunnels stay up until explicitly disconnected.
130
+ */
131
+ export function watchdogCheck() {
132
+ if (!activeTunnelName || tunnelInstalledAt === 0) return { active: false };
133
+ return { active: true, name: activeTunnelName, uptimeMs: Date.now() - tunnelInstalledAt };
134
+ }
135
+
136
+ // ─── Elevated WireGuard runner ────────────────────────────────────────────────
137
+ /**
138
+ * Run a WireGuard command, elevating via PowerShell if not already admin.
139
+ * When already admin: direct execFileSync.
140
+ * When not admin: Start-Process -Verb RunAs -Wait (pops UAC once per call).
141
+ */
142
+ function runWgCommand(args, timeoutMs = 30_000) {
143
+ if (!WG_EXE) throw new Error('WireGuard not found');
144
+
145
+ if (IS_ADMIN) {
146
+ // Already elevated — run directly
147
+ execFileSync(WG_EXE, args, { timeout: timeoutMs, stdio: 'pipe' });
148
+ return;
149
+ }
150
+
151
+ // Not admin — elevate via PowerShell Start-Process -Verb RunAs
152
+ // This pops a one-time UAC dialog per tunnel operation.
153
+ const argStr = args.map(a => `'${a.replace(/'/g, "''")}'`).join(',');
154
+ const ps = `Start-Process -FilePath '${WG_EXE.replace(/'/g, "''")}' -ArgumentList ${argStr} -Verb RunAs -Wait -WindowStyle Hidden`;
155
+ const result = spawnSync('powershell', ['-NoProfile', '-Command', ps], {
156
+ timeout: timeoutMs + 5000,
157
+ encoding: 'utf8',
158
+ });
159
+ if (result.status !== 0) {
160
+ const msg = (result.stderr || result.stdout || '').trim();
161
+ throw new Error(`WireGuard elevated run failed: ${msg || `exit code ${result.status}`}`);
162
+ }
163
+ }
164
+
165
+ // ─── Install tunnel ───────────────────────────────────────────────────────────
166
+ /**
167
+ * Install and activate a WireGuard tunnel.
168
+ * confPath: absolute path to the .conf file.
169
+ * NOTE: activeTunnelName is set ONLY after successful install so that
170
+ * a failed install doesn't cause uninstallWgTunnel to attempt removal of
171
+ * a service that was never registered (avoids "service does not exist" error).
172
+ */
173
+ export async function installWgTunnel(confPath) {
174
+ const name = path.basename(confPath, '.conf'); // e.g. "wgsent0"
175
+
176
+ if (WG_EXE) {
177
+ // Always force-remove any leftover tunnel with this name before installing
178
+ try { runWgCommand(['/uninstalltunnelservice', name], 10_000); } catch { }
179
+ await sleep(1000);
180
+
181
+ runWgCommand(['/installtunnelservice', confPath], 30_000);
182
+ activeTunnelConf = confPath;
183
+ activeTunnelName = name;
184
+ tunnelInstalledAt = Date.now();
185
+
186
+ // VERIFY the service actually started — don't return success on silent failure.
187
+ // WireGuard /installtunnelservice can silently fail if:
188
+ // - Config path is in user temp (SYSTEM account can't read it)
189
+ // - Service registration fails (duplicate name, permission issue)
190
+ // - Config syntax error (WireGuard rejects it quietly)
191
+ const verified = await verifyTunnelRunning(name);
192
+ if (!verified) {
193
+ // Cleanup the failed state
194
+ try { runWgCommand(['/uninstalltunnelservice', name], 10_000); } catch {}
195
+ activeTunnelConf = null;
196
+ activeTunnelName = null;
197
+ tunnelInstalledAt = 0;
198
+ throw new Error(
199
+ `WireGuard tunnel '${name}' failed to start. The service was registered but never reached RUNNING state. ` +
200
+ `This usually means the config file path (${confPath}) is not readable by the SYSTEM account. ` +
201
+ `Ensure the app is running as Administrator.`
202
+ );
203
+ }
204
+
205
+ return activeTunnelName;
206
+ } else if (WG_QUICK) {
207
+ // Force-remove any existing tunnel with this name before installing (prevents "already exists" on Linux)
208
+ try { execFileSync(WG_QUICK, ['down', name], { timeout: 10_000, stdio: 'pipe' }); } catch {}
209
+ await sleep(500);
210
+ execFileSync(WG_QUICK, ['up', confPath], { timeout: 30_000, stdio: 'inherit' });
211
+ activeTunnelConf = confPath;
212
+ activeTunnelName = name;
213
+ tunnelInstalledAt = Date.now();
214
+ // Verify wg-quick created the interface
215
+ await sleep(1000);
216
+ try {
217
+ const wgOut = execFileSync('wg', ['show'], { encoding: 'utf8', timeout: 5000, stdio: 'pipe' });
218
+ if (!wgOut.includes(name)) {
219
+ throw new Error(`WireGuard interface '${name}' not found after wg-quick up. Check permissions (sudo may be required).`);
220
+ }
221
+ } catch (err) {
222
+ if (err.message.includes('not found')) throw err;
223
+ // wg command not available — skip verification
224
+ }
225
+ return activeTunnelName;
226
+ } else {
227
+ const installHint = process.platform === 'win32'
228
+ ? 'Install from https://download.wireguard.com/windows-client/wireguard-installer.exe'
229
+ : process.platform === 'darwin'
230
+ ? 'Install: brew install wireguard-tools'
231
+ : 'Install: sudo apt install wireguard (or equivalent)';
232
+ throw new Error(`WireGuard not found. ${installHint}`);
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Verify a WireGuard tunnel service is actually running.
238
+ * Polls `sc query` over up to 15 seconds with 500ms intervals.
239
+ * Distinguishes START_PENDING (keep waiting) from STOPPED (give up).
240
+ * Returns true if the service reaches RUNNING state.
241
+ */
242
+ async function verifyTunnelRunning(tunnelName, maxWaitMs = 15000) {
243
+ const serviceName = `WireGuardTunnel$${tunnelName}`;
244
+ const start = Date.now();
245
+ const pollInterval = 500;
246
+
247
+ while (Date.now() - start < maxWaitMs) {
248
+ try {
249
+ const out = execFileSync('sc', ['query', serviceName], { encoding: 'utf8', timeout: 3000, stdio: 'pipe' });
250
+ if (out.includes('RUNNING')) return true;
251
+ // Service explicitly stopped or failed — don't keep waiting
252
+ if (out.includes('STOPPED') || out.includes('STOP_PENDING')) return false;
253
+ // START_PENDING — keep polling (normal during WireGuard driver init)
254
+ } catch {
255
+ // Service not registered in SCM yet — keep waiting
256
+ }
257
+ await sleep(pollInterval);
258
+ }
259
+ return false;
260
+ }
261
+
262
+ // ─── Uninstall tunnel ─────────────────────────────────────────────────────────
263
+ export async function uninstallWgTunnel(tunnelName) {
264
+ const name = tunnelName || activeTunnelName;
265
+ if (!name) return;
266
+
267
+ try {
268
+ if (WG_EXE) {
269
+ runWgCommand(['/uninstalltunnelservice', name], 15_000);
270
+ } else if (WG_QUICK) {
271
+ // Use conf path if available, otherwise fall back to interface name
272
+ const target = activeTunnelConf || name;
273
+ execFileSync(WG_QUICK, ['down', target], { timeout: 15_000, stdio: 'pipe' });
274
+ }
275
+ } catch (err) {
276
+ console.error(` [WG] Disconnect warning: ${err.message}`);
277
+ }
278
+
279
+ try {
280
+ if (activeTunnelConf && existsSync(activeTunnelConf)) {
281
+ // Scrub private key before deletion — if unlink fails (file locked),
282
+ // at least the key bytes are zeroed on disk.
283
+ try {
284
+ const size = statSync(activeTunnelConf).size;
285
+ writeFileSync(activeTunnelConf, Buffer.alloc(size, 0));
286
+ } catch { /* scrub best-effort */ }
287
+ unlinkSync(activeTunnelConf);
288
+ }
289
+ } catch { /* cleanup best-effort */ }
290
+ activeTunnelName = null;
291
+ activeTunnelConf = null;
292
+ tunnelInstalledAt = 0;
293
+ }
294
+
295
+ // ─── Legacy compat (still used by old connectWireGuard callers) ───────────────
296
+ export async function connectWireGuard(wgInstance) {
297
+ const tmpDir = path.join(os.tmpdir(), 'sentinel-wg');
298
+ mkdirSync(tmpDir, { recursive: true });
299
+ const confPath = path.join(tmpDir, 'wgsent0.conf');
300
+ wgInstance.writeConfig(confPath);
301
+ return installWgTunnel(confPath);
302
+ }
303
+
304
+ export async function disconnectWireGuard() {
305
+ return uninstallWgTunnel(activeTunnelName);
306
+ }
307
+