vimp-engine 0.38.1 → 0.38.3

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 (77) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -6
  3. package/bin/vimp-sim.js +2 -2
  4. package/core/Cargo.toml +4 -3
  5. package/package.json +14 -5
  6. package/src/client/boot.js +30 -7
  7. package/src/client/components/controller/ClientReports.js +1 -1
  8. package/src/client/components/controller/Games.js +1 -1
  9. package/src/client/components/model/ClientReports.js +1 -1
  10. package/src/client/components/model/Games.js +24 -0
  11. package/src/client/components/view/Auth.js +4 -2
  12. package/src/client/components/view/ClientReports.js +1 -1
  13. package/src/client/components/view/Games.js +19 -1
  14. package/src/client/components/view/Lobby.js +1 -7
  15. package/src/client/debug.js +1 -1
  16. package/src/client/lib/authApi.js +1 -1
  17. package/src/client/lib/diagnostics.js +19 -4
  18. package/src/client/lib/engineVersion.js +0 -6
  19. package/src/client/lib/footerLink.js +15 -14
  20. package/src/client/lib/formBuilder.js +9 -8
  21. package/src/client/lib/retryOnNetworkError.js +60 -0
  22. package/src/client/main.js +46 -28
  23. package/src/client/network/HiddenHostHealthLog.js +40 -3
  24. package/src/client/network/HostController.js +4 -4
  25. package/src/client/network/HostHealthPolicy.js +41 -0
  26. package/src/client/network/InlineHostBridge.js +8 -1
  27. package/src/client/network/WebSocketTransport.js +1 -1
  28. package/src/client/session/HandoffFlow.js +5 -0
  29. package/src/client/session/HostRole.js +5 -2
  30. package/src/client/style.css +5 -6
  31. package/src/client/views/gameShell.js +3 -3
  32. package/src/client/views/includes/auth.pug +4 -4
  33. package/src/client/views/includes/lobby.pug +6 -5
  34. package/src/client/views/includes/reports.pug +1 -1
  35. package/src/config/authClient.js +4 -5
  36. package/src/config/gameCodes.js +2 -2
  37. package/src/config/lobby.js +4 -5
  38. package/src/config/opcodes.js +2 -1
  39. package/src/config/wsports.js +2 -2
  40. package/src/devtools/builtinScenario.js +3 -3
  41. package/src/devtools/contract/loadContext.js +2 -2
  42. package/src/devtools/contract/rules/a2-package-scripts.js +4 -3
  43. package/src/devtools/contract/rules/a4-vite-config.js +4 -3
  44. package/src/devtools/contract/rules/a7-package-repository.js +7 -7
  45. package/src/devtools/contract/rules/c4-component-dependencies.js +5 -5
  46. package/src/devtools/contract/rules/index.js +1 -1
  47. package/src/devtools/invariants.js +2 -2
  48. package/src/devtools/pluginLoader.js +3 -3
  49. package/src/host/DebugRecorder.js +1 -1
  50. package/src/host/HostGame.js +35 -15
  51. package/src/host/PortMachine.js +6 -5
  52. package/src/host/commandFailure.js +140 -0
  53. package/src/host/host.worker.js +8 -2
  54. package/src/host/meta/SocketManager.js +2 -2
  55. package/src/host/meta/core/CommandProcessor.js +37 -7
  56. package/src/lib/applyRoomOverrides.js +1 -1
  57. package/src/lib/capabilities.js +1 -1
  58. package/src/lib/checkpointCodec.js +1 -2
  59. package/src/lib/coreConfig.js +1 -1
  60. package/src/lib/createHostRuntime.js +1 -1
  61. package/src/lib/gameId.js +1 -1
  62. package/src/lib/gamePlugin.js +6 -2
  63. package/src/lib/jwt.js +1 -1
  64. package/src/lib/loadGamePackage.js +1 -1
  65. package/src/lib/packageLink.js +21 -63
  66. package/src/lib/validators.js +3 -3
  67. package/src/standalone/index.js +6 -6
  68. package/tests/fixtures/generations/gen-api3/README.md +2 -2
  69. package/tests/fixtures/generations/gen-api4/README.md +2 -2
  70. package/bin/vimp-surface.js +0 -88
  71. package/src/config/env.js +0 -104
  72. package/src/config/master.js +0 -288
  73. package/src/devtools/surface/abiParse.js +0 -175
  74. package/src/devtools/surface/collect.js +0 -413
  75. package/src/lib/clientIp.js +0 -96
  76. package/src/lib/rateLimiter.js +0 -40
  77. package/src/lib/security.js +0 -45
