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,335 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * FULL PLAN LIFECYCLE TEST — Live Mainnet
4
+ *
5
+ * Tests the entire commercial/white-label dVPN flow:
6
+ * 1. Register as provider
7
+ * 2. Create a plan
8
+ * 3. Find a good node and link it to the plan
9
+ * 4. Generate a new user wallet
10
+ * 5. Transfer P2P to the user wallet
11
+ * 6. Subscribe the user to the plan
12
+ * 7. Issue fee grant to the user
13
+ * 8. Connect the user via the plan (gas-free)
14
+ * 9. Verify connection works
15
+ * 10. Disconnect and clean up
16
+ *
17
+ * Cost: ~5-10 P2P total (plan creation + session + transfer + gas)
18
+ */
19
+
20
+ import 'dotenv/config';
21
+ const operatorMnemonic = process.env.MNEMONIC;
22
+ if (!operatorMnemonic) { console.error('Set MNEMONIC in .env'); process.exit(1); }
23
+
24
+ import {
25
+ createWallet, generateWallet, getBalance, formatP2P, sendTokens,
26
+ createClient, broadcast, broadcastWithFeeGrant,
27
+ fetchAllNodes, queryPlanNodes, discoverPlans,
28
+ subscribeToPlan, hasActiveSubscription,
29
+ connectViaPlan, connectDirect, disconnect,
30
+ encodeMsgCreatePlan, encodeMsgUpdatePlanStatus, encodeMsgLinkNode,
31
+ encodeMsgRegisterProvider, encodeMsgStartLease,
32
+ buildFeeGrantMsg, queryFeeGrants,
33
+ sentToSentprov, extractId,
34
+ registerCleanupHandlers, nodeStatusV3, createNodeHttpsAgent,
35
+ DEFAULT_RPC, LCD_ENDPOINTS,
36
+ } from './index.js';
37
+
38
+ registerCleanupHandlers();
39
+
40
+ const R = { pass: 0, fail: 0, errors: [] };
41
+ async function t(name, fn) {
42
+ try {
43
+ const r = await fn();
44
+ if (r) { R.pass++; console.log(' ✓', name); return r; }
45
+ else { R.fail++; R.errors.push(name); console.log(' ✗', name, '→ falsy'); return null; }
46
+ } catch (e) {
47
+ R.fail++; R.errors.push(name + ': ' + e.message?.slice(0, 150));
48
+ console.log(' ✗', name, '→', e.message?.slice(0, 150));
49
+ return null;
50
+ }
51
+ }
52
+
53
+ console.log('═══════════════════════════════════════');
54
+ console.log(' FULL PLAN LIFECYCLE TEST — Live Mainnet');
55
+ console.log('═══════════════════════════════════════\n');
56
+
57
+ // ─── Step 1: Operator wallet setup ──────────────────────────────────────────
58
+ console.log('═══ STEP 1: OPERATOR WALLET ═══');
59
+ const { wallet: opWallet, account: opAccount } = await createWallet(operatorMnemonic);
60
+ const opClient = await createClient(DEFAULT_RPC, opWallet);
61
+ const opBal = await getBalance(opClient, opAccount.address);
62
+ console.log(' Operator:', opAccount.address);
63
+ console.log(' Balance:', formatP2P(opBal.udvpn));
64
+ console.log(' Provider:', sentToSentprov(opAccount.address));
65
+
66
+ if (opBal.udvpn < 5_000_000) {
67
+ console.error(' Need at least 5 P2P to run this test');
68
+ process.exit(1);
69
+ }
70
+
71
+ // ─── Step 2: Register provider (if not already) ────────────────────────────
72
+ console.log('\n═══ STEP 2: REGISTER PROVIDER ═══');
73
+ const provAddr = sentToSentprov(opAccount.address);
74
+ await t('2.1 Register/update provider', async () => {
75
+ // Encoders return raw protobuf bytes — wrap in { typeUrl, value }
76
+ try {
77
+ const msg = {
78
+ typeUrl: '/sentinel.provider.v3.MsgRegisterProviderRequest',
79
+ value: { from: opAccount.address, name: 'SDK Test Provider', identity: '', website: '', description: 'Automated SDK lifecycle test' },
80
+ };
81
+ const result = await broadcast(opClient, opAccount.address, [msg]);
82
+ console.log(' Registered. TX:', result.transactionHash);
83
+ return true;
84
+ } catch (e) {
85
+ if (e.message?.includes('already registered') || e.message?.includes('duplicate')) {
86
+ console.log(' Already registered (OK)');
87
+ return true;
88
+ }
89
+ throw e;
90
+ }
91
+ });
92
+
93
+ // ─── Step 3: Create a plan ──────────────────────────────────────────────────
94
+ console.log('\n═══ STEP 3: CREATE PLAN ═══');
95
+ let planId = null;
96
+ await t('3.1 Create plan (1GB, 30 days, 1 P2P)', async () => {
97
+ // encodeMsgCreatePlan expects: { from, bytes, duration, prices }
98
+ // bytes = total bandwidth in bytes string (1 GB = 1000000000)
99
+ // duration = { seconds: N } — plan validity
100
+ // prices = subscription price array
101
+ const planMsg = {
102
+ typeUrl: '/sentinel.plan.v3.MsgCreatePlanRequest',
103
+ value: {
104
+ from: provAddr,
105
+ bytes: '1000000000', // 1 GB
106
+ duration: { seconds: 30 * 24 * 3600 }, // 30 days
107
+ prices: [{ denom: 'udvpn', base_value: '0.000001000000000000', quote_value: '1000000' }],
108
+ },
109
+ };
110
+ const result = await broadcast(opClient, opAccount.address, [planMsg]);
111
+ planId = extractId(result, /plan/i, ['plan_id', 'id']);
112
+ if (!planId) {
113
+ // Try extracting from events differently
114
+ for (const ev of (result.events || [])) {
115
+ for (const attr of (ev.attributes || [])) {
116
+ if (attr.key === 'plan_id' || attr.key === 'id') {
117
+ planId = attr.value;
118
+ break;
119
+ }
120
+ // Try base64 decode
121
+ try {
122
+ const decoded = Buffer.from(attr.key, 'base64').toString('utf8');
123
+ if (decoded === 'plan_id' || decoded === 'id') {
124
+ planId = Buffer.from(attr.value, 'base64').toString('utf8');
125
+ break;
126
+ }
127
+ } catch {}
128
+ }
129
+ if (planId) break;
130
+ }
131
+ }
132
+ console.log(' Plan created. ID:', planId, 'TX:', result.transactionHash);
133
+ return !!planId;
134
+ });
135
+
136
+ if (!planId) {
137
+ console.error(' Plan creation failed — cannot continue');
138
+ process.exit(1);
139
+ }
140
+
141
+ // ─── Step 3b: Activate plan ─────────────────────────────────────────────────
142
+ await t('3.2 Activate plan', async () => {
143
+ // encodeMsgUpdatePlanStatus expects: { from, id, status }
144
+ const activateMsg = {
145
+ typeUrl: '/sentinel.plan.v3.MsgUpdatePlanStatusRequest',
146
+ value: { from: provAddr, id: parseInt(planId), status: 1 },
147
+ };
148
+ const result = await broadcast(opClient, opAccount.address, [activateMsg]);
149
+ console.log(' Plan activated. TX:', result.transactionHash);
150
+ return result.code === 0;
151
+ });
152
+
153
+ // ─── Step 4: Find a good WG node and link it ───────────────────────────────
154
+ console.log('\n═══ STEP 4: LINK NODE TO PLAN ═══');
155
+ const allNodes = await fetchAllNodes();
156
+ let linkedNode = null;
157
+
158
+ // Find a WG node that responds
159
+ for (const n of allNodes.slice(0, 30)) {
160
+ try {
161
+ const url = 'https://' + (n.remote_addrs?.[0] || '');
162
+ if (!url || url === 'https://') continue;
163
+ const agent = createNodeHttpsAgent(n.address, 'tofu');
164
+ const status = await nodeStatusV3(url, agent);
165
+ if (status.type === 'wireguard' && (!status.address || status.address === n.address)) {
166
+ linkedNode = n;
167
+ console.log(' Found WG node:', n.address, '-', status.moniker);
168
+ break;
169
+ }
170
+ } catch {}
171
+ }
172
+
173
+ if (!linkedNode) {
174
+ console.error(' No reachable WG node found — cannot link');
175
+ process.exit(1);
176
+ }
177
+
178
+ await t('4.1 Lease node (required before linking)', async () => {
179
+ try {
180
+ const gbPrice = linkedNode.gigabyte_prices?.find(p => p.denom === 'udvpn');
181
+ if (!gbPrice) throw new Error('Node has no udvpn pricing');
182
+ // encodeMsgStartLease expects: { from, nodeAddress, hours, maxPrice, renewalPricePolicy }
183
+ // maxPrice must EXACTLY match node's hourly_prices
184
+ const hrPrice = linkedNode.hourly_prices?.find(p => p.denom === 'udvpn') || gbPrice;
185
+ const leaseMsg = {
186
+ typeUrl: '/sentinel.lease.v1.MsgStartLeaseRequest',
187
+ value: {
188
+ from: provAddr,
189
+ nodeAddress: linkedNode.address,
190
+ hours: 1,
191
+ maxPrice: hrPrice, // must match node's price exactly
192
+ renewalPricePolicy: 7,
193
+ },
194
+ };
195
+ const result = await broadcast(opClient, opAccount.address, [leaseMsg]);
196
+ console.log(' Leased. TX:', result.transactionHash);
197
+ return result.code === 0;
198
+ } catch (e) {
199
+ if (e.message?.includes('already exists') || e.message?.includes('active lease')) {
200
+ console.log(' Active lease exists (OK)');
201
+ return true;
202
+ }
203
+ throw e;
204
+ }
205
+ });
206
+
207
+ await t('4.2 Link node to plan', async () => {
208
+ try {
209
+ // encodeMsgLinkNode expects: { from, id, nodeAddress }
210
+ const linkMsg = {
211
+ typeUrl: '/sentinel.plan.v3.MsgLinkNodeRequest',
212
+ value: { from: provAddr, id: parseInt(planId), nodeAddress: linkedNode.address },
213
+ };
214
+ const result = await broadcast(opClient, opAccount.address, [linkMsg]);
215
+ console.log(' Linked. TX:', result.transactionHash);
216
+ return result.code === 0;
217
+ } catch (e) {
218
+ if (e.message?.includes('duplicate') || e.message?.includes('already')) {
219
+ console.log(' Already linked (OK)');
220
+ return true;
221
+ }
222
+ throw e;
223
+ }
224
+ });
225
+
226
+ // Verify plan has nodes
227
+ await t('4.3 queryPlanNodes returns linked node', async () => {
228
+ const { items } = await queryPlanNodes(planId);
229
+ console.log(' Plan nodes:', items.length);
230
+ return items.length > 0;
231
+ });
232
+
233
+ // ─── Step 5: Generate user wallet + fund it ─────────────────────────────────
234
+ console.log('\n═══ STEP 5: CREATE USER WALLET ═══');
235
+ const { mnemonic: userMnemonic, account: userAccount } = await generateWallet();
236
+ console.log(' User address:', userAccount.address);
237
+ console.log(' User mnemonic: (generated, not printed for safety)');
238
+
239
+ await t('5.1 Transfer 2 P2P to user wallet', async () => {
240
+ const result = await sendTokens(opClient, opAccount.address, userAccount.address, '2000000', 'udvpn');
241
+ console.log(' TX:', result.transactionHash);
242
+ return result.code === 0;
243
+ });
244
+
245
+ // Wait for transfer to confirm
246
+ await new Promise(r => setTimeout(r, 5000));
247
+
248
+ await t('5.2 User balance = 2 P2P', async () => {
249
+ const { wallet: uWallet } = await createWallet(userMnemonic);
250
+ const uClient = await createClient(DEFAULT_RPC, uWallet);
251
+ const uBal = await getBalance(uClient, userAccount.address);
252
+ console.log(' User balance:', formatP2P(uBal.udvpn));
253
+ return uBal.udvpn >= 1_000_000;
254
+ });
255
+
256
+ // ─── Step 6: Subscribe user to plan ─────────────────────────────────────────
257
+ console.log('\n═══ STEP 6: SUBSCRIBE TO PLAN ═══');
258
+ await t('6.1 hasActiveSubscription = false before subscribe', async () => {
259
+ const sub = await hasActiveSubscription(userAccount.address, planId);
260
+ console.log(' Has subscription:', sub.has);
261
+ return !sub.has;
262
+ });
263
+
264
+ await t('6.2 subscribeToPlan', async () => {
265
+ const { wallet: uWallet } = await createWallet(userMnemonic);
266
+ const uClient = await createClient(DEFAULT_RPC, uWallet);
267
+ const result = await subscribeToPlan(uClient, userAccount.address, planId);
268
+ console.log(' Subscription ID:', result.subscriptionId, 'TX:', result.txHash);
269
+ return !!result.subscriptionId;
270
+ });
271
+
272
+ await new Promise(r => setTimeout(r, 5000));
273
+
274
+ await t('6.3 hasActiveSubscription = true after subscribe', async () => {
275
+ const sub = await hasActiveSubscription(userAccount.address, planId);
276
+ console.log(' Has subscription:', sub.has);
277
+ return sub.has;
278
+ });
279
+
280
+ // ─── Step 7: Issue fee grant ────────────────────────────────────────────────
281
+ console.log('\n═══ STEP 7: FEE GRANT ═══');
282
+ await t('7.1 Grant fee allowance to user', async () => {
283
+ const grantMsg = buildFeeGrantMsg(opAccount.address, userAccount.address, {
284
+ spendLimit: 5_000_000, // 5 P2P max
285
+ });
286
+ const result = await broadcast(opClient, opAccount.address, [grantMsg]);
287
+ console.log(' TX:', result.transactionHash);
288
+ return result.code === 0;
289
+ });
290
+
291
+ await new Promise(r => setTimeout(r, 3000));
292
+
293
+ await t('7.2 queryFeeGrants shows grant', async () => {
294
+ const lcd = LCD_ENDPOINTS[0]?.url || LCD_ENDPOINTS[0];
295
+ const grants = await queryFeeGrants(lcd, userAccount.address);
296
+ console.log(' Grants received:', grants.length);
297
+ if (grants.length > 0) console.log(' Granter:', grants[0].granter);
298
+ return grants.length > 0;
299
+ });
300
+
301
+ // ─── Step 8: Connect user via plan with fee grant ───────────────────────────
302
+ console.log('\n═══ STEP 8: CONNECT VIA PLAN (FEE GRANTED) ═══');
303
+ await t('8.1 connectViaPlan with feeGranter', async () => {
304
+ const result = await connectViaPlan({
305
+ mnemonic: userMnemonic,
306
+ planId: parseInt(planId),
307
+ nodeAddress: linkedNode.address,
308
+ feeGranter: opAccount.address,
309
+ fullTunnel: false,
310
+ dns: 'handshake',
311
+ v2rayExePath: process.env.V2RAY_PATH || undefined, // SDK auto-detects
312
+ onProgress: (step, detail) => console.log(' [' + step + ']', detail),
313
+ });
314
+ console.log(' Session:', result.sessionId, 'Type:', result.serviceType);
315
+ console.log(' Connected via plan with fee grant!');
316
+
317
+ await disconnect();
318
+ console.log(' Disconnected');
319
+ return result.serviceType === 'wireguard';
320
+ });
321
+
322
+ // ─── RESULTS ────────────────────────────────────────────────────────────────
323
+ console.log('\n═══════════════════════════════════════');
324
+ console.log(' PLAN LIFECYCLE TEST COMPLETE');
325
+ console.log(' Plan ID:', planId);
326
+ console.log(' Linked node:', linkedNode?.address);
327
+ console.log(' User wallet:', userAccount?.address);
328
+ console.log('═══════════════════════════════════════');
329
+ console.log('RESULTS:', R.pass, 'passed,', R.fail, 'failed');
330
+ if (R.errors.length > 0) {
331
+ console.log('\nFAILURES:');
332
+ for (const e of R.errors) console.log(' ✗', e);
333
+ }
334
+ console.log('═══════════════════════════════════════');
335
+ process.exit(R.fail > 0 ? 1 : 0);
package/tls-trust.js ADDED
@@ -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.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,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "node16",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "declaration": true,
12
+ "declarationMap": true,
13
+ "sourceMap": true,
14
+ "outDir": "./dist",
15
+ "rootDir": "./src",
16
+ "lib": ["ES2022", "DOM"]
17
+ },
18
+ "include": ["src/**/*.ts"],
19
+ "exclude": ["node_modules"]
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "node",
6
+ "allowJs": true,
7
+ "checkJs": true,
8
+ "strict": true,
9
+ "noEmit": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationDir": "./types",
16
+ "typeRoots": ["./types", "./node_modules/@types"],
17
+ "paths": {
18
+ "sentinel-dvpn-sdk": ["./types/index.d.ts"]
19
+ }
20
+ },
21
+ "include": [
22
+ "*.js",
23
+ "chain/**/*.js",
24
+ "connection/**/*.js",
25
+ "protocol/**/*.js",
26
+ "types/**/*.d.ts"
27
+ ],
28
+ "exclude": [
29
+ "node_modules",
30
+ "test",
31
+ "examples",
32
+ "bin"
33
+ ]
34
+ }