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/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
|
+
}
|
package/app-settings.js
ADDED
|
@@ -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
|
+
}
|