@@ -12,7 +12,7 @@ import { ENGINE_CAPABILITIES } from './capabilities.js';
12
12
  // должен зависеть от отладочного контура (граница из CLAUDE.md).
13
13
  //
14
14
  // entries.wasmNode — node-сборка ядра игры (конвенция core/pkg-node,
15
- // docs/en/packaging.md). Для боевой игры она обязательна: без ядра
15
+ // docs.vimp.lgick.dev/en/packaging). Для боевой игры она обязательна: без ядра
16
16
  // авторитетную симуляцию крутить нечем.
17
17
 
18
18
  /**
@@ -1,63 +1,31 @@
1
- // Ссылка на проект для футеров лобби и формы входа: движок показывает рядом
2
- // npm-версию, и клик по соседней ячейке ведёт в репозиторий этого пакета.
1
+ // Адрес проекта пакета — его `repository`: ссылка ведёт на саму игру, в том
2
+ // числе закрытую. `homepage` не читается. Фолбэка нет: пакет без repository
3
+ // адреса не получает — страница пакета в npm маскировала бы недостающие
4
+ // метаданные. О пропущенном поле предупреждает правило контракта A7.
3
5
  //
4
- // Источник — `repository` его package.json (иначе `homepage`). Фолбэка нет:
5
- // пакет, не объявивший ни того, ни другого, ссылки не получает, и ячейка
6
- // футера остаётся пустой. Это сознательно — прежний фолбэк на страницу пакета
7
- // в npm давал разнобой («GitHub» у одной игры, «npm» у другой) и молча
8
- // маскировал недостающие метаданные вместо того, чтобы показать их отсутствие.
9
- // О пропущенном поле предупреждает правило контракта A7.
10
- //
11
- // Хостинг не ограничен github.com: подпись выводится из хоста, поэтому чужой
12
- // плагин на GitLab получает честную ссылку, а не пустую ячейку.
13
- //
14
- // Модуль изоморфный: мастер зовёт resolveProjectUrl для пакета игры
15
- // (GameCatalog), клиент — для собственного package.json движка
16
- // (client/lib/engineVersion.js).
6
+ // Хостинг не ограничен github.com. Модуль изоморфный: мастер зовёт
7
+ // resolveProjectUrl для метаданных пакета игры (адрес уходит в реестр игр),
8
+ // devtools — для package.json игры (правило A7).
17
9
 
18
10
  /**
19
- * Адрес проекта из package.json, приведённый к https: repository, иначе
20
- * homepage.
11
+ * Адрес проекта из package.json, приведённый к http(s): поле repository.
21
12
  * @param {Object} pkg - разобранный package.json
22
- * @returns {string|null} https-URL либо null, если поля нет или это не адрес
13
+ * @returns {string|null} http(s)-URL либо null, если поля нет или это не адрес
23
14
  */
