neuroalpha 0.3.17 → 0.5.2

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 (39) hide show
  1. package/config/certs/russian_trusted_root_ca.pem +33 -0
  2. package/docs//320/224/320/225/320/237/320/233/320/236/320/231.md +4 -4
  3. package/docs//320/230/320/235/320/241/320/242/320/240/320/243/320/232/320/246/320/230/320/257.md +1 -4
  4. package/docs//320/234/320/225/320/245/320/220/320/235/320/230/320/232/320/220_/320/230_/320/237/320/220/320/240/320/220/320/234/320/225/320/242/320/240/320/253.md +2 -2
  5. package/package.json +60 -53
  6. package/public/activate.html +36 -0
  7. package/public/activate.js +33 -0
  8. package/public/admin.html +126 -7
  9. package/public/app.js +589 -66
  10. package/public/index.html +48 -21
  11. package/public/landing.html +106 -37
  12. package/public/legal.html +4 -4
  13. package/public/styles.css +152 -36
  14. package/server/api/routes.js +308 -37
  15. package/server/auth/index.js +79 -15
  16. package/server/brokers/index.js +1095 -168
  17. package/server/build-info.js +7 -0
  18. package/server/config.js +13 -6
  19. package/server/data/moex.js +6 -4
  20. package/server/db.js +22 -18
  21. package/server/desktop-entry.js +103 -0
  22. package/server/hub.js +28 -14
  23. package/server/index.js +82 -12
  24. package/server/instance/agent.js +259 -0
  25. package/server/instance/central.js +491 -0
  26. package/server/instance/identity.js +79 -0
  27. package/server/instance/provision.js +166 -0
  28. package/server/instance/signing.js +71 -0
  29. package/server/model/edge.js +19 -0
  30. package/server/model/forecast.js +107 -57
  31. package/server/model/live.js +1 -1
  32. package/server/model/models.js +12 -3
  33. package/server/model/research.js +218 -30
  34. package/server/notify/index.js +51 -16
  35. package/server/role.js +44 -0
  36. package/server/security.js +40 -0
  37. package/server/strategist/index.js +86 -47
  38. package/server/strategist/tools.js +231 -42
  39. package/server/tls-ca.js +32 -0
@@ -5,9 +5,19 @@ import path from 'node:path';
5
5
  import { AsyncLocalStorage } from 'node:async_hooks';
6
6
  import { ROOT, DATA_DIR, DEFAULT_CLOUD_URL } from '../config.js';
7
7
  import { db, kvGet, kvSet, registerUidResolver } from '../db.js';
8
+ import { IS_INSTANCE, IS_DESKTOP } from '../role.js';
9
+ import { currentInstanceLicense } from '../instance/agent.js';
10
+ import { isActivated } from '../instance/identity.js';
8
11
 
9
12
  const ctx = new AsyncLocalStorage();
10
- registerUidResolver(() => ctx.getStore()?.user?.id || 1);
13
+ // Фоновые задачи сервера (без контекста запроса) работают от имени владельца (id = 1).
14
+ // Запрос без авторизованного пользователя получает uid = 0 — изолированное пустое пространство данных.
15
+ const resolveUid = () => {
16
+ const store = ctx.getStore();
17
+ if (!store) return 1;
18
+ return store.user?.id || 0;
19
+ };
20
+ registerUidResolver(resolveUid);
11
21
 
12
22
  // ---------- шифрование секретов (AES-256-GCM) ----------
13
23
  let masterKey = null;
@@ -66,14 +76,39 @@ export function hashPassword(password) {
66
76
  return `${salt}:${hash}`;
67
77
  }
68
78
 
