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,413 @@
1
+ /**
2
+ * Sentinel AI Path — Decision Engine for Autonomous Agents
3
+ *
4
+ * An autonomous agent calls recommend() BEFORE connect().
5
+ * It receives structured recommendations with alternatives,
6
+ * cost estimates, warnings, and fallback strategies.
7
+ *
8
+ * The agent makes the final decision — the SDK never decides for it.
9
+ */
10
+
11
+ import {
12
+ queryOnlineNodes,
13
+ fetchActiveNodes,
14
+ filterNodes,
15
+ getNodePrices,
16
+ formatP2P,
17
+ IS_ADMIN,
18
+ WG_AVAILABLE,
19
+ TRANSPORT_SUCCESS_RATES,
20
+ COUNTRY_MAP,
21
+ // v1.5.0: RPC queries (protobuf, ~10x faster than LCD for node lists)
22
+ createRpcQueryClientWithFallback,
23
+ rpcQueryNodes,
24
+ } from '../index.js';
25
+
26
+ // ─── Country Proximity Map ──────────────────────────────────────────────────
27
+
28
+ const REGION_MAP = {
29
+ // Western Europe
30
+ 'DE': ['AT', 'CH', 'NL', 'BE', 'LU', 'FR', 'CZ', 'PL', 'DK'],
31
+ 'FR': ['BE', 'LU', 'CH', 'DE', 'ES', 'IT', 'NL', 'GB'],
32
+ 'GB': ['IE', 'NL', 'FR', 'BE', 'DE', 'DK', 'NO'],
33
+ 'NL': ['BE', 'DE', 'GB', 'LU', 'FR', 'DK'],
34
+ // Nordic
35
+ 'SE': ['NO', 'DK', 'FI', 'DE', 'NL', 'EE'],
36
+ 'NO': ['SE', 'DK', 'FI', 'GB', 'DE', 'NL'],
37
+ 'FI': ['SE', 'EE', 'NO', 'DK', 'LV', 'LT'],
38
+ 'DK': ['SE', 'NO', 'DE', 'NL', 'GB'],
39
+ // Eastern Europe
40
+ 'PL': ['CZ', 'SK', 'DE', 'LT', 'UA', 'AT'],
41
+ 'CZ': ['SK', 'DE', 'AT', 'PL'],
42
+ 'RO': ['BG', 'HU', 'MD', 'UA', 'RS'],
43
+ 'UA': ['PL', 'RO', 'MD', 'HU', 'SK', 'CZ'],
44
+ // Southern Europe
45
+ 'IT': ['CH', 'AT', 'FR', 'SI', 'HR'],
46
+ 'ES': ['PT', 'FR', 'IT'],
47
+ 'GR': ['BG', 'TR', 'CY', 'AL', 'MK', 'IT'],
48
+ 'TR': ['GR', 'BG', 'GE', 'CY'],
49
+ // North America
50
+ 'US': ['CA', 'MX'],
51
+ 'CA': ['US'],
52
+ // Asia
53
+ 'JP': ['KR', 'TW', 'HK', 'SG'],
54
+ 'KR': ['JP', 'TW', 'HK', 'SG'],
55
+ 'SG': ['MY', 'ID', 'TH', 'VN', 'HK', 'JP', 'KR', 'TW'],
56
+ 'IN': ['SG', 'AE', 'LK', 'BD'],
57
+ 'AE': ['IN', 'SG', 'BH', 'QA', 'SA'],
58
+ // Oceania
59
+ 'AU': ['NZ', 'SG', 'JP'],
60
+ 'NZ': ['AU', 'SG'],
61
+ // South America
62
+ 'BR': ['AR', 'CL', 'UY', 'CO'],
63
+ 'AR': ['BR', 'CL', 'UY'],
64
+ // Africa
65
+ 'ZA': ['NA', 'BW', 'MZ', 'KE'],
66
+ };
67
+
68
+ /**
69
+ * Get nearby countries sorted by proximity.
70
+ */
71
+ function getNearbyCountries(countryCode) {
72
+ const code = countryCode.toUpperCase();
73
+ const nearby = REGION_MAP[code] || [];
74
+ return nearby;
75
+ }
76
+
77
+ /**
78
+ * Normalize a country input to ISO code.
79
+ */
80
+ function toCountryCode(input) {
81
+ if (!input) return null;
82
+ const upper = input.toUpperCase().trim();
83
+ if (upper.length === 2) return upper;
84
+ // Check COUNTRY_MAP from SDK if available
85
+ if (COUNTRY_MAP) {
86
+ for (const [name, code] of Object.entries(COUNTRY_MAP)) {
87
+ if (name.toUpperCase() === upper) return code;
88
+ }
89
+ }
90
+ // Common names
91
+ const common = {
92
+ 'GERMANY': 'DE', 'UNITED STATES': 'US', 'USA': 'US', 'UNITED KINGDOM': 'GB',
93
+ 'UK': 'GB', 'FRANCE': 'FR', 'JAPAN': 'JP', 'CANADA': 'CA', 'AUSTRALIA': 'AU',
94
+ 'NETHERLANDS': 'NL', 'SWITZERLAND': 'CH', 'SWEDEN': 'SE', 'NORWAY': 'NO',
95
+ 'SINGAPORE': 'SG', 'SOUTH KOREA': 'KR', 'KOREA': 'KR', 'INDIA': 'IN',
96
+ 'BRAZIL': 'BR', 'SPAIN': 'ES', 'ITALY': 'IT', 'TURKEY': 'TR', 'RUSSIA': 'RU',
97
+ 'UKRAINE': 'UA', 'POLAND': 'PL', 'ROMANIA': 'RO', 'FINLAND': 'FI',
98
+ 'DENMARK': 'DK', 'IRELAND': 'IE', 'PORTUGAL': 'PT', 'AUSTRIA': 'AT',
99
+ 'CZECH REPUBLIC': 'CZ', 'CZECHIA': 'CZ', 'HUNGARY': 'HU', 'BELGIUM': 'BE',
100
+ 'SOUTH AFRICA': 'ZA', 'ARGENTINA': 'AR', 'MEXICO': 'MX', 'COLOMBIA': 'CO',
101
+ 'HONG KONG': 'HK', 'TAIWAN': 'TW', 'THAILAND': 'TH', 'VIETNAM': 'VN',
102
+ 'INDONESIA': 'ID', 'MALAYSIA': 'MY', 'PHILIPPINES': 'PH', 'NEW ZEALAND': 'NZ',
103
+ 'UNITED ARAB EMIRATES': 'AE', 'UAE': 'AE', 'ISRAEL': 'IL',
104
+ };
105
+ return common[upper] || null;
106
+ }
107
+
108
+ // ─── recommend() ─────────────────────────────────────────────────────────────
109
+
110
+ /**
111
+ * Generate structured recommendations for an autonomous AI agent.
112
+ *
113
+ * The agent provides its preferences. The SDK returns ranked options
114
+ * with cost estimates, warnings, and fallback strategies.
115
+ * The agent makes the final decision.
116
+ *
117
+ * @param {object} preferences
118
+ * @param {string} [preferences.country] - Preferred country (name or ISO code)
119
+ * @param {number} [preferences.budget] - Available budget in udvpn
120
+ * @param {'reliability'|'cost'|'speed'|'location'} [preferences.priority='reliability'] - What matters most
121
+ * @param {number} [preferences.gigabytes=1] - Planned data usage
122
+ * @param {string} [preferences.protocol] - Force 'wireguard' or 'v2ray'
123
+ * @param {boolean} [preferences.strictCountry=false] - If true, fail if exact country unavailable
124
+ * @param {number} [preferences.maxNodes=50] - Max nodes to evaluate
125
+ *
126
+ * @returns {Promise<{
127
+ * action: 'connect'|'connect-fallback'|'cannot-connect',
128
+ * confidence: number,
129
+ * primary: object|null,
130
+ * alternatives: object[],
131
+ * fallbackStrategy: string,
132
+ * estimatedCost: { udvpn: number, p2p: string },
133
+ * warnings: string[],
134
+ * reasoning: string[],
135
+ * capabilities: { wireguard: boolean, v2ray: boolean, admin: boolean },
136
+ * }>}
137
+ */
138
+ export async function recommend(preferences = {}) {
139
+ if (preferences && typeof preferences !== 'object') {
140
+ throw new Error('recommend(): preferences must be an object or undefined');
141
+ }
142
+ const {
143
+ country = null,
144
+ budget = 0,
145
+ priority = 'reliability',
146
+ gigabytes = 1,
147
+ protocol = null,
148
+ strictCountry = false,
149
+ maxNodes = 50,
150
+ } = preferences;
151
+
152
+ const warnings = [];
153
+ const reasoning = [];
154
+ const countryCode = toCountryCode(country);
155
+
156
+ // ─── Capabilities assessment ───────────────────────────────────────────
157
+
158
+ const canWG = WG_AVAILABLE && IS_ADMIN;
159
+ // Check actual V2Ray availability instead of assuming true
160
+ let canV2 = false;
161
+ try {
162
+ const { getEnvironment } = await import('./environment.js');
163
+ canV2 = getEnvironment().v2ray?.available ?? false;
164
+ } catch { canV2 = true; /* fallback: assume available if detection fails */ }
165
+ const capabilities = { wireguard: canWG, v2ray: canV2, admin: IS_ADMIN };
166
+
167
+ if (protocol === 'wireguard' && !canWG) {
168
+ warnings.push('WireGuard requested but not available (need admin + WireGuard installed). Falling back to V2Ray.');
169
+ reasoning.push('Protocol constraint: WireGuard unavailable, using V2Ray');
170
+ }
171
+ if (!IS_ADMIN && WG_AVAILABLE) {
172
+ warnings.push('WireGuard installed but not admin — running as admin unlocks faster WireGuard nodes');
173
+ }
174
+
175
+ // ─── Fetch nodes ───────────────────────────────────────────────────────
176
+
177
+ reasoning.push('Fetching active nodes from chain...');
178
+ let allNodes;
179
+ try {
180
+ // fetchActiveNodes returns raw chain data (no country/location).
181
+ // If country filter needed, we need enriched data from queryOnlineNodes.
182
+ if (countryCode) {
183
+ reasoning.push('Country filter requested — probing nodes for location data...');
184
+ allNodes = await queryOnlineNodes({ maxNodes: maxNodes * 3 });
185
+ } else {
186
+ // v1.5.0: Try RPC first (protobuf, ~10x faster), fall back to LCD
187
+ try {
188
+ const rpcClient = await createRpcQueryClientWithFallback();
189
+ allNodes = await rpcQueryNodes(rpcClient, { status: 1, limit: 5000 });
190
+ reasoning.push('Used RPC query (fast path)');
191
+ } catch {
192
+ allNodes = await fetchActiveNodes();
193
+ reasoning.push('Used LCD query (fallback)');
194
+ }
195
+ }
196
+ reasoning.push(`Found ${allNodes.length} active nodes`);
197
+ } catch (err) {
198
+ return {
199
+ action: 'cannot-connect',
200
+ confidence: 0,
201
+ primary: null,
202
+ alternatives: [],
203
+ fallbackStrategy: 'none',
204
+ estimatedCost: { udvpn: 0, p2p: '0 P2P' },
205
+ warnings: [`Chain query failed: ${err.message}`],
206
+ reasoning: ['Cannot fetch nodes — network may be unreachable'],
207
+ capabilities,
208
+ };
209
+ }
210
+
211
+ // ─── Filter by protocol ────────────────────────────────────────────────
212
+
213
+ let candidates = [...allNodes];
214
+ const effectiveProtocol = protocol === 'wireguard' && canWG ? 'wireguard'
215
+ : protocol === 'v2ray' ? 'v2ray'
216
+ : null; // auto
217
+
218
+ // Only filter by protocol if nodes have service_type data (enriched/probed nodes).
219
+ // Raw chain data from fetchActiveNodes() does NOT include service_type.
220
+ const hasServiceType = candidates.some(n => n.service_type !== undefined || n.serviceType !== undefined);
221
+
222
+ if (hasServiceType) {
223
+ if (effectiveProtocol === 'wireguard') {
224
+ candidates = candidates.filter(n => String(n.serviceType || n.service_type || '').toLowerCase() === 'wireguard');
225
+ reasoning.push(`Filtered to ${candidates.length} WireGuard nodes`);
226
+ } else if (effectiveProtocol === 'v2ray') {
227
+ candidates = candidates.filter(n => String(n.serviceType || n.service_type || '').toLowerCase() === 'v2ray');
228
+ reasoning.push(`Filtered to ${candidates.length} V2Ray nodes`);
229
+ } else if (!canWG) {
230
+ candidates = candidates.filter(n => String(n.serviceType || n.service_type || '').toLowerCase() === 'v2ray');
231
+ reasoning.push(`No admin — filtered to ${candidates.length} V2Ray nodes`);
232
+ }
233
+ } else {
234
+ reasoning.push(`${candidates.length} nodes from chain (protocol unknown until probe — connectAuto handles selection)`);
235
+ if (!canWG) {
236
+ reasoning.push('No admin — connectAuto will auto-select V2Ray nodes');
237
+ }
238
+ }
239
+
240
+ // ─── Filter by country ─────────────────────────────────────────────────
241
+
242
+ let exactCountryNodes = [];
243
+ let nearbyNodes = [];
244
+ let anyNodes = candidates;
245
+
246
+ if (countryCode) {
247
+ // Try exact country match
248
+ exactCountryNodes = filterNodes(candidates, { country: countryCode });
249
+ reasoning.push(`${exactCountryNodes.length} nodes in ${country} (${countryCode})`);
250
+
251
+ if (exactCountryNodes.length === 0 && !strictCountry) {
252
+ // Try nearby countries
253
+ const nearby = getNearbyCountries(countryCode);
254
+ reasoning.push(`No nodes in ${countryCode}. Checking nearby: ${nearby.join(', ')}`);
255
+
256
+ for (const nc of nearby) {
257
+ const found = filterNodes(candidates, { country: nc });
258
+ if (found.length > 0) {
259
+ nearbyNodes.push(...found.map(n => ({ ...n, _fallbackCountry: nc })));
260
+ reasoning.push(` Found ${found.length} nodes in ${nc}`);
261
+ }
262
+ }
263
+ }
264
+ }
265
+
266
+ // ─── Score and rank ────────────────────────────────────────────────────
267
+
268
+ function scoreNode(node, isExactCountry, isNearby) {
269
+ let score = 50; // base
270
+
271
+ // Protocol bonus
272
+ const isWG = String(node.serviceType || node.service_type || '').toLowerCase() === 'wireguard';
273
+ if (isWG) score += 15; // WireGuard more reliable
274
+
275
+ // Country bonus
276
+ if (isExactCountry) score += 30;
277
+ else if (isNearby) score += 15;
278
+
279
+ // Pricing bonus (cheaper = better if priority is cost)
280
+ const gbPrices = node.gigabyte_prices || [];
281
+ const udvpnPrice = gbPrices.find(p => p.denom === 'udvpn');
282
+ const price = parseInt(udvpnPrice?.quote_value || udvpnPrice?.amount || '999999999', 10);
283
+ if (priority === 'cost') {
284
+ score += Math.max(0, 20 - (price / 50000)); // cheaper gets more points
285
+ }
286
+
287
+ // Quality score from SDK (if enriched)
288
+ if (node.qualityScore) score += node.qualityScore * 0.2;
289
+
290
+ // Peer count: fewer peers = less loaded
291
+ if (node.peers !== undefined) {
292
+ if (node.peers < 5) score += 10;
293
+ else if (node.peers < 20) score += 5;
294
+ else score -= 5;
295
+ }
296
+
297
+ return { ...node, _score: Math.round(score), _price: price, _isWG: isWG };
298
+ }
299
+
300
+ // Build ranked list
301
+ const ranked = [];
302
+
303
+ for (const n of exactCountryNodes) {
304
+ ranked.push(scoreNode(n, true, false));
305
+ }
306
+ for (const n of nearbyNodes) {
307
+ ranked.push(scoreNode(n, false, true));
308
+ }
309
+ // Fill rest from any nodes (not already included)
310
+ const included = new Set(ranked.map(n => n.address || n.acc_address));
311
+ for (const n of anyNodes) {
312
+ const addr = n.address || n.acc_address;
313
+ if (!included.has(addr)) {
314
+ ranked.push(scoreNode(n, false, false));
315
+ }
316
+ }
317
+
318
+ // Sort by score descending
319
+ ranked.sort((a, b) => b._score - a._score);
320
+ const top = ranked.slice(0, maxNodes);
321
+
322
+ // ─── Build recommendation ──────────────────────────────────────────────
323
+
324
+ if (top.length === 0) {
325
+ return {
326
+ action: 'cannot-connect',
327
+ confidence: 0,
328
+ primary: null,
329
+ alternatives: [],
330
+ fallbackStrategy: strictCountry ? 'fail' : 'none',
331
+ estimatedCost: { udvpn: 0, p2p: '0 P2P' },
332
+ warnings: [`No nodes available${country ? ` for ${country}` : ''}`],
333
+ reasoning,
334
+ capabilities,
335
+ };
336
+ }
337
+
338
+ if (countryCode && exactCountryNodes.length === 0 && strictCountry) {
339
+ return {
340
+ action: 'cannot-connect',
341
+ confidence: 0,
342
+ primary: null,
343
+ alternatives: top.slice(0, 5).map(formatNode),
344
+ fallbackStrategy: 'fail — strictCountry is true',
345
+ estimatedCost: { udvpn: 0, p2p: '0 P2P' },
346
+ warnings: [`No nodes in ${country} (${countryCode}). strictCountry=true prevents fallback.`],
347
+ reasoning,
348
+ capabilities,
349
+ };
350
+ }
351
+
352
+ const primary = top[0];
353
+ const alternatives = top.slice(1, 6).map(formatNode);
354
+ const gasCost = 40000;
355
+ const sessionCost = (primary._price || 100000) * gigabytes;
356
+ const totalCost = sessionCost + gasCost;
357
+
358
+ // Budget check
359
+ if (budget > 0 && budget < totalCost) {
360
+ warnings.push(`Budget (${formatP2P(budget)}) may be insufficient for ${gigabytes} GB (estimated ${formatP2P(totalCost)})`);
361
+ }
362
+
363
+ // Determine action
364
+ let action = 'connect';
365
+ let confidence = 0.9;
366
+
367
+ if (countryCode && exactCountryNodes.length === 0) {
368
+ action = 'connect-fallback';
369
+ confidence = 0.7;
370
+ const fc = primary._fallbackCountry || 'nearest available';
371
+ warnings.push(`Exact country ${country} not available. Recommending ${fc} as fallback.`);
372
+ reasoning.push(`Fallback: ${country} → ${fc}`);
373
+ }
374
+
375
+ // Determine fallback strategy
376
+ let fallbackStrategy = 'auto — SDK tries next node on failure';
377
+ if (countryCode && exactCountryNodes.length > 0) {
378
+ fallbackStrategy = `${exactCountryNodes.length} nodes in ${country}; SDK retries within country`;
379
+ } else if (nearbyNodes.length > 0) {
380
+ fallbackStrategy = `nearest-country — ${nearbyNodes.length} nodes in nearby countries`;
381
+ }
382
+
383
+ return {
384
+ action,
385
+ confidence,
386
+ primary: formatNode(primary),
387
+ alternatives,
388
+ fallbackStrategy,
389
+ estimatedCost: { udvpn: totalCost, p2p: formatP2P(totalCost) },
390
+ warnings,
391
+ reasoning,
392
+ capabilities,
393
+ };
394
+ }
395
+
396
+ /**
397
+ * Format a node for the recommendation response.
398
+ */
399
+ function formatNode(node) {
400
+ return {
401
+ address: node.address || node.acc_address,
402
+ country: node.country || node._fallbackCountry || null,
403
+ protocol: node._isWG ? 'wireguard' : 'v2ray',
404
+ score: node._score || 0,
405
+ pricePerGb: node._price ? { udvpn: node._price, p2p: formatP2P(node._price) } : null,
406
+ peers: node.peers ?? null,
407
+ reason: node._fallbackCountry
408
+ ? `Fallback from requested country (nearest: ${node._fallbackCountry})`
409
+ : node._score >= 80 ? 'High reliability score'
410
+ : node._score >= 60 ? 'Good match'
411
+ : 'Available',
412
+ };
413
+ }
@@ -0,0 +1,25 @@
1
+ ' run-admin.vbs
2
+ ' Launches a Node.js script as Administrator.
3
+ ' UAC prompt appears ONCE. Required for WireGuard tunnel operations.
4
+ '
5
+ ' Usage: Double-click this file, OR:
6
+ ' cscript run-admin.vbs (runs setup.js)
7
+ ' cscript run-admin.vbs test-wireguard.mjs (runs specific script)
8
+
9
+ Dim oShell, oFSO, sDir, sScript, sCmd
10
+ Set oShell = CreateObject("Shell.Application")
11
+ Set oFSO = CreateObject("Scripting.FileSystemObject")
12
+
13
+ sDir = oFSO.GetParentFolderName(WScript.ScriptFullName)
14
+
15
+ ' Get script argument or default to setup.js
16
+ If WScript.Arguments.Count > 0 Then
17
+ sScript = WScript.Arguments(0)
18
+ Else
19
+ sScript = "setup.js"
20
+ End If
21
+
22
+ sCmd = "/k title Sentinel AI Path (Admin) && cd /d """ & sDir & """ && echo. && echo Sentinel AI Path - Running as Administrator && echo Script: " & sScript & " && echo. && node " & sScript
23
+
24
+ ' Launch elevated cmd.exe (triggers UAC once)
25
+ oShell.ShellExecute "cmd.exe", sCmd, sDir, "runas", 1