24
15
  export function resolveProjectUrl(pkg) {
25
16
  if (!pkg || typeof pkg !== 'object') {
26
17
  return null;
27
18
  }
28
19
 
29
- const { homepage, repository } = pkg;
20
+ const { repository } = pkg;
30
21
 
31
22
  // repository бывает и строкой ("github:user/repo"), и объектом {type, url}
32
- const declared =
33
- typeof repository === 'string' ? repository : repository?.url;
34
-
35
- // объявленный, но не приводящийся к http(s) repository (пустая строка,
36
- // внутренний git-хост) не повод терять валидный homepage: поле проверяется
37
- // по результату, а не по факту наличия
38
- return normalizeRepository(declared) ?? normalize(homepage);
39
- }
40
-
41
- /**
42
- * Ссылка футера по готовому адресу проекта.
43
- * @param {string|null} [url] - результат resolveProjectUrl
44
- * @returns {{url: string, label: string}|null} null, если адреса нет
45
- */
46
- export function projectLink(url) {
47
- // адрес перепроверяется, хотя свои вызовы приходят уже из
48
- // resolveProjectUrl: функция — публичный экспорт ("./lib/*"), а результат
49
- // уходит прямо в href якоря (client/lib/footerLink.js)
50
- const value = typeof url === 'string' ? url.trim() : '';
51
-
52
- if (!/^https?:\/\//.test(value)) {
53
- return null;
54
- }
55
-
56
- return { url: value, label: labelFor(value) };
23
+ return normalizeRepository(
24
+ typeof repository === 'string' ? repository : repository?.url,
25
+ );
57
26
  }
58
27
 