69
- export function verifyPassword(password, stored) {
79
+ export async function verifyPassword(password, stored) {
70
80
  if (!stored || !stored.includes(':')) return false;
71
81
  const [salt, hex] = stored.split(':');
72
- const hash = crypto.scryptSync(String(password), salt, 64);
82
+ const hash = await new Promise((resolve, reject) => {
83
+ crypto.scrypt(String(password), salt, 64, (err, key) => (err ? reject(err) : resolve(key)));
84
+ });
73
85
  const target = Buffer.from(hex, 'hex');
74
86
  return hash.length === target.length && crypto.timingSafeEqual(hash, target);
75
87
  }
76
88
 
89
+ // ---------- ограничение попыток входа (по IP и по email) ----------
90
+ const LOGIN_WINDOW_MS = 15 * 60000;
91
+ const LOGIN_MAX_FAILS = 8;
92
+ const loginFails = new Map(); // key -> { count, firstAt }
93
+
94
+ function loginBlockedFor(key) {
95
+ const rec = loginFails.get(key);
96
+ if (!rec) return 0;
97
+ const left = rec.firstAt + LOGIN_WINDOW_MS - Date.now();
98
+ if (left <= 0) { loginFails.delete(key); return 0; }
99
+ return rec.count >= LOGIN_MAX_FAILS ? left : 0;
100
+ }
101
+
102
+ function noteLoginFail(key) {
103
+ const now = Date.now();
104
+ const rec = loginFails.get(key);
105
+ if (!rec || now - rec.firstAt > LOGIN_WINDOW_MS) loginFails.set(key, { count: 1, firstAt: now });
106
+ else rec.count += 1;
107
+ if (loginFails.size > 50000) {
108
+ for (const [k, v] of loginFails) if (now - v.firstAt > LOGIN_WINDOW_MS) loginFails.delete(k);
109
+ }
110
+ }
111
+
77
112
  // ---------- пользователи, тарифные планы (Инвестор, Профи, VIP + режим Хозяин) и сессии ----------
78
113
  export const PLANS = {
79
114
  none: {
@@ -87,7 +122,6 @@ export const PLANS = {
87
122
  allowShorts: false,
88
123
  allowVipBoost: false,
89
124
  allowResearch: false,
90
- maxAnnualReturnPct: 0,
91
125
  maxChatPerDay: 10,
92
126
  },
93
127
  investor: {
@@ -101,7 +135,6 @@ export const PLANS = {
101
135
  allowShorts: false,
102
136
  allowVipBoost: false,
103
137
  allowResearch: false,
104
- maxAnnualReturnPct: 60,
105
138
  maxChatPerDay: 200,
106
139
  },
107
140
  pro: {
@@ -115,7 +148,6 @@ export const PLANS = {
115
148
  allowShorts: true,
116
149
  allowVipBoost: false,
117
150
  allowResearch: false,
118
- maxAnnualReturnPct: 120,
119
151
  maxChatPerDay: 1000,
120
152
  },
121
153
  vip: {
@@ -130,7 +162,7 @@ export const PLANS = {
130
162
  allowShorts: true,
131
163
  allowVipBoost: true,
132
164
  allowResearch: false,
133
- maxAnnualReturnPct: 240,
165
+ allowDesktop: true, // установка на свой компьютер — по договору (VIP)
134
166
  maxChatPerDay: 5000,
135
167
  },
136
168
  owner: {
@@ -145,7 +177,7 @@ export const PLANS = {
145
177
  allowShorts: true,
146
178
  allowVipBoost: true,
147
179
  allowResearch: true,
148
- maxAnnualReturnPct: 0, // 0 = без потолка доходности (полный форсаж >400% годовых)
180
+ allowDesktop: true,
149
181
  maxChatPerDay: 10000,
150
182
  },
151
183
  };
@@ -227,6 +259,15 @@ async function patchUserLicense(cleanKey, baseUrl) {
227
259
 
228
260
  export function effectivePlan(user) {
229
261
  if (!user) return PLANS.none;
262
+ // Машина клиента: тариф только из действующей подписанной лицензии центра
263
+ if (IS_INSTANCE) {
264
+ if (user.id !== 1) return PLANS.none;
265
+ const lic = currentInstanceLicense();
266
+ if (!lic || lic.revoked) return PLANS.none;
267
+ const p = PLANS[lic.plan] || PLANS.none;
268
+ if (p.id !== 'none' && lic.planUntil > 0 && lic.planUntil < Date.now()) return PLANS.none;
269
+ return p;
270
+ }
230
271
  if (process.env.CLOUD_MODE !== '1' && user.id === 1) {
231
272
  if (user.plan && PLANS[user.plan] && user.plan !== 'owner') {
232
273
  if (user.plan !== 'none' && user.plan_until > 0 && user.plan_until < Date.now()) return PLANS.none;
@@ -286,13 +327,24 @@ export function getUserById(id) {
286
327
 
287
328
  export function currentUser() {
288
329
  const store = ctx.getStore();
289
- if (store?.user) return store.user;
330
+ if (store) return store.user || null;
290
331
  return getUserById(1);
291
332
  }
292
333
 
293
334
  export function currentUserId() {
294
- const store = ctx.getStore();
295
- return store?.user?.id || 1;
335
+ return resolveUid();
336
+ }
337
+
338
+ // Локальный режим без входа допускается только с этого же компьютера (не из локальной сети и не через прокси).
339
+ // LOCAL_TRUST_LAN=1 возвращает прежнее поведение (доступ владельца любому устройству в сети).
340
+ export function isLoopbackRequest(req) {
341
+ if (process.env.LOCAL_TRUST_LAN === '1') return true;
342
+ if (req?.headers?.['x-forwarded-for'] || req?.headers?.['x-real-ip'] || req?.headers?.forwarded) return false;
343
+ const addr = String(req?.socket?.remoteAddress || '');
344
+ if (!(addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1')) return false;
345
+ // защита от DNS-rebinding: чужой домен, указывающий на 127.0.0.1, не получает доступ владельца
346
+ const host = String(req?.headers?.host || '').toLowerCase().replace(/:\d+$/, '');
347
+ return host === 'localhost' || host === '127.0.0.1' || host === '[::1]';
296
348
  }
297
349
 
298
350
  export function runWithUser(user, fn) {
@@ -326,8 +378,10 @@ export function userFromRequest(req) {
326
378
  if (u) return u;
327
379
  }
328
380
  }
329
- // В локальном режиме (без CLOUD_MODE=1) по умолчанию открывается владелец (id = 1)
330
- if (process.env.CLOUD_MODE !== '1' && cookies.sid !== 'logged_out') {
381
+ // В локальном режиме (без CLOUD_MODE=1) владелец (id = 1) открывается без входа только с этого компьютера
382
+ // Установка на компьютер клиента после активации — так же, без пароля только с этого компьютера
383
+ const localTrusted = !IS_INSTANCE || (IS_DESKTOP && isActivated());
384
+ if (process.env.CLOUD_MODE !== '1' && localTrusted && cookies.sid !== 'logged_out' && isLoopbackRequest(req)) {
331
385
  return getUserById(1);
332
386
  }
333
387
  return null;
@@ -359,10 +413,20 @@ export function registerUser({ email, password, name }) {
359
413
  return getUserById(Number(r.lastInsertRowid));
360
414
  }
361
415
 
362
- export function loginUser({ email, password }) {
416
+ export async function loginUser({ email, password }, { ip = '' } = {}) {
363
417
  const cleanEmail = String(email || '').trim().toLowerCase();
418
+ const keys = [`e:${cleanEmail}`, ...(ip ? [`ip:${ip}`] : [])];
419
+ const blockedMs = Math.max(...keys.map(loginBlockedFor));
420
+ if (blockedMs > 0) {
421
+ throw new Error(`Слишком много неудачных попыток входа. Повторите через ${Math.ceil(blockedMs / 60000)} мин.`);
422
+ }
364
423
  const u = db.prepare('SELECT * FROM users WHERE email = ?').get(cleanEmail);
365
- if (!u || !verifyPassword(password, u.pass_hash)) throw new Error('Неверный email или пароль');
424
+ const ok = u ? await verifyPassword(password, u.pass_hash) : false;
425
+ if (!ok) {
426
+ for (const k of keys) noteLoginFail(k);
427
+ throw new Error('Неверный email или пароль');
428
+ }
429
+ for (const k of keys) loginFails.delete(k);
366
430
  return u;
367
431
  }
368
432