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,125 @@
1
+ /**
2
+ * Sentinel WireGuard Config Builder
3
+ *
4
+ * Generates WireGuard key pairs and writes .conf files from v3 handshake results.
5
+ * Handles platform-specific security (Windows NTFS ACLs, Unix file permissions).
6
+ */
7
+
8
+ import { randomBytes } from 'crypto';
9
+ import { x25519 } from '@noble/curves/ed25519.js';
10
+ import path from 'path';
11
+ import os from 'os';
12
+ import { mkdirSync, writeFileSync, unlinkSync } from 'fs';
13
+ import { execFileSync } from 'child_process';
14
+ import { SecurityError, ErrorCodes } from '../errors.js';
15
+
16
+ // ─── WireGuard Key Generation ─────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Generate a WireGuard-compatible Curve25519 key pair.
20
+ * Returns { privateKey: Buffer(32), publicKey: Buffer(32) }
21
+ */
22
+ export function generateWgKeyPair() {
23
+ // Generate private key with WireGuard bit clamping
24
+ const priv = Buffer.from(randomBytes(32));
25
+ priv[0] &= 248; // clear bottom 3 bits
26
+ priv[31] &= 127; // clear top bit
27
+ priv[31] |= 64; // set second-highest bit
28
+
29
+ // Derive public key via X25519 (Curve25519 scalar base mult)
30
+ const pub = Buffer.from(x25519.getPublicKey(priv));
31
+
32
+ return { privateKey: priv, publicKey: pub };
33
+ }
34
+
35
+ // ─── Build & Write WireGuard Config ──────────────────────────────────────────
36
+
37
+ /**
38
+ * Write a WireGuard .conf file from v3 handshake result.
39
+ * @param {Buffer} wgPrivKey - Our WireGuard private key (32 bytes)
40
+ * @param {string[]} assignedAddrs - Our assigned IPs from node (e.g. ["10.8.0.2/24"])
41
+ * @param {string} serverPubKey - Server WireGuard public key (base64)
42
+ * @param {string} serverEndpoint - "IP:PORT" for the WireGuard server
43
+ * @param {string[]} [splitIPs] - If provided, only route these IPs through tunnel (split tunneling).
44
+ * Prevents internet death if tunnel cleanup fails.
45
+ * Pass null/empty for full tunnel (0.0.0.0/0) — NOT recommended for testing.
46
+ * @param {object} [opts] - Optional config overrides { mtu, dns, keepalive }
47
+ * @returns {string} Path to the written .conf file
48
+ */
49
+ export function writeWgConfig(wgPrivKey, assignedAddrs, serverPubKey, serverEndpoint, splitIPs = null, opts = {}) {
50
+ // Use a SYSTEM-readable path on Windows. The WireGuard service runs as SYSTEM
51
+ // and often can't read configs from user temp dirs (C:\Users\X\AppData\Local\Temp).
52
+ // C:\ProgramData is readable by all accounts including SYSTEM.
53
+ const tmpDir = process.platform === 'win32'
54
+ ? path.join(process.env.PROGRAMDATA || 'C:\\ProgramData', 'sentinel-wg')
55
+ : path.join(os.tmpdir(), 'sentinel-wg');
56
+ mkdirSync(tmpDir, { recursive: true, mode: 0o700 });
57
+
58
+ // SECURITY: Restrict directory ACL BEFORE writing the config file.
59
+ // The file inherits the directory ACL on creation, closing the race window
60
+ // where the private key would be world-readable between write and ACL set.
61
+ if (process.platform === 'win32') {
62
+ const user = process.env.USERNAME || 'BUILTIN\\Users';
63
+ try {
64
+ execFileSync('icacls', [tmpDir, '/inheritance:r', '/grant:r', `${user}:F`, '/grant:r', 'SYSTEM:F'], { stdio: 'pipe', timeout: 5000 });
65
+ } catch (dirAclErr) {
66
+ throw new SecurityError(ErrorCodes.TLS_CERT_CHANGED, `Failed to secure WireGuard config directory (private key exposure risk): ${dirAclErr.message}`, { tmpDir });
67
+ }
68
+ }
69
+
70
+ const confPath = path.join(tmpDir, 'wgsent0.conf');
71
+ const privKeyBase64 = wgPrivKey.toString('base64');
72
+ const address = assignedAddrs.join(', ');
73
+
74
+ // Split tunneling: only route speedtest target IPs through tunnel.
75
+ // Full tunnel (0.0.0.0/0) captures ALL traffic — if tunnel dies, internet dies.
76
+ const useSplit = splitIPs && splitIPs.length > 0;
77
+ const allowedIPsStr = useSplit
78
+ ? splitIPs.map(ip => ip.includes('/') ? ip : `${ip}/32`).join(', ')
79
+ : '0.0.0.0/0, ::/0';
80
+
81
+ // WireGuard config values — configurable via opts parameter or defaults
82
+ // MTU 1280 = Sentinel nodes configured for 1280. Using 1420 causes TLS failures. (FAILURES.md T2)
83
+ // DNS 10.8.0.1 = node's internal resolver (always reachable inside tunnel). (FAILURES.md T3)
84
+ // External DNS (Cloudflare, Google) may be unreachable through some nodes.
85
+ // Caller can override via opts.dns for specific DNS presets.
86
+ // PersistentKeepalive 15 = safe for all NAT routers (20-30s timeout windows). (FAILURES.md CF6)
87
+ const wgMtu = opts?.mtu || 1280;
88
+ const wgDns = opts?.dns || '10.8.0.1, 1.1.1.1';
89
+ const wgKeepalive = opts?.keepalive || 15;
90
+
91
+ const lines = [
92
+ '[Interface]',
93
+ `PrivateKey = ${privKeyBase64}`,
94
+ `Address = ${address}`,
95
+ `MTU = ${wgMtu}`,
96
+ ];
97
+ // Only set DNS for full tunnel; split tunnel uses system DNS (safer)
98
+ if (!useSplit) lines.push(`DNS = ${wgDns}`);
99
+ lines.push(
100
+ '',
101
+ '[Peer]',
102
+ `PublicKey = ${serverPubKey}`,
103
+ `Endpoint = ${serverEndpoint}`,
104
+ `AllowedIPs = ${allowedIPsStr}`,
105
+ `PersistentKeepalive = ${wgKeepalive}`,
106
+ '',
107
+ );
108
+
109
+ const conf = lines.join('\n');
110
+
111
+ writeFileSync(confPath, conf, { encoding: 'utf8', mode: 0o600 }); // restrict: owner-only read/write
112
+ // On Windows, POSIX mode bits are ignored — file inherits directory ACL set above.
113
+ // Belt-and-suspenders: also set file-level ACL explicitly.
114
+ if (process.platform === 'win32') {
115
+ const user = process.env.USERNAME || 'BUILTIN\\Users';
116
+ try {
117
+ execFileSync('icacls', [confPath, '/inheritance:r', '/grant:r', `${user}:F`, '/grant:r', 'SYSTEM:F'], { stdio: 'pipe', timeout: 5000 });
118
+ } catch (aclErr) {
119
+ // Don't leave an unprotected private key on disk
120
+ try { unlinkSync(confPath); } catch { /* cleanup best-effort */ }
121
+ throw new SecurityError(ErrorCodes.TLS_CERT_CHANGED, `Failed to secure WireGuard config (private key exposure risk): ${aclErr.message}`, { confPath });
122
+ }
123
+ }
124
+ return confPath;
125
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * TLS Trust-On-First-Use (TOFU) for Sentinel Nodes
3
+ *
4
+ * Sentinel nodes use self-signed certificates (no CA issues certs for ephemeral IP servers).
5
+ * TOFU model: save cert fingerprint on first connect, reject if it changes later.
6
+ * Same concept as SSH known_hosts.
7
+ *
8
+ * Usage:
9
+ * import { createNodeHttpsAgent } from './tls-trust.js';
10
+ * const agent = createNodeHttpsAgent('sentnode1abc...', 'tofu');
11
+ * const res = await axios.post(url, body, { httpsAgent: agent });
12
+ */
13
+
14
+ import https from 'https';
15
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
16
+ import path from 'path';
17
+ import os from 'os';
18
+ import { SecurityError, ErrorCodes } from '../errors/index.js';
19
+
20
+ const KNOWN_NODES_DIR = path.join(os.homedir(), '.sentinel-sdk');
21
+ const KNOWN_NODES_PATH = path.join(KNOWN_NODES_DIR, 'known_nodes.json');
22
+
23
+ // In-memory cache to avoid file I/O on every request
24
+ let knownNodesCache = null;
25
+
26
+ function loadKnownNodes() {
27
+ if (knownNodesCache) return knownNodesCache;
28
+ try {
29
+ knownNodesCache = JSON.parse(readFileSync(KNOWN_NODES_PATH, 'utf8'));
30
+ } catch {
31
+ knownNodesCache = {};
32
+ }
33
+ return knownNodesCache;
34
+ }
35
+
36
+ function saveKnownNodes(nodes) {
37
+ knownNodesCache = nodes;
38
+ try {
39
+ if (!existsSync(KNOWN_NODES_DIR)) mkdirSync(KNOWN_NODES_DIR, { recursive: true, mode: 0o700 });
40
+ writeFileSync(KNOWN_NODES_PATH, JSON.stringify(nodes, null, 2), { mode: 0o600 });
41
+ } catch (e) {
42
+ console.warn('[sentinel-sdk] Failed to save known_nodes.json:', e.message);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Create an HTTPS agent with TOFU certificate pinning for a specific node.
48
+ *
49
+ * Modes:
50
+ * - 'tofu' (default): Pin cert on first connection, reject if it changes
51
+ * - 'none': Accept any cert (current behavior, for testing only)
52
+ *
53
+ * @param {string} nodeAddress - sentnode1... address (used as lookup key)
54
+ * @param {'tofu'|'none'} mode - Trust mode
55
+ * @returns {https.Agent}
56
+ */
57
+ export function createNodeHttpsAgent(nodeAddress, mode = 'tofu') {
58
+ if (mode === 'none') {
59
+ return new https.Agent({ rejectUnauthorized: false });
60
+ }
61
+
62
+ const known = loadKnownNodes();
63
+
64
+ return new https.Agent({
65
+ rejectUnauthorized: false,
66
+ checkServerIdentity: (_hostname, cert) => {
67
+ const fingerprint = cert.fingerprint256;
68
+ if (!fingerprint) return new Error('Certificate missing fingerprint — possible MITM or malformed cert');
69
+
70
+ const saved = known[nodeAddress];
71
+
72
+ if (saved && saved.fingerprint !== fingerprint) {
73
+ throw new SecurityError(
74
+ ErrorCodes.TLS_CERT_CHANGED,
75
+ `TLS certificate CHANGED for ${nodeAddress}. ` +
76
+ `Expected: ${saved.fingerprint.substring(0, 20)}... ` +
77
+ `Got: ${fingerprint.substring(0, 20)}... ` +
78
+ `This could indicate a man-in-the-middle attack. ` +
79
+ `If the node legitimately rotated its certificate, call clearKnownNode('${nodeAddress}') or delete ~/.sentinel-sdk/known_nodes.json`,
80
+ { nodeAddress, expected: saved.fingerprint, got: fingerprint, firstSeen: saved.firstSeen },
81
+ );
82
+ }
83
+
84
+ if (!saved) {
85
+ known[nodeAddress] = {
86
+ fingerprint,
87
+ firstSeen: new Date().toISOString(),
88
+ lastSeen: new Date().toISOString(),
89
+ };
90
+ saveKnownNodes(known);
91
+ } else {
92
+ saved.lastSeen = new Date().toISOString();
93
+ saveKnownNodes(known);
94
+ }
95
+ },
96
+ });
97
+ }
98
+
99
+ /**
100
+ * Clear a specific node's stored certificate fingerprint.
101
+ * Call after a node legitimately rotates its TLS cert.
102
+ */
103
+ export function clearKnownNode(nodeAddress) {
104
+ const known = loadKnownNodes();
105
+ delete known[nodeAddress];
106
+ saveKnownNodes(known);
107
+ }
108
+
109
+ /**
110
+ * Clear all stored node certificate fingerprints.
111
+ */
112
+ export function clearAllKnownNodes() {
113
+ saveKnownNodes({});
114
+ }
115
+
116
+ /**
117
+ * Get stored certificate info for a node (null if not known).
118
+ */
119
+ export function getKnownNode(nodeAddress) {
120
+ const known = loadKnownNodes();
121
+ return known[nodeAddress] || null;
122
+ }
123
+
124
+ /**
125
+ * Secure agent for LCD/RPC public endpoints (CA-validated).
126
+ * These endpoints have valid CA-signed certificates — no reason to skip verification.
127
+ * TOFU is only for node-direct connections (self-signed certs).
128
+ */
129
+ export const publicEndpointAgent = new https.Agent({ rejectUnauthorized: true });
130
+
131
+ /** @deprecated Use publicEndpointAgent. Kept for backward compatibility. */
132
+ export const insecureAgent = publicEndpointAgent;
@@ -0,0 +1,329 @@
1
+ /**
2
+ * Sentinel dVPN SDK — Session Manager
3
+ *
4
+ * Reusable session management class ported from the Node Tester's battle-tested
5
+ * implementation. Provides:
6
+ * - Paginated session map (all active sessions for a wallet)
7
+ * - Session reuse (find existing session for a node)
8
+ * - Credential cache (disk-persistent WG keys / V2Ray UUIDs)
9
+ * - Session poisoning (track failed handshakes)
10
+ * - Duplicate payment guard (prevent double-paying in a run)
11
+ *
12
+ * Usage:
13
+ * import { SessionManager } from './session-manager.js';
14
+ * const mgr = new SessionManager('https://lcd.sentinel.co', 'sent1...');
15
+ * await mgr.buildSessionMap();
16
+ * const sid = await mgr.findExistingSession('sentnode1...');
17
+ */
18
+
19
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from 'fs';
20
+ import path from 'path';
21
+ import os from 'os';
22
+ import { ChainError, ErrorCodes } from './errors.js';
23
+ import { DEFAULT_LCD } from './defaults.js';
24
+ import { lcdPaginatedSafe } from './cosmjs-setup.js';
25
+ import { loadPoisonedKeys, savePoisonedKeys } from './state.js';
26
+
27
+ // ─── Constants ───────────────────────────────────────────────────────────────
28
+
29
+ const STATE_DIR = path.join(os.homedir(), '.sentinel-sdk');
30
+ const CRED_FILE = path.join(STATE_DIR, 'session-credentials.json');
31
+
32
+ /** Default session map TTL: 5 minutes */
33
+ const DEFAULT_MAP_TTL = 5 * 60 * 1000;
34
+
35
+ // ─── SessionManager Class ────────────────────────────────────────────────────
36
+
37
+ /**
38
+ * Manages session state for a wallet: session map, credential cache,
39
+ * poisoning, and duplicate payment tracking.
40
+ *
41
+ * @example
42
+ * const mgr = new SessionManager('https://lcd.sentinel.co', 'sent1abc...');
43
+ * await mgr.buildSessionMap();
44
+ * const sid = await mgr.findExistingSession('sentnode1xyz...');
45
+ * if (sid) console.log(`Reuse session ${sid}`);
46
+ */
47
+ export class SessionManager {
48
+ /**
49
+ * @param {string} lcdUrl - LCD endpoint URL (falls back to DEFAULT_LCD)
50
+ * @param {string} walletAddress - Wallet address (sent1...)
51
+ * @param {object} [options]
52
+ * @param {number} [options.mapTtl=300000] - Session map cache TTL in ms (default 5 min)
53
+ * @param {string} [options.credentialPath] - Custom path for credential cache file
54
+ * @param {Function} [options.logger] - Optional logger function (msg) => void
55
+ */
56
+ constructor(lcdUrl, walletAddress, options = {}) {
57
+ this._lcdUrl = lcdUrl || DEFAULT_LCD;
58
+ this._walletAddress = walletAddress;
59
+ this._mapTtl = options.mapTtl ?? DEFAULT_MAP_TTL;
60
+ this._credPath = options.credentialPath || CRED_FILE;
61
+ this._logger = options.logger || null;
62
+
63
+ /** @type {Map<string, {sessionId: bigint, maxBytes: number, usedBytes: number}>|null} */
64
+ this._sessionMap = null;
65
+ this._sessionMapAt = 0;
66
+
67
+ /** @type {Set<string>} Poisoned session keys: "nodeAddr:sessionId" */
68
+ this._poisoned = new Set(loadPoisonedKeys());
69
+
70
+ /** @type {Set<string>} Nodes paid this run */
71
+ this._paidNodes = new Set();
72
+
73
+ /** @type {object|null} In-memory credential cache (lazy-loaded from disk) */
74
+ this._credentials = null;
75
+ }
76
+
77
+ // ─── Session Map ─────────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Fetch ALL active sessions for the wallet with full pagination.
81
+ * Builds a Map<nodeAddr, {sessionId, maxBytes, usedBytes}> for O(1) lookups.
82
+ * Skips exhausted sessions (used >= max), wrong-wallet sessions, and poisoned sessions.
83
+ *
84
+ * @param {string} [walletAddress] - Override wallet address (default: constructor value)
85
+ * @returns {Promise<Map<string, {sessionId: bigint, maxBytes: number, usedBytes: number}>>}
86
+ */
87
+ async buildSessionMap(walletAddress) {
88
+ const addr = walletAddress || this._walletAddress;
89
+ if (!addr) {
90
+ throw new ChainError(
91
+ ErrorCodes.INVALID_OPTIONS,
92
+ 'buildSessionMap requires a wallet address',
93
+ );
94
+ }
95
+
96
+ const map = new Map();
97
+ const queryPath = `/sentinel/session/v3/sessions?address=${addr}&status=1`;
98
+
99
+ let items;
100
+ try {
101
+ const result = await lcdPaginatedSafe(this._lcdUrl, queryPath, 'sessions');
102
+ items = result.items || [];
103
+ } catch (err) {
104
+ throw new ChainError(
105
+ ErrorCodes.LCD_ERROR,
106
+ `Failed to build session map: ${err.message}`,
107
+ { walletAddress: addr, original: err.message },
108
+ );
109
+ }
110
+
111
+ for (const s of items) {
112
+ const bs = s.base_session || s;
113
+ const nodeAddr = bs.node_address || bs.node;
114
+ if (!nodeAddr) continue;
115
+
116
+ const acct = bs.acc_address || bs.address;
117
+ if (acct && acct !== addr) continue;
118
+ if (bs.status && bs.status !== 'active') continue;
119
+
120
+ const maxBytes = parseInt(bs.max_bytes || '0');
121
+ const used = parseInt(bs.download_bytes || '0') + parseInt(bs.upload_bytes || '0');
122
+ if (maxBytes > 0 && used >= maxBytes) continue;
123
+
124
+ const sid = BigInt(bs.id);
125
+ if (this.isPoisoned(nodeAddr, String(sid))) continue;
126
+
127
+ // Keep the session with the most remaining bandwidth per node
128
+ const existing = map.get(nodeAddr);
129
+ if (!existing || (maxBytes - used) > (existing.maxBytes - existing.usedBytes)) {
130
+ map.set(nodeAddr, { sessionId: sid, maxBytes, usedBytes: used });
131
+ }
132
+ }
133
+
134
+ this._sessionMap = map;
135
+ this._sessionMapAt = Date.now();
136
+ this._log(`Session map: ${map.size} reusable sessions (${items.length} fetched)`);
137
+ return map;
138
+ }
139
+
140
+ /**
141
+ * Find an existing reusable session for a node.
142
+ * Auto-refreshes the session map if stale or missing.
143
+ *
144
+ * @param {string} nodeAddr - Node address (sentnode1...)
145
+ * @returns {Promise<bigint|null>} Session ID or null
146
+ */
147
+ async findExistingSession(nodeAddr) {
148
+ try {
149
+ const now = Date.now();
150
+ if (!this._sessionMap || now - this._sessionMapAt > this._mapTtl) {
151
+ await this.buildSessionMap();
152
+ }
153
+ const entry = this._sessionMap?.get(nodeAddr);
154
+ return entry ? entry.sessionId : null;
155
+ } catch (err) {
156
+ if (err?.name !== 'AbortError' && !/timeout|ECONNREFUSED|ENOTFOUND/i.test(err?.message || '')) {
157
+ this._log(`findExistingSession error: ${err.message}`);
158
+ }
159
+ return null;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Invalidate the session map cache, forcing a full refetch on next access.
165
+ */
166
+ invalidateSessionMap() {
167
+ this._sessionMap = null;
168
+ this._sessionMapAt = 0;
169
+ }
170
+
171
+ /**
172
+ * Manually add a session to the map (e.g. after batch payment creates new sessions).
173
+ *
174
+ * @param {string} nodeAddr - Node address
175
+ * @param {bigint} sessionId - Session ID
176
+ * @param {number} [maxBytes=1000000000] - Max bytes (default 1 GB)
177
+ */
178
+ addToSessionMap(nodeAddr, sessionId, maxBytes = 1_000_000_000) {
179
+ if (!this._sessionMap) this._sessionMap = new Map();
180
+ this._sessionMap.set(nodeAddr, { sessionId, maxBytes, usedBytes: 0 });
181
+ }
182
+
183
+ /**
184
+ * Get the current session map (may be null if never built).
185
+ *
186
+ * @returns {Map<string, {sessionId: bigint, maxBytes: number, usedBytes: number}>|null}
187
+ */
188
+ getSessionMap() {
189
+ return this._sessionMap;
190
+ }
191
+
192
+ // ─── Credential Cache (disk-persistent) ──────────────────────────────────
193
+
194
+ /**
195
+ * Save handshake credentials for a node (WG keys, V2Ray UUID, etc.).
196
+ * Persists to disk at ~/.sentinel-sdk/session-credentials.json.
197
+ *
198
+ * @param {string} nodeAddr - Node address (sentnode1...)
199
+ * @param {object} data - Credential data to save
200
+ */
201
+ saveCredential(nodeAddr, data) {
202
+ this._loadCredentials();
203
+ this._credentials[nodeAddr] = { ...data, savedAt: new Date().toISOString() };
204
+ this._writeCredentials();
205
+ }
206
+
207
+ /**
208
+ * Get cached credentials for a node.
209
+ *
210
+ * @param {string} nodeAddr - Node address
211
+ * @returns {object|null} Credential data or null
212
+ */
213
+ getCredential(nodeAddr) {
214
+ this._loadCredentials();
215
+ return this._credentials[nodeAddr] || null;
216
+ }
217
+
218
+ /**
219
+ * Clear cached credentials for a node.
220
+ *
221
+ * @param {string} nodeAddr - Node address
222
+ */
223
+ clearCredential(nodeAddr) {
224
+ this._loadCredentials();
225
+ delete this._credentials[nodeAddr];
226
+ this._writeCredentials();
227
+ }
228
+
229
+ /**
230
+ * Clear all cached credentials.
231
+ */
232
+ clearAllCredentials() {
233
+ this._credentials = {};
234
+ this._writeCredentials();
235
+ }
236
+
237
+ /** @private Load credential store from disk (lazy, once). */
238
+ _loadCredentials() {
239
+ if (this._credentials !== null) return;
240
+ try {
241
+ if (existsSync(this._credPath)) {
242
+ this._credentials = JSON.parse(readFileSync(this._credPath, 'utf8'));
243
+ } else {
244
+ this._credentials = {};
245
+ }
246
+ } catch {
247
+ this._credentials = {};
248
+ }
249
+ }
250
+
251
+ /** @private Write credential store to disk with atomic rename. */
252
+ _writeCredentials() {
253
+ try {
254
+ mkdirSync(path.dirname(this._credPath), { recursive: true, mode: 0o700 });
255
+ const tmp = this._credPath + '.tmp';
256
+ writeFileSync(tmp, JSON.stringify(this._credentials, null, 2), { encoding: 'utf8', mode: 0o600 });
257
+ renameSync(tmp, this._credPath);
258
+ } catch (err) {
259
+ this._log(`Failed to write credentials: ${err.message}`);
260
+ }
261
+ }
262
+
263
+ // ─── Session Poisoning ───────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Mark a session as poisoned (failed handshake — should not be reused).
267
+ *
268
+ * @param {string} nodeAddr - Node address
269
+ * @param {string|bigint} sessionId - Session ID
270
+ */
271
+ markPoisoned(nodeAddr, sessionId) {
272
+ this._poisoned.add(`${nodeAddr}:${sessionId}`);
273
+ savePoisonedKeys([...this._poisoned]);
274
+ }
275
+
276
+ /**
277
+ * Check if a session is poisoned.
278
+ *
279
+ * @param {string} nodeAddr - Node address
280
+ * @param {string|bigint} sessionId - Session ID
281
+ * @returns {boolean}
282
+ */
283
+ isPoisoned(nodeAddr, sessionId) {
284
+ return this._poisoned.has(`${nodeAddr}:${sessionId}`);
285
+ }
286
+
287
+ /**
288
+ * Clear all poisoned session markers.
289
+ */
290
+ clearPoisonedSessions() {
291
+ this._poisoned.clear();
292
+ savePoisonedKeys([]);
293
+ }
294
+
295
+ // ─── Duplicate Payment Guard ─────────────────────────────────────────────
296
+
297
+ /**
298
+ * Mark a node as paid in this run (prevents double-paying).
299
+ *
300
+ * @param {string} nodeAddr - Node address
301
+ */
302
+ markPaid(nodeAddr) {
303
+ this._paidNodes.add(nodeAddr);
304
+ }
305
+
306
+ /**
307
+ * Check if a node has been paid in this run.
308
+ *
309
+ * @param {string} nodeAddr - Node address
310
+ * @returns {boolean}
311
+ */
312
+ isPaid(nodeAddr) {
313
+ return this._paidNodes.has(nodeAddr);
314
+ }
315
+
316
+ /**
317
+ * Clear all paid node markers (e.g. at start of a new scan run).
318
+ */
319
+ clearPaidNodes() {
320
+ this._paidNodes.clear();
321
+ }
322
+
323
+ // ─── Internals ───────────────────────────────────────────────────────────
324
+
325
+ /** @private Log a message if a logger is configured. */
326
+ _log(msg) {
327
+ if (this._logger) this._logger(msg);
328
+ }
329
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Sentinel SDK — Session Payment Mode Tracker
3
+ *
4
+ * The chain doesn't distinguish GB-based from hourly sessions.
5
+ * This persists the payment mode per session ID so apps can
6
+ * show the correct pricing model after restart.
7
+ *
8
+ * Usage:
9
+ * import { trackSession, getSessionMode, getAllTrackedSessions } from './session-tracker.js';
10
+ * trackSession('37546368', 'gb'); // after connectDirect with gigabytes
11
+ * trackSession('37546652', 'hour'); // after connectDirect with hours
12
+ * trackSession('37547890', 'plan'); // after connectViaPlan
13
+ * getSessionMode('37546368'); // → 'gb'
14
+ */
15
+
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
17
+ import path from 'path';
18
+ import os from 'os';
19
+
20
+ const SESSION_DIR = path.join(os.homedir(), '.sentinel-sdk');
21
+ const SESSION_FILE = path.join(SESSION_DIR, 'session-modes.json');
22
+
23
+ let _modes = null; // lazy load
24
+
25
+ function _load() {
26
+ if (_modes) return _modes;
27
+ try {
28
+ if (existsSync(SESSION_FILE)) {
29
+ _modes = JSON.parse(readFileSync(SESSION_FILE, 'utf8'));
30
+ }
31
+ } catch { /* corrupt file — start fresh */ }
32
+ _modes = _modes || {};
33
+ return _modes;
34
+ }
35
+
36
+ function _save() {
37
+ try {
38
+ if (!existsSync(SESSION_DIR)) mkdirSync(SESSION_DIR, { recursive: true });
39
+ writeFileSync(SESSION_FILE, JSON.stringify(_modes, null, 2));
40
+ } catch { /* non-fatal */ }
41
+ }
42
+
43
+ /**
44
+ * Track payment mode for a session.
45
+ * @param {string|number|bigint} sessionId
46
+ * @param {'gb'|'hour'|'plan'} mode
47
+ */
48
+ export function trackSession(sessionId, mode) {
49
+ _load();
50
+ _modes[String(sessionId)] = mode;
51
+ _save();
52
+ }
53
+
54
+ /**
55
+ * Get payment mode for a session.
56
+ * @param {string|number|bigint} sessionId
57
+ * @returns {'gb'|'hour'|'plan'} Defaults to 'gb' for unknown sessions
58
+ */
59
+ export function getSessionMode(sessionId) {
60
+ _load();
61
+ return _modes[String(sessionId)] || 'gb';
62
+ }
63
+
64
+ /**
65
+ * Get all tracked sessions.
66
+ * @returns {Record<string, 'gb'|'hour'|'plan'>}
67
+ */
68
+ export function getAllTrackedSessions() {
69
+ return { ..._load() };
70
+ }
71
+
72
+ /**
73
+ * Clear tracking for a session.
74
+ * @param {string|number|bigint} sessionId
75
+ */
76
+ export function clearSessionMode(sessionId) {
77
+ _load();
78
+ delete _modes[String(sessionId)];
79
+ _save();
80
+ }