59
- // шорткат "user/repo" — семантика поля repository (так его читает npm), в
60
- // homepage такая строка означала бы относительный путь, а не адрес на GitHub
28
+ // шорткат "user/repo" — семантика поля repository (так его читает npm)
61
29
  function normalizeRepository(raw) {
62
30
  const value = typeof raw === 'string' ? raw.trim() : '';
63
31
 
@@ -94,8 +62,12 @@ function normalize(raw) {
94
62
 
95
63
  url = url.replace(/^git\+/, '');
96
64
 
97
- // scp-форма git@host:user/repo — двоеточие здесь разделитель, не порт
98
- const scp = /^git@([^:/]+):(.+)$/.exec(url);
65
+ // порт SSH (ssh://host:2222/…) к адресу https не относится
66
+ url = url.replace(/^(ssh:\/\/(?:[^/@]*@)?[^/:@]+):\d+(?=\/|$)/, '$1');
67
+
68
+ // scp-форма git@host:user/repo (в том числе с лишним ssh://, как её
69
+ // нередко пишет npm) — двоеточие здесь разделитель, не порт
70
+ const scp = /^(?:ssh:\/\/)?git@([^:/]+):(.+)$/.exec(url);
99
71
 
100
72
  if (scp) {
101
73
  url = `https://${scp[1]}/${scp[2]}`;
@@ -116,22 +88,8 @@ function normalize(raw) {
116
88
  // в href он уехал бы в DOM и историю браузера
117
89
  .replace(/^(https?:\/\/)[^/@]*@/, '$1')
118
90
  .replace(/\.git(?=$|[#?])/, '')
119
- // npm сам дописывает "#readme" в homepage при генерации package.json
91
+ // якорь #readme в адресе репозитория ссылке не нужен
120
92
  .replace(/#readme$/, '')
121
93
  .replace(/\/+$/, '')
122
94
  );
123
95
  }
124
-
125
- // подпись ячейки: у наших пакетов это всегда GitHub, у чужого хостинга —
126
- // его хост, чтобы подпись не врала об источнике
127
- function labelFor(url) {
128
- let host;
129
-
130
- try {
131
- host = new URL(url).hostname.replace(/^www\./, '');
132
- } catch {
133
- return 'Website';
134
- }
135
-
136
- return host === 'github.com' ? 'GitHub' : host;
137
- }
@@ -13,7 +13,7 @@ const NAME_REGEXP = new RegExp('^[a-zA-Z]([\\w\\s#]{0,13})[\\w]{1}$');
13
13
  export const isValidName = name =>
14
14
  typeof name === 'string' && NAME_REGEXP.test(name);
15
15
 
16
- // ник профиля: копия правила auth-сервиса (packages/auth/src/lib/validators.js)
16
+ // ник профиля: копия правила auth-сервиса (валидаторы auth-сервиса)
17
17
  // — общей рантайм-зависимости у пакетов нет. Строже isValidName: внутри ника
18
18
  // допустим только обычный пробел, не любой \s
19
19
  const NICK_REGEXP = new RegExp('^[a-zA-Z]([\\w #]{0,13})[\\w]{1}$');
@@ -272,8 +272,8 @@ export const clampLimit = (value, fallback, max) => {
272
272
  // на его стороне — минута (lobbyConfig.playerData.minFlushInterval), столько
273
273
  // игр в неё не влезает.
274
274
  //
275
- // Множитель СВЯЗАН с rank.maxPoints auth-сервиса (packages/auth/src/config/
276
- // auth.js): мастер не должен пропускать то, что auth отклонит. Отклонённое
275
+ // Множитель СВЯЗАН с rank.maxPoints из конфига auth-сервиса: мастер
276
+ // не должен пропускать то, что auth отклонит. Отклонённое
277
277
  // тело для хоста неустранимо — PlayerDataSync повторял бы его до конца жизни
278
278
  // комнаты, — поэтому при правке per-game maxGameScore проверяется и потолок
279
279
  // суммы на той стороне.
@@ -10,7 +10,7 @@ import { ensureGameShell } from '../client/views/gameShell.js';
10
10
  // репозитория игры — `npm run dev` открывает вкладку с работающим матчем без
11
11
  // мастера, без входа и без экрана лобби.
12
12
  //
13
- // Отличия от прода (осознанные, см. plan/standalone-sdk/README.md):
13
+ // Отличия от прода (осознанные):
14
14
  // - хост крутится в главном потоке (inline), а не в Worker'е: HostPlugin
15
15
  // непередаваем постмесседжем, а SDK получает живой объект плагина;
16
16
  // - личность гостевая (ник из формы или из playerName), рейтинг и
@@ -44,7 +44,7 @@ import { ensureGameShell } from '../client/views/gameShell.js';
44
44
  * @param {boolean} [options.devMode] - room.isDevMode: рекордер и хостовый
45
45
  * CONSOLE-лог.
46
46
  * @param {string|null} [options.reportUrl] - Адрес приёма журнала клиентских
47
- * ошибок (`POST`, формат — docs/en/client.md, «Error reporting»). По
47
+ * ошибок (`POST`, формат — docs.vimp.lgick.dev/en/standalone). По
48
48
  * умолчанию `null`: в solo нет бокса, и отчёты не отправляются.
49
49
  * @returns {Promise<Object>} `{ stop() }` — останов матча.
50
50
  */
@@ -71,10 +71,10 @@ export async function startStandaloneGame({
71
71
  // возможность, которой в этой сборке движка нет, иначе всплыла бы где-то
72
72
  // в середине хендшейка непрозрачной ошибкой. Мастера тут нет, а значит нет
73
73
  // и GameManifest с его `requires` — источником служат сами половины
74
- // плагина (необязательное поле `requires`, docs/en/plugin-api.md) либо
75
- // опция вызова, если встраивающий знает лучше. Одна проверка вместо двух:
76
- // сообщение адресовано разработчику встраивания, делить его по половинам
77
- // незачем
74
+ // плагина (необязательное поле `requires`, docs.vimp.lgick.dev/en/plugin-api)
75
+ // либо опция вызова, если встраивающий знает лучше. Одна проверка вместо
76
+ // двух: сообщение адресовано разработчику встраивания, делить его по
77
+ // половинам незачем
78
78
  requireCompatible({
79
79
  id: hostPlugin.id,
80
80
  // mergeRequires, а не спред: строка вместо массива ('accolades' —
@@ -1,7 +1,7 @@
1
1
  # gen-api3 — ЗАМОРОЖЕНО
2
2
 
3
3
  Снимок плагина 2026-08-29, поколение `engineApi: 3`.
4
- Не править: правка фикстуры маскирует слом совместимости
5
- (этап 1 плана `plan/plugin-forward-compat`). Новое поколение
4
+ Не править: правка фикстуры маскирует слом совместимости.
5
+ Новое поколение
6
6
  добавляется копированием текущего `../../miniGame`, а не правкой
7
7
  этого каталога.
@@ -1,7 +1,7 @@
1
1
  # gen-api4 — ЗАМОРОЖЕНО
2
2
 
3
3
  Снимок плагина 2026-08-29, поколение `engineApi: 4`.
4
- Не править: правка фикстуры маскирует слом совместимости
5
- (этап 1 плана `plan/plugin-forward-compat`). Новое поколение
4
+ Не править: правка фикстуры маскирует слом совместимости.
5
+ Новое поколение
6
6
  добавляется копированием текущего `../../miniGame`, а не правкой
7
7
  этого каталога.
@@ -1,88 +0,0 @@
1
- #!/usr/bin/env node
2
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
- import {
6
- collectSurface,
7
- formatSurface,
8
- diffSurface,
9
- } from '../src/devtools/surface/collect.js';
10
-
11
- // CLI слепка плагинной поверхности (этап 1 плана plugin-forward-compat):
12
- // без флага печатает расхождение с закоммиченным contract/surface.json и
13
- // возвращает 1 при нарушении И1/И3, с --write перезаписывает слепок.
14
- // Внутренний инструмент репозитория движка: в поле `bin` публикуемого
15
- // пакета он не значится.
16
-
17
- const SURFACE_PATH = fileURLToPath(
18
- new URL('../contract/surface.json', import.meta.url),
19
- );
20
-
21
- const USAGE = `Usage: vimp-surface [options]
22
-
23
- --write rewrite contract/surface.json with the collected surface
24
- --help
25
- `;
26
-
27
- async function main(argv) {
28
- if (argv.includes('--help')) {
29
- process.stdout.write(USAGE);
30
- return 0;
31
- }
32
-
33
- const surface = await collectSurface();
34
-
35
- if (argv.includes('--write')) {
36
- await mkdir(path.dirname(SURFACE_PATH), { recursive: true });
37
- await writeFile(SURFACE_PATH, formatSurface(surface));
38
- process.stdout.write(`surface: written ${SURFACE_PATH}\n`);
39
-
40
- return 0;
41
- }
42
-
43
- let committed;
44
-
45
- try {
46
- committed = JSON.parse(await readFile(SURFACE_PATH, 'utf8'));
47
- } catch {
48
- process.stderr.write(
49
- `surface: no snapshot at ${SURFACE_PATH} — run ` +
50
- '`npm run surface:update` to create it\n',
51
- );
52
-
53
- return 1;
54
- }
55
-
56
- const { violations, additions } = diffSurface(committed, surface);
57
-
58
- for (const addition of additions) {
59
- process.stdout.write(`surface: добавлено ${addition}\n`);
60
- }
61
-
62
- if (additions.length > 0 && violations.length === 0) {
63
- process.stdout.write(
64
- 'слепок устарел, запусти `npm run surface:update` (добавление ' +
65
- 'поверхности совместимость не ломает)\n',
66
- );
67
- }
68
-
69
- for (const violation of violations) {
70
- process.stderr.write(`${violation}\n\n`);
71
- }
72
-
73
- if (violations.length === 0 && additions.length === 0) {
74
- process.stdout.write('surface: слепок совпадает\n');
75
- }
76
-
77
- return violations.length > 0 ? 1 : 0;
78
- }
79
-
80
- main(process.argv.slice(2)).then(
81
- code => {
82
- process.exitCode = code;
83
- },
84
- err => {
85
- process.stderr.write(`${err.message}\n`);
86
- process.exitCode = 1;
87
- },
88
- );
package/src/config/env.js DELETED
@@ -1,104 +0,0 @@
1
- // Env-переопределения серверного конфига (Этап 4 плана standalone-sdk).
2
- // Раньше этот блок жил в master/main.js под условием NODE_ENV=production:
3
- // лобби-мастер в dev работает на дефолтах конфига. Dedicated-серверу так
4
- // нельзя — игру, порт и настройки комнаты он получает только из окружения,
5
- // в том числе локально, — поэтому чтение вынесено сюда и применяется каждым
6
- // входом по своим правилам.
7
-
8
- /**
9
- * Применяет env-переопределения к конфигу мастера.
10
- * @param {Object} config - Синглтон lib/config.js.
11
- * @param {Object} [env] - Окружение (по умолчанию process.env).
12
- */
13
- export function applyMasterEnv(config, env = process.env) {
14
- if (env.VIMP_DOMAIN) {
15
- config.set('master:domain', env.VIMP_DOMAIN);
16
- }
17
-
18
- // порт серверного процесса
19
- if (env.VIMP_MASTER_PORT) {
20
- config.set('master:port', Number(env.VIMP_MASTER_PORT));
21
- }
22
-
23
- // домен central auth-сервиса — попадает в CSP connect-src, т.к. лобби
24
- // делает туда прямые fetch (ручки профиля)
25
- if (env.VIMP_AUTH_SERVICE_URL) {
26
- config.set('master:security:authServiceUrl', env.VIMP_AUTH_SERVICE_URL);
27
- }
28
-
29
- // корень хранилища игровых пакетов (направление master-game-registry) —
30
- // в проде это смонтированный том, переживающий пересоздание контейнера
31
- if (env.VIMP_GAMES_DIR) {
32
- config.set('master:gameStore:dir', env.VIMP_GAMES_DIR);
33
- }
34
-
35
- // общий секрет бокса и auth-сервиса для пересылки журнала клиентских
36
- // ошибок (plan/client-reports)
37
- if (env.VIMP_CLIENT_REPORTS_TOKEN) {
38
- config.set('master:clientReports:token', env.VIMP_CLIENT_REPORTS_TOKEN);
39
- }
40
- }
41
-
42
- // минимальная длина VIMP_ROOM_SECRET_KEY — ключ HMAC-SHA256
43
- const ROOM_SECRET_KEY_MIN_BYTES = 32;
44
-
45
- /**
46
- * Ключ секрета комнаты (host-migration, этап 2): roomSecret = HMAC(ключ,
47
- * roomId:epoch:hostUserId) — им хост доказывает комнату после рестарта
48
- * мастера. В production обязателен; в dev без него генерируется случайный на
49
- * время процесса (комнаты после рестарта dev-мастера не восстанавливаются).
50
- * @param {Object} [env] - Окружение (по умолчанию process.env).
51
- * @param {Object} opts
52
- * @param {boolean} opts.isProduction
53
- * @param {Function} opts.randomBytes - crypto.randomBytes.
54
- * @returns {{ key: string|Buffer, ephemeral: boolean }}
55
- */
56
- export function readRoomSecretKey(
57
- env = process.env,
58
- { isProduction, randomBytes },
59
- ) {
60
- const key = env.VIMP_ROOM_SECRET_KEY;
61
-
62
- if (key) {
63
- if (Buffer.byteLength(key) < ROOM_SECRET_KEY_MIN_BYTES) {
64
- throw new Error(
65
- `VIMP_ROOM_SECRET_KEY: at least ${ROOM_SECRET_KEY_MIN_BYTES} bytes required`,
66
- );
67
- }
68
-
69
- return { key, ephemeral: false };
70
- }
71
-
72
- if (isProduction) {
73
- throw new Error('VIMP_ROOM_SECRET_KEY must be set in production');
74
- }
75
-
76
- return { key: randomBytes(ROOM_SECRET_KEY_MIN_BYTES), ephemeral: true };
77
- }
78
-
79
- /**
80
- * Настройки комнаты dedicated-сервера: VIMP_DEDICATED_ROOM — JSON-объект
81
- * (map, maxPlayers, roundTime, mapTime, friendlyFire, seed). Мусор в
82
- * переменной не должен ронять процесс молча — отказ именованный.
83
- * @param {Object} [env] - Окружение (по умолчанию process.env).
84
- * @returns {Object} Переопределения комнаты.
85
- */
86
- export function readDedicatedRoom(env = process.env) {
87
- if (!env.VIMP_DEDICATED_ROOM) {
88
- return {};
89
- }
90
-
91
- let room;
92
-
93
- try {
94
- room = JSON.parse(env.VIMP_DEDICATED_ROOM);
95
- } catch (e) {
96
- throw new Error(`VIMP_DEDICATED_ROOM: invalid JSON — ${e.message}`);
97
- }
98
-
99
- if (!room || typeof room !== 'object' || Array.isArray(room)) {
100
- throw new Error('VIMP_DEDICATED_ROOM: expected a JSON object');
101
- }
102
-
103
- return room;
104
- }