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
package/app-helpers.js ADDED
@@ -0,0 +1,363 @@
1
+ /**
2
+ * Sentinel SDK — App Builder Helpers
3
+ *
4
+ * Utilities for building consumer dVPN applications:
5
+ * - Country name → ISO code mapping (80+ countries on Sentinel network)
6
+ * - Flag URL/emoji helpers
7
+ * - Node pricing display formatters (GB + hourly)
8
+ * - Session cost estimation for both pricing models
9
+ *
10
+ * These are NOT protocol functions — they're UI/UX helpers that every
11
+ * consumer app needs but shouldn't have to build from scratch.
12
+ */
13
+
14
+ // ─── Country Name → ISO Code Map ────────────────────────────────────────────
15
+ // Comprehensive map of all country names returned by Sentinel nodes.
16
+ // Includes standard names, variant names (chain returns these), and short codes.
17
+ // 80+ countries confirmed on the Sentinel network as of 2026-03.
18
+
19
+ export const COUNTRY_MAP = Object.freeze({
20
+ // Standard names
21
+ 'united states': 'US', 'germany': 'DE', 'france': 'FR', 'united kingdom': 'GB',
22
+ 'netherlands': 'NL', 'canada': 'CA', 'japan': 'JP', 'singapore': 'SG',
23
+ 'australia': 'AU', 'brazil': 'BR', 'india': 'IN', 'south korea': 'KR',
24
+ 'turkey': 'TR', 'romania': 'RO', 'poland': 'PL', 'spain': 'ES',
25
+ 'italy': 'IT', 'sweden': 'SE', 'norway': 'NO', 'finland': 'FI',
26
+ 'switzerland': 'CH', 'austria': 'AT', 'ireland': 'IE', 'portugal': 'PT',
27
+ 'czech republic': 'CZ', 'hungary': 'HU', 'bulgaria': 'BG', 'greece': 'GR',
28
+ 'ukraine': 'UA', 'russia': 'RU', 'hong kong': 'HK', 'taiwan': 'TW',
29
+ 'thailand': 'TH', 'vietnam': 'VN', 'indonesia': 'ID', 'philippines': 'PH',
30
+ 'mexico': 'MX', 'argentina': 'AR', 'chile': 'CL', 'colombia': 'CO',
31
+ 'south africa': 'ZA', 'israel': 'IL', 'united arab emirates': 'AE',
32
+ 'nigeria': 'NG', 'latvia': 'LV', 'lithuania': 'LT', 'estonia': 'EE',
33
+ 'croatia': 'HR', 'serbia': 'RS', 'denmark': 'DK', 'belgium': 'BE',
34
+ 'luxembourg': 'LU', 'malta': 'MT', 'cyprus': 'CY', 'iceland': 'IS',
35
+ 'new zealand': 'NZ', 'malaysia': 'MY', 'bangladesh': 'BD', 'pakistan': 'PK',
36
+ 'egypt': 'EG', 'kenya': 'KE', 'morocco': 'MA', 'peru': 'PE',
37
+ 'venezuela': 'VE', 'georgia': 'GE', 'guatemala': 'GT', 'puerto rico': 'PR',
38
+ 'china': 'CN', 'saudi arabia': 'SA', 'kazakhstan': 'KZ', 'mongolia': 'MN',
39
+ 'slovakia': 'SK', 'albania': 'AL', 'moldova': 'MD', 'jamaica': 'JM',
40
+ 'bolivia': 'BO', 'ecuador': 'EC', 'uruguay': 'UY', 'bahrain': 'BH',
41
+ 'dr congo': 'CD', 'costa rica': 'CR', 'panama': 'PA', 'paraguay': 'PY',
42
+ 'dominican republic': 'DO', 'el salvador': 'SV', 'honduras': 'HN',
43
+ 'nicaragua': 'NI', 'cuba': 'CU', 'haiti': 'HT', 'trinidad and tobago': 'TT',
44
+
45
+ // Variant names the chain actually returns
46
+ 'the netherlands': 'NL',
47
+ 'türkiye': 'TR',
48
+ 'turkiye': 'TR',
49
+ 'czechia': 'CZ',
50
+ 'russian federation': 'RU',
51
+ 'viet nam': 'VN',
52
+ 'korea': 'KR',
53
+ 'republic of korea': 'KR',
54
+ 'uae': 'AE',
55
+ 'uk': 'GB',
56
+ 'usa': 'US',
57
+ 'democratic republic of the congo': 'CD',
58
+ 'congo': 'CD',
59
+
60
+ // Short codes (some nodes return these directly)
61
+ 'us': 'US', 'de': 'DE', 'fr': 'FR', 'gb': 'GB', 'nl': 'NL', 'ca': 'CA',
62
+ 'jp': 'JP', 'sg': 'SG', 'au': 'AU', 'br': 'BR', 'in': 'IN', 'kr': 'KR',
63
+ 'tr': 'TR', 'ro': 'RO', 'pl': 'PL', 'es': 'ES', 'it': 'IT', 'se': 'SE',
64
+ 'no': 'NO', 'fi': 'FI', 'ch': 'CH', 'at': 'AT', 'ie': 'IE', 'pt': 'PT',
65
+ 'cz': 'CZ', 'hu': 'HU', 'bg': 'BG', 'gr': 'GR', 'ua': 'UA', 'ru': 'RU',
66
+ 'hk': 'HK', 'tw': 'TW', 'th': 'TH', 'vn': 'VN', 'id': 'ID', 'ph': 'PH',
67
+ 'mx': 'MX', 'ar': 'AR', 'cl': 'CL', 'co': 'CO', 'za': 'ZA', 'il': 'IL',
68
+ 'ae': 'AE', 'ng': 'NG', 'lv': 'LV', 'lt': 'LT', 'ee': 'EE', 'hr': 'HR',
69
+ 'rs': 'RS', 'dk': 'DK', 'be': 'BE', 'lu': 'LU', 'mt': 'MT', 'cy': 'CY',
70
+ 'is': 'IS', 'nz': 'NZ', 'my': 'MY', 'bd': 'BD', 'pk': 'PK', 'eg': 'EG',
71
+ 'ke': 'KE', 'ma': 'MA', 'pe': 'PE', 've': 'VE', 'ge': 'GE', 'gt': 'GT',
72
+ 'pr': 'PR', 'cn': 'CN', 'sa': 'SA', 'kz': 'KZ', 'mn': 'MN', 'sk': 'SK',
73
+ 'al': 'AL', 'md': 'MD', 'jm': 'JM', 'bo': 'BO', 'ec': 'EC', 'uy': 'UY',
74
+ 'bh': 'BH', 'cd': 'CD',
75
+ });
76
+
77
+ /**
78
+ * Convert a country name to ISO 3166-1 alpha-2 code.
79
+ * Handles standard names, chain variants ("The Netherlands", "Türkiye"),
80
+ * and short codes. Falls back to fuzzy contains matching.
81
+ *
82
+ * @param {string} name - Country name from node status
83
+ * @returns {string|null} ISO code (uppercase) or null if unknown
84
+ */
85
+ export function countryNameToCode(name) {
86
+ if (!name) return null;
87
+ const lower = name.trim().toLowerCase();
88
+
89
+ // Exact match
90
+ const exact = COUNTRY_MAP[lower];
91
+ if (exact) return exact;
92
+
93
+ // Already a 2-letter code?
94
+ if (lower.length === 2) return lower.toUpperCase();
95
+
96
+ // Fuzzy: find first key that contains or is contained by the input
97
+ for (const [key, code] of Object.entries(COUNTRY_MAP)) {
98
+ if (key.length > 2 && (lower.includes(key) || key.includes(lower))) {
99
+ return code;
100
+ }
101
+ }
102
+
103
+ return null;
104
+ }
105
+
106
+ // ─── Flag Helpers ────────────────────────────────────────────────────────────
107
+
108
+ /**
109
+ * Get flag image URL from flagcdn.com.
110
+ * Use for native apps (WPF, Electron) where emoji flags don't render.
111
+ *
112
+ * @param {string} code - ISO 3166-1 alpha-2 code (e.g. 'US')
113
+ * @param {number} [width=40] - Image width in pixels (flagcdn supports 16-256)
114
+ * @returns {string} URL to PNG flag image
115
+ */
116
+ export function getFlagUrl(code, width = 40) {
117
+ if (!code || code.length !== 2) return '';
118
+ return `https://flagcdn.com/w${width}/${code.toLowerCase()}.png`;
119
+ }
120
+
121
+ /**
122
+ * Get emoji flag for a country code (for web apps / browsers).
123
+ * Uses regional indicator symbols — works in Chrome, Firefox, Safari.
124
+ * Does NOT work in WPF/WinForms — use getFlagUrl() for native Windows apps.
125
+ *
126
+ * @param {string} code - ISO 3166-1 alpha-2 code (e.g. 'US')
127
+ * @returns {string} Emoji flag string (e.g. '🇺🇸')
128
+ */
129
+ export function getFlagEmoji(code) {
130
+ if (!code || code.length !== 2) return '';
131
+ const upper = code.toUpperCase();
132
+ return String.fromCodePoint(
133
+ upper.charCodeAt(0) + 0x1F1A5,
134
+ upper.charCodeAt(1) + 0x1F1A5,
135
+ );
136
+ }
137
+
138
+ // ─── Pricing Display Helpers ────────────────────────────────────────────────
139
+
140
+ /**
141
+ * Parse a chain price entry into a human-readable P2P amount.
142
+ * Chain prices use udvpn (micro denomination, 6 decimals).
143
+ *
144
+ * @param {string|number} udvpnAmount - Raw udvpn amount (e.g. "40152030" or 40152030)
145
+ * @param {number} [decimals=2] - Decimal places
146
+ * @returns {string} Formatted price (e.g. "40.15")
147
+ */
148
+ export function formatPriceP2P(udvpnAmount, decimals = 2) {
149
+ const raw = typeof udvpnAmount === 'string' ? parseInt(udvpnAmount, 10) : udvpnAmount;
150
+ if (!raw || isNaN(raw)) return '0.00';
151
+ return (raw / 1_000_000).toFixed(decimals);
152
+ }
153
+
154
+ /**
155
+ * Format a node's pricing for display in a UI.
156
+ * Returns both GB and hourly prices when available.
157
+ *
158
+ * @param {object} node - Chain node object with gigabyte_prices / hourly_prices
159
+ * @returns {{ perGb: string|null, perHour: string|null, cheapest: 'gb'|'hour'|null }}
160
+ *
161
+ * @example
162
+ * const p = formatNodePricing(node);
163
+ * // { perGb: '0.04 P2P/GB', perHour: '0.02 P2P/hr', cheapest: 'hour' }
164
+ */
165
+ export function formatNodePricing(node) {
166
+ const gbPrice = _extractUdvpnPrice(node.gigabyte_prices || node.GigabytePrices);
167
+ const hrPrice = _extractUdvpnPrice(node.hourly_prices || node.HourlyPrices);
168
+
169
+ const perGb = gbPrice ? `${formatPriceP2P(gbPrice)} P2P/GB` : null;
170
+ const perHour = hrPrice ? `${formatPriceP2P(hrPrice)} P2P/hr` : null;
171
+
172
+ let cheapest = null;
173
+ if (gbPrice && hrPrice) {
174
+ // Rough comparison: 1 GB ≈ 1 hour of streaming at 10Mbps
175
+ // But for user display, we just show both and let them pick
176
+ cheapest = hrPrice < gbPrice ? 'hour' : 'gb';
177
+ } else if (gbPrice) {
178
+ cheapest = 'gb';
179
+ } else if (hrPrice) {
180
+ cheapest = 'hour';
181
+ }
182
+
183
+ return { perGb, perHour, cheapest, gbRaw: gbPrice, hrRaw: hrPrice };
184
+ }
185
+
186
+ /**
187
+ * Estimate session cost for a given duration/amount.
188
+ *
189
+ * @param {object} node - Chain node with pricing
190
+ * @param {'gb'|'hour'} model - Pricing model
191
+ * @param {number} amount - GB or hours
192
+ * @returns {{ cost: string, costUdvpn: number, model: string, amount: number }}
193
+ */
194
+ export function estimateSessionPrice(node, model, amount) {
195
+ const pricing = formatNodePricing(node);
196
+ const raw = model === 'hour' ? pricing.hrRaw : pricing.gbRaw;
197
+ if (!raw) return { cost: 'N/A', costUdvpn: 0, model, amount };
198
+ const totalUdvpn = raw * amount;
199
+ return {
200
+ cost: `${formatPriceP2P(totalUdvpn)} P2P`,
201
+ costUdvpn: totalUdvpn,
202
+ model,
203
+ amount,
204
+ unit: model === 'hour' ? 'hours' : 'GB',
205
+ };
206
+ }
207
+
208
+ /** Extract udvpn price from a chain price array (filter for denom='udvpn'). */
209
+ function _extractUdvpnPrice(prices) {
210
+ if (!prices || !Array.isArray(prices)) return null;
211
+ for (const p of prices) {
212
+ const denom = p.denom || p.Denom;
213
+ if (denom === 'udvpn') {
214
+ const val = p.quote_value || p.base_value || p.amount || p.QuoteValue || p.BaseValue;
215
+ if (val) return parseInt(String(val), 10);
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+
221
+ // ─── Node Display Helpers ───────────────────────────────────────────────────
222
+
223
+ /**
224
+ * Build a display-ready node object for UI rendering.
225
+ * Combines chain data with status enrichment.
226
+ *
227
+ * @param {object} chainNode - Raw chain node
228
+ * @param {object} [status] - Optional node status from nodeStatusV3()
229
+ * @returns {object} Display-ready node with all fields apps need
230
+ */
231
+ export function buildNodeDisplay(chainNode, status = null) {
232
+ const country = status?.location?.country || status?.Location?.Country || null;
233
+ const code = countryNameToCode(country);
234
+
235
+ return {
236
+ address: chainNode.address,
237
+ moniker: status?.moniker || status?.Moniker || null,
238
+ country,
239
+ countryCode: code,
240
+ city: status?.location?.city || status?.Location?.City || null,
241
+ flagUrl: code ? getFlagUrl(code) : null,
242
+ flagEmoji: code ? getFlagEmoji(code) : '',
243
+ serviceType: status?.type || status?.ServiceType || null,
244
+ protocol: status?.type === 'wireguard' ? 'WG' : status?.type === 'v2ray' ? 'V2' : null,
245
+ pricing: formatNodePricing(chainNode),
246
+ peers: status?.peers || status?.Peers || 0,
247
+ maxPeers: status?.max_peers || status?.MaxPeers || 0,
248
+ version: status?.version || status?.Version || null,
249
+ online: status !== null,
250
+ };
251
+ }
252
+
253
+ /**
254
+ * Group nodes by country for sidebar display.
255
+ *
256
+ * @param {object[]} nodes - Array of display-ready nodes (from buildNodeDisplay)
257
+ * @returns {object[]} Sorted array of { country, countryCode, flagUrl, flagEmoji, nodes[], onlineCount, totalCount }
258
+ */
259
+ export function groupNodesByCountry(nodes) {
260
+ const groups = new Map();
261
+
262
+ for (const node of nodes) {
263
+ const key = node.countryCode || 'ZZ'; // ZZ = unknown
264
+ if (!groups.has(key)) {
265
+ groups.set(key, {
266
+ country: node.country || 'Unknown',
267
+ countryCode: key,
268
+ flagUrl: node.flagUrl || '',
269
+ flagEmoji: node.flagEmoji || '',
270
+ nodes: [],
271
+ onlineCount: 0,
272
+ totalCount: 0,
273
+ });
274
+ }
275
+ const g = groups.get(key);
276
+ g.nodes.push(node);
277
+ g.totalCount++;
278
+ if (node.online) g.onlineCount++;
279
+ }
280
+
281
+ // Sort: most nodes first, unknown last
282
+ return [...groups.values()].sort((a, b) => {
283
+ if (a.countryCode === 'ZZ') return 1;
284
+ if (b.countryCode === 'ZZ') return -1;
285
+ return b.onlineCount - a.onlineCount;
286
+ });
287
+ }
288
+
289
+ // ─── Session Duration Helpers ───────────────────────────────────────────────
290
+
291
+ /** Common hour options for hourly session selection UI. */
292
+ export const HOUR_OPTIONS = [1, 2, 4, 8, 12, 24];
293
+
294
+ /** Common GB options for per-GB session selection UI. */
295
+ export const GB_OPTIONS = [1, 2, 5, 10, 25, 50];
296
+
297
+ // ─── Display Formatters ─────────────────────────────────────────────────────
298
+
299
+ /**
300
+ * Format milliseconds into human-readable uptime.
301
+ * @param {number} ms - Milliseconds
302
+ * @returns {string} e.g. "2h 15m", "5m 30s", "45s"
303
+ */
304
+ export function formatUptime(ms) {
305
+ if (!ms || ms < 0) return '0s';
306
+ const sec = Math.floor(ms / 1000);
307
+ const h = Math.floor(sec / 3600);
308
+ const m = Math.floor((sec % 3600) / 60);
309
+ const s = sec % 60;
310
+ if (h > 0) return `${h}h ${m}m`;
311
+ if (m > 0) return `${m}m ${s}s`;
312
+ return `${s}s`;
313
+ }
314
+
315
+ /**
316
+ * Format bytes into human-readable size.
317
+ * @param {number|string} bytes
318
+ * @returns {string} e.g. "1.5 GB", "250 MB", "12 KB"
319
+ */
320
+ export function formatBytes(bytes) {
321
+ const b = typeof bytes === 'string' ? parseInt(bytes, 10) : bytes;
322
+ if (!b || isNaN(b) || b === 0) return '0 B';
323
+ if (b >= 1e9) return (b / 1e9).toFixed(2) + ' GB';
324
+ if (b >= 1e6) return (b / 1e6).toFixed(1) + ' MB';
325
+ if (b >= 1e3) return (b / 1e3).toFixed(0) + ' KB';
326
+ return b + ' B';
327
+ }
328
+
329
+ /**
330
+ * Compute session allocation stats from chain session data.
331
+ * Works for both GB-based and hourly sessions.
332
+ *
333
+ * @param {object} session - Chain session with downloadBytes, uploadBytes, maxBytes, duration, maxDuration
334
+ * @returns {{ usedBytes: number, maxBytes: number, remainingBytes: number, usedPercent: number,
335
+ * usedDisplay: string, maxDisplay: string, remainingDisplay: string,
336
+ * isGbBased: boolean, isHourlyBased: boolean }}
337
+ */
338
+ export function computeSessionAllocation(session) {
339
+ const dl = parseInt(session.downloadBytes || session.download_bytes || '0', 10);
340
+ const ul = parseInt(session.uploadBytes || session.upload_bytes || '0', 10);
341
+ const max = parseInt(session.maxBytes || session.max_bytes || '0', 10);
342
+ const maxDuration = session.maxDuration || session.max_duration || '0s';
343
+
344
+ const usedBytes = dl + ul;
345
+ const remainingBytes = Math.max(0, max - usedBytes);
346
+ const usedPercent = max > 0 ? Math.min(100, (usedBytes / max) * 100) : 0;
347
+
348
+ // GB-based: maxDuration is "0s". Hourly: maxDuration is "3600s" or similar.
349
+ const isHourlyBased = maxDuration !== '0s' && maxDuration !== '0' && maxDuration !== null;
350
+ const isGbBased = !isHourlyBased;
351
+
352
+ return {
353
+ usedBytes,
354
+ maxBytes: max,
355
+ remainingBytes,
356
+ usedPercent: Math.round(usedPercent * 10) / 10,
357
+ usedDisplay: formatBytes(usedBytes),
358
+ maxDisplay: formatBytes(max),
359
+ remainingDisplay: formatBytes(remainingBytes),
360
+ isGbBased,
361
+ isHourlyBased,
362
+ };
363
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Sentinel SDK — App Settings Persistence
3
+ *
4
+ * Typed settings object with defaults, disk persistence, and atomic writes.
5
+ * Covers: DNS, tunnel, session defaults, polling intervals.
6
+ * Every setting has a sane default — apps can use this out of the box.
7
+ *
8
+ * Usage:
9
+ * import { loadAppSettings, saveAppSettings } from './app-settings.js';
10
+ * const settings = loadAppSettings();
11
+ * settings.dnsPreset = 'google';
12
+ * saveAppSettings(settings);
13
+ */
14
+
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+
19
+ const SETTINGS_DIR = path.join(os.homedir(), '.sentinel-sdk');
20
+ const SETTINGS_FILE = path.join(SETTINGS_DIR, 'app-settings.json');
21
+
22
+ // ─── Default Settings ───────────────────────────────────────────────────────
23
+
24
+ /** All settings with their defaults. */
25
+ export const APP_SETTINGS_DEFAULTS = Object.freeze({
26
+ // Network
27
+ dnsPreset: 'handshake', // 'handshake' | 'google' | 'cloudflare' | 'custom'
28
+ customDns: '', // Custom DNS IPs (comma-separated)
29
+
30
+ // Tunnel
31
+ fullTunnel: true, // Route all traffic through VPN
32
+ systemProxy: false, // Set OS SOCKS proxy for V2Ray
33
+ killSwitch: false, // Block all traffic if tunnel drops
34
+ wgMtu: 1420, // WireGuard MTU (1280-1500)
35
+ wgKeepalive: 25, // WireGuard keepalive seconds (15-60)
36
+
37
+ // Session
38
+ defaultGigabytes: 1, // Default GB amount for per-GB sessions
39
+ defaultHours: 1, // Default hour amount for hourly sessions
40
+ preferHourly: false, // Prefer hourly pricing when available
41
+ protocolPreference: 'auto', // 'auto' | 'wireguard' | 'v2ray'
42
+
43
+ // Polling (seconds)
44
+ statusPollSec: 3, // Connection status check
45
+ ipCheckSec: 60, // Public IP check
46
+ balanceCheckSec: 300, // Wallet balance refresh (5 min)
47
+ allocationCheckSec: 120, // Session allocation refresh (2 min)
48
+
49
+ // Plan
50
+ planProbeMax: 500, // Max plan ID to probe in discoverPlans
51
+
52
+ // Favorites
53
+ favoriteNodes: [], // Array of sentnode1... addresses
54
+
55
+ // Last connection
56
+ lastNodeAddress: null, // For quick reconnect
57
+ lastServiceType: null, // 'wireguard' | 'v2ray'
58
+ });
59
+
60
+ // ─── Load / Save ────────────────────────────────────────────────────────────
61
+
62
+ /**
63
+ * Load app settings from disk. Returns defaults for missing/corrupt files.
64
+ * @returns {object} Settings object (mutate + pass to saveAppSettings)
65
+ */
66
+ export function loadAppSettings() {
67
+ try {
68
+ if (existsSync(SETTINGS_FILE)) {
69
+ const raw = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8'));
70
+ // Merge with defaults — new settings get defaults, removed settings get dropped
71
+ return { ...APP_SETTINGS_DEFAULTS, ...raw };
72
+ }
73
+ } catch { /* corrupt file — return defaults */ }
74
+ return { ...APP_SETTINGS_DEFAULTS };
75
+ }
76
+
77
+ /**
78
+ * Save app settings to disk (atomic write).
79
+ * @param {object} settings - Settings object to save
80
+ */
81
+ export function saveAppSettings(settings) {
82
+ try {
83
+ if (!existsSync(SETTINGS_DIR)) mkdirSync(SETTINGS_DIR, { recursive: true });
84
+ const tmpFile = SETTINGS_FILE + '.tmp';
85
+ writeFileSync(tmpFile, JSON.stringify(settings, null, 2));
86
+ renameSync(tmpFile, SETTINGS_FILE);
87
+ } catch { /* non-fatal */ }
88
+ }
89
+
90
+ /**
91
+ * Reset all settings to defaults.
92
+ */
93
+ export function resetAppSettings() {
94
+ saveAppSettings({ ...APP_SETTINGS_DEFAULTS });
95
+ }