vimp-engine 0.38.1 → 0.38.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.
- package/LICENSE +21 -0
- package/README.md +12 -6
- package/bin/vimp-sim.js +2 -2
- package/core/Cargo.toml +4 -3
- package/package.json +12 -5
- package/src/client/components/view/Auth.js +4 -2
- package/src/client/lib/formBuilder.js +9 -8
- package/src/client/main.js +11 -10
- package/src/client/network/WebSocketTransport.js +1 -1
- package/src/client/views/includes/lobby.pug +2 -1
- package/src/config/authClient.js +1 -2
- package/src/config/gameCodes.js +2 -2
- package/src/config/wsports.js +2 -2
- package/src/devtools/builtinScenario.js +3 -3
- package/src/devtools/contract/rules/a2-package-scripts.js +4 -3
- package/src/devtools/contract/rules/a4-vite-config.js +4 -3
- package/src/devtools/contract/rules/a7-package-repository.js +6 -5
- package/src/devtools/contract/rules/c4-component-dependencies.js +5 -5
- package/src/devtools/invariants.js +1 -1
- package/src/devtools/pluginLoader.js +3 -3
- package/src/host/PortMachine.js +6 -5
- package/src/lib/loadGamePackage.js +1 -1
- package/src/lib/packageLink.js +11 -10
- package/src/standalone/index.js +5 -5
- package/bin/vimp-surface.js +0 -88
- package/src/config/env.js +0 -104
- package/src/config/master.js +0 -288
- package/src/lib/clientIp.js +0 -96
- package/src/lib/rateLimiter.js +0 -40
- package/src/lib/security.js +0 -45
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lgick
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# vimp-engine
|
|
2
2
|
|
|
3
|
-
VIMP
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
The VIMP engine SDK for game plugins: the browser host and client runtime,
|
|
4
|
+
the standalone mode (`vimp-engine/standalone`) and the CLI tools `vimp-sim`
|
|
5
|
+
and `vimp-contract`. Game rules live in separate plugin packages that the
|
|
6
|
+
engine loads at runtime through the
|
|
7
|
+
[plugin contract](https://docs.vimp.lgick.dev/en/plugin-api).
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Start a game:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm create vimp-game my-game
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Documentation: https://docs.vimp.lgick.dev
|
package/bin/vimp-sim.js
CHANGED
|
@@ -40,8 +40,8 @@ const BUILTIN_NOTICE =
|
|
|
40
40
|
'notice: the built-in smoke scenario is running against --game. It only\n' +
|
|
41
41
|
'notice: proves the loop closes on this plugin: key coverage (invariant 2)\n' +
|
|
42
42
|
'notice: and prediction drift (invariant 9) are skipped — both need a\n' +
|
|
43
|
-
'notice: scenario written for your game. See § Scenario format
|
|
44
|
-
|
|
43
|
+
'notice: scenario written for your game. See § Scenario format at\n' +
|
|
44
|
+
'notice: https://docs.vimp.lgick.dev/en/debugging#scenario-format, then run\n' +
|
|
45
45
|
'notice: vimp-sim --scenario <file>.\n\n';
|
|
46
46
|
|
|
47
47
|
// типичная опечатка «--core без --game»: фикстурное ядро — обычный JS и
|
package/core/Cargo.toml
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "vimp-engine-core"
|
|
3
|
-
version = "0.23.
|
|
3
|
+
version = "0.23.3"
|
|
4
4
|
edition = "2024"
|
|
5
|
-
description = "VIMP
|
|
5
|
+
description = "VIMP engine simulation core: physics, map, snapshot framing, interpolation/prediction/raycast primitives, navigation utilities. No wasm-bindgen — the game crate builds the WASM ABI wrappers."
|
|
6
6
|
license = "MIT"
|
|
7
|
-
|
|
7
|
+
homepage = "https://docs.vimp.lgick.dev/en/core"
|
|
8
|
+
documentation = "https://docs.rs/vimp-engine-core"
|
|
8
9
|
readme = "README.md"
|
|
9
10
|
|
|
10
11
|
[lib]
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vimp-engine",
|
|
3
|
-
"version": "0.38.
|
|
4
|
-
"description": "VIMP
|
|
3
|
+
"version": "0.38.2",
|
|
4
|
+
"description": "VIMP engine SDK: browser host and client runtime, standalone mode and CLI tools (vimp-sim, vimp-contract) for VIMP game plugins",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "git+ssh://git@github.com/lgick/vimp.git"
|
|
8
|
+
"url": "git+ssh://git@github.com/lgick/vimp.git",
|
|
9
|
+
"directory": "packages/engine"
|
|
9
10
|
},
|
|
10
|
-
"homepage": "https://
|
|
11
|
+
"homepage": "https://docs.vimp.lgick.dev",
|
|
11
12
|
"type": "module",
|
|
12
13
|
"engines": {
|
|
13
14
|
"node": ">=20.11.0"
|
|
@@ -23,7 +24,12 @@
|
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"src/lib",
|
|
27
|
+
"!src/lib/clientIp.js",
|
|
28
|
+
"!src/lib/rateLimiter.js",
|
|
29
|
+
"!src/lib/security.js",
|
|
26
30
|
"src/config",
|
|
31
|
+
"!src/config/env.js",
|
|
32
|
+
"!src/config/master.js",
|
|
27
33
|
"src/host",
|
|
28
34
|
"src/client",
|
|
29
35
|
"!src/client/_*",
|
|
@@ -31,7 +37,8 @@
|
|
|
31
37
|
"src/devtools",
|
|
32
38
|
"tests/fixtures",
|
|
33
39
|
"core/Cargo.toml",
|
|
34
|
-
"bin"
|
|
40
|
+
"bin",
|
|
41
|
+
"!bin/vimp-surface.js"
|
|
35
42
|
],
|
|
36
43
|
"scripts": {
|
|
37
44
|
"predev": "node scripts/sync-pixi-vendor.mjs",
|
|
@@ -13,7 +13,8 @@ export default class AuthView {
|
|
|
13
13
|
// texts — игровые тексты формы (authSchema.texts: title, sections);
|
|
14
14
|
// сам каркас (auth.pug) нейтрален и текстов игры не содержит.
|
|
15
15
|
// params — та же дескрипторная схема формы, что и у room-формы
|
|
16
|
-
// (docs/en/plugin-api
|
|
16
|
+
// (docs.vimp.lgick.dev/en/plugin-api "Form schema"), едет по сети в
|
|
17
|
+
// PS_AUTH_DATA
|
|
17
18
|
constructor(model, elems, texts = null, params = null) {
|
|
18
19
|
if (authView) {
|
|
19
20
|
return authView;
|
|
@@ -63,7 +64,8 @@ export default class AuthView {
|
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// строит контролы формы игрока из той же дескрипторной схемы, что и
|
|
66
|
-
// room-форма (единый formBuilder, docs/en/plugin-api
|
|
67
|
+
// room-форма (единый formBuilder, docs.vimp.lgick.dev/en/plugin-api "Form
|
|
68
|
+
// schema")
|
|
67
69
|
_renderFields(elems, params) {
|
|
68
70
|
if (!Array.isArray(params)) {
|
|
69
71
|
return;
|
|
@@ -4,9 +4,9 @@ import { resolveDescriptor } from '../../lib/formControls.js';
|
|
|
4
4
|
import { toDisplay, toStored, isNumericField } from '../../lib/formUnit.js';
|
|
5
5
|
|
|
6
6
|
// Общий билдер полей форм (room-форма и auth-форма используют один
|
|
7
|
-
// контракт дескрипторов — docs/en/plugin-api
|
|
8
|
-
// control: 'select'|'text'|'checkbox'|'radio'; все контролы —
|
|
9
|
-
// элементы формы, без визуальной кастомизации.
|
|
7
|
+
// контракт дескрипторов — docs.vimp.lgick.dev/en/plugin-api, раздел "Form
|
|
8
|
+
// schema"). control: 'select'|'text'|'checkbox'|'radio'; все контролы —
|
|
9
|
+
// нативные элементы формы, без визуальной кастомизации.
|
|
10
10
|
//
|
|
11
11
|
// Имя контрола приезжает от игры и разрешается через append-only реестр
|
|
12
12
|
// (lib/formControls.js): имена, выведенные из эксплуатации в v3 ('range',
|
|
@@ -270,7 +270,8 @@ const builders = {
|
|
|
270
270
|
// control» на имени, которое реестр считает живым
|
|
271
271
|
export const FORM_CONTROLS = Object.keys(builders);
|
|
272
272
|
|
|
273
|
-
// строит одно поле формы по дескриптору (docs/en/plugin-api
|
|
273
|
+
// строит одно поле формы по дескриптору (docs.vimp.lgick.dev/en/plugin-api,
|
|
274
|
+
// "Form schema")
|
|
274
275
|
export function buildField(descriptor, ctx = {}) {
|
|
275
276
|
// алиас выведенного контрола ('range' → numeric 'text') разрешается
|
|
276
277
|
// здесь же, где строится поле, и теми же правилами, что в валидации
|
|
@@ -291,10 +292,10 @@ export function buildField(descriptor, ctx = {}) {
|
|
|
291
292
|
}
|
|
292
293
|
|
|
293
294
|
// засеивает default каждого дескриптора room-формы значением из roomDefaults
|
|
294
|
-
// (docs/en/plugin-api
|
|
295
|
-
// источником значений по умолчанию); явный descriptor.default,
|
|
296
|
-
// в схеме, побеждает. Вынесено из populateRoomForm (main.js),
|
|
297
|
-
// код был накрыт тестом, а не продублирован в нём
|
|
295
|
+
// (docs.vimp.lgick.dev/en/plugin-api "Form schema": roomDefaults остаётся
|
|
296
|
+
// единственным источником значений по умолчанию); явный descriptor.default,
|
|
297
|
+
// если он есть в схеме, побеждает. Вынесено из populateRoomForm (main.js),
|
|
298
|
+
// чтобы этот же код был накрыт тестом, а не продублирован в нём
|
|
298
299
|
export function mergeRoomDefaults(descriptors, roomDefaults) {
|
|
299
300
|
return descriptors.map(descriptor => ({
|
|
300
301
|
default: roomDefaults[descriptor.name],
|
package/src/client/main.js
CHANGED
|
@@ -934,7 +934,8 @@ socketMethods[PS_GAME_INFORM_DATA] = data => {
|
|
|
934
934
|
};
|
|
935
935
|
|
|
936
936
|
// проигрывает shimmer-волну по логотипу в начале раунда; логотип живёт вне
|
|
937
|
-
// PanelView.containerId, поэтому панель им не управляет
|
|
937
|
+
// PanelView.containerId, поэтому панель им не управляет
|
|
938
|
+
// (см. docs.vimp.lgick.dev/en/client-api)
|
|
938
939
|
const playLogoRoundStart = () => {
|
|
939
940
|
const logo = document.getElementById('logo');
|
|
940
941
|
|
|
@@ -2182,8 +2183,8 @@ let roomFormFields = new Map();
|
|
|
2182
2183
|
let roomFormDescriptors = [];
|
|
2183
2184
|
|
|
2184
2185
|
// генерирует форму создания комнаты по явной схеме манифеста (roomForm,
|
|
2185
|
-
// docs/en/plugin-api
|
|
2186
|
-
// значения, схема плагина полностью описывает форму
|
|
2186
|
+
// docs.vimp.lgick.dev/en/plugin-api "Form schema") — движок не выводит контролы
|
|
2187
|
+
// из типа значения, схема плагина полностью описывает форму
|
|
2187
2188
|
function populateRoomForm(manifest) {
|
|
2188
2189
|
const container = document.getElementById(lobbyConfig.elems.fieldsId);
|
|
2189
2190
|
|
|
@@ -2569,13 +2570,13 @@ function initLobby() {
|
|
|
2569
2570
|
hostBtn?.addEventListener('click', async () => {
|
|
2570
2571
|
// валидация (pattern/required/min/max) — единственная граница
|
|
2571
2572
|
// room-формы: она едет клиенту как JSON манифеста, JS-валидаторы
|
|
2572
|
-
// (как в auth-форме) туда не сериализуются
|
|
2573
|
-
// "Form schema"); авторитетный клампинг
|
|
2574
|
-
// (вызывается из host.worker.js при
|
|
2575
|
-
// активации игры: неверная форма не должна
|
|
2576
|
-
// arm() и рисует ошибки, и снимает фильтр «только
|
|
2577
|
-
// это ответ за форму целиком, включая поля, которых
|
|
2578
|
-
// Он же чистит блок, когда ошибок нет
|
|
2573
|
+
// (как в auth-форме) туда не сериализуются
|
|
2574
|
+
// (docs.vimp.lgick.dev/en/plugin-api "Form schema"); авторитетный клампинг
|
|
2575
|
+
// всё равно в applyRoomOverrides.js (вызывается из host.worker.js при
|
|
2576
|
+
// создании комнаты). Проверяем до активации игры: неверная форма не должна
|
|
2577
|
+
// стоить загрузки плагина. arm() и рисует ошибки, и снимает фильтр «только
|
|
2578
|
+
// тронутые поля»: клик — это ответ за форму целиком, включая поля, которых
|
|
2579
|
+
// игрок не касался. Он же чистит блок, когда ошибок нет
|
|
2579
2580
|
if (liveErrors.arm().length) {
|
|
2580
2581
|
return;
|
|
2581
2582
|
}
|
|
@@ -5,7 +5,7 @@ import Publisher from '../../lib/Publisher.js';
|
|
|
5
5
|
// publisher с 'message'/'close', connect/send/close, — поэтому клиентский код
|
|
6
6
|
// про него не знает.
|
|
7
7
|
//
|
|
8
|
-
// Отличия от WebRTC, которые надо держать в
|
|
8
|
+
// Отличия от WebRTC, которые надо держать в голове:
|
|
9
9
|
// разделения meta/state тут нет, флаг reliable игнорируется. Значит PING/PONG
|
|
10
10
|
// идут надёжным каналом (замер RTT становится замером TCP-пути), а бэкпрешер
|
|
11
11
|
// вместо дропа позиционных кадров живёт на серверной стороне.
|
|
@@ -32,7 +32,8 @@ div#lobby(style='display:none')
|
|
|
32
32
|
label.form-label(for='lobby-game')= 'Select game'
|
|
33
33
|
select#lobby-game.field-select
|
|
34
34
|
//- поля настроек комнаты генерируются по явной схеме roomForm
|
|
35
|
-
//- манифеста активной игры (docs/en/plugin-api
|
|
35
|
+
//- манифеста активной игры (docs.vimp.lgick.dev/en/plugin-api "Form
|
|
36
|
+
//- schema")
|
|
36
37
|
div#lobby-fields
|
|
37
38
|
//- ошибки формы (formBuilder.renderFormErrors) и отказ загрузки
|
|
38
39
|
//- плагина выбранной игры — строкой в форме, а не в #tech-informer:
|
package/src/config/authClient.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Конфиг клиента профиля лобби. Бандлится в клиентскую сборку, как
|
|
2
2
|
// config/lobby.js — auth-сервис живёт на отдельном домене/порте, поэтому
|
|
3
|
-
// serviceUrl подставляется под конкретный деплой при
|
|
4
|
-
// docs/en/auth.md).
|
|
3
|
+
// serviceUrl подставляется под конкретный деплой при сборке.
|
|
5
4
|
export default {
|
|
6
5
|
// базовый URL центрального auth-сервиса (packages/auth). Дефолт — dev-порт
|
|
7
6
|
// из packages/auth/src/config/auth.js; в проде подставляется Vite'ом из
|
package/src/config/gameCodes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// коды игровых информ-сообщений (порт GAME_INFORM_DATA, см.
|
|
2
|
-
// docs/en/
|
|
3
|
-
// (main.js): значение не должно расходиться между ними молча
|
|
2
|
+
// docs.vimp.lgick.dev/en/snapshot) — общий контракт хоста (SocketManager) и
|
|
3
|
+
// клиента (main.js): значение не должно расходиться между ними молча
|
|
4
4
|
export default {
|
|
5
5
|
winnerTeam: [0],
|
|
6
6
|
roundStart: [1],
|
package/src/config/wsports.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Номера портов JSON-канала (docs/en/
|
|
2
|
-
// остальные словари плагинной поверхности (этап 3 плана
|
|
1
|
+
// Номера портов JSON-канала (docs.vimp.lgick.dev/en/snapshot). Реестр
|
|
2
|
+
// append-only, как и остальные словари плагинной поверхности (этап 3 плана
|
|
3
3
|
// plugin-forward-compat): номер НИКОГДА не переиспользуется и не
|
|
4
4
|
// перенумеровывается — уже опубликованная сборка игры шлёт и слушает те
|
|
5
5
|
// номера, что видела при сборке, и её dist больше никто не тронет.
|
|
@@ -13,8 +13,8 @@ import { isFixture } from './pluginLoader.js';
|
|
|
13
13
|
// Ввод начинается на 40-м тике, а не сразу: кадр спавна с force_reset
|
|
14
14
|
// приходит примерно на interpolation.delay (движковый дефолт — 100 мс, тут
|
|
15
15
|
// ~3× запаса) позже входа и чистит удержанные клавиши предиктора
|
|
16
|
-
// (docs/en/debugging
|
|
17
|
-
// ровно ту ловушку, от которой предостерегает документация
|
|
16
|
+
// (docs.vimp.lgick.dev/en/debugging) — встроенный сценарий не должен
|
|
17
|
+
// демонстрировать ровно ту ловушку, от которой предостерегает документация
|
|
18
18
|
const INPUT_DOWN_TICK = 40;
|
|
19
19
|
const INPUT_UP_TICK = 100;
|
|
20
20
|
const TICKS = 120;
|
|
@@ -26,7 +26,7 @@ const FIXTURE_UNUSED_KEYS = ['e1'];
|
|
|
26
26
|
const firstKey = dict => Object.keys(dict ?? {})[0];
|
|
27
27
|
|
|
28
28
|
// Клавиша для смоука обязана быть удерживаемой: по конвенции playerKeys
|
|
29
|
-
// (docs/en/plugin-api
|
|
29
|
+
// (docs.vimp.lgick.dev/en/plugin-api) `type: 1` — триггер, у которого 'up'
|
|
30
30
|
// игнорируется, и «нажал — отпустил» не даёт ни удержания, ни движения.
|
|
31
31
|
// Порядок объявления в playerKeys контрактом не задан, поэтому берём первую
|
|
32
32
|
// без type; если игра объявила одни триггеры — первую любую, смоук всё равно
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ERROR, skip, verdict } from '../result.js';
|
|
2
2
|
|
|
3
|
-
// Набор npm-скриптов пакета игры (docs/en/packaging
|
|
4
|
-
// требование: инструкции движка и CI сгенерированной игры зовут их по
|
|
5
|
-
// а отсутствующий core:build:node тихо оставляет headless-прогон без
|
|
3
|
+
// Набор npm-скриптов пакета игры (docs.vimp.lgick.dev/en/packaging). Это не
|
|
4
|
+
// вкусовое требование: инструкции движка и CI сгенерированной игры зовут их по
|
|
5
|
+
// имени, а отсутствующий core:build:node тихо оставляет headless-прогон без
|
|
6
|
+
// ядра.
|
|
6
7
|
const REQUIRED = [
|
|
7
8
|
'build',
|
|
8
9
|
'build:client',
|
|
@@ -2,9 +2,10 @@ import { ERROR, skip, verdict } from '../result.js';
|
|
|
2
2
|
|
|
3
3
|
// vite.config.js проверяется как текст, а не импортом: конфиг —
|
|
4
4
|
// функция от mode и вне Vite не исполняется. Каждый пункт здесь ловит
|
|
5
|
-
// молчаливую поломку сборки, а не стилистику
|
|
6
|
-
// без preserveEntrySignatures
|
|
7
|
-
// tree-shaking, а build.lib всегда инлайнит
|
|
5
|
+
// молчаливую поломку сборки, а не стилистику
|
|
6
|
+
// (docs.vimp.lgick.dev/en/packaging): без preserveEntrySignatures
|
|
7
|
+
// default-экспорт плагина вытряхивает tree-shaking, а build.lib всегда инлайнит
|
|
8
|
+
// ассеты.
|
|
8
9
|
const REQUIRED = [
|
|
9
10
|
[/emptyOutDir\s*:\s*false/, 'emptyOutDir: false (two builds share dist/)'],
|
|
10
11
|
[/assetsInlineLimit\s*:\s*0/, 'assetsInlineLimit: 0'],
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { WARN, skip, verdict } from '../result.js';
|
|
2
2
|
import { resolveProjectUrl } from '../../../lib/packageLink.js';
|
|
3
3
|
|
|
4
|
-
// Игра без объявленного
|
|
5
|
-
// Но движок показывает ссылку на проект в футере формы входа, и берёт
|
|
6
|
-
// только
|
|
7
|
-
// футера у игроков останется пустой, а заметить это без подсказки негде.
|
|
4
|
+
// Игра без объявленного homepage/repository собирается и играется — это не
|
|
5
|
+
// отказ. Но движок показывает ссылку на проект в футере формы входа, и берёт
|
|
6
|
+
// он её только из этих полей (фолбэка нет, см. lib/packageLink.js): без них
|
|
7
|
+
// ячейка футера у игроков останется пустой, а заметить это без подсказки негде.
|
|
8
8
|
// Отсюда warn — под --strict он становится ошибкой.
|
|
9
9
|
export default {
|
|
10
10
|
id: 'A7',
|
|
11
11
|
name: 'packageRepository',
|
|
12
12
|
level: WARN,
|
|
13
|
-
title:
|
|
13
|
+
title:
|
|
14
|
+
'package.json declares homepage or repository (the #auth footer links to it)',
|
|
14
15
|
|
|
15
16
|
check(ctx) {
|
|
16
17
|
if (!ctx.pkg) {
|
|
@@ -4,11 +4,11 @@ import { SERVICES } from '../../../config/clientServices.js';
|
|
|
4
4
|
// Движковый пул сервисов держит реестр (config/clientServices.js), но пул
|
|
5
5
|
// ими не исчерпывается: игра доливает туда свои через
|
|
6
6
|
// ClientPlugin.hooks.services(core) — например геометрию предсказанной
|
|
7
|
-
// динамики карты (docs/en/plugin-api
|
|
8
|
-
// ошибка для движка: part получает undefined и рисует пустоту
|
|
9
|
-
// assetsBase выглядит как чистый холст без единой строки в консоли,
|
|
10
|
-
// этого правило и существует. Правило остаётся ERROR: оно работает на
|
|
11
|
-
// разработки игры, а не в рантайме.
|
|
7
|
+
// динамики карты (docs.vimp.lgick.dev/en/plugin-api «hooks.services»).
|
|
8
|
+
// Незнакомое имя не ошибка для движка: part получает undefined и рисует пустоту
|
|
9
|
+
// — карта без assetsBase выглядит как чистый холст без единой строки в консоли,
|
|
10
|
+
// ради этого правило и существует. Правило остаётся ERROR: оно работает на
|
|
11
|
+
// этапе разработки игры, а не в рантайме.
|
|
12
12
|
//
|
|
13
13
|
// Что именно возвращает `hooks.services(core)`, статически не видно — хук
|
|
14
14
|
// требует живого ядра. Поэтому у плагина есть необязательное поле
|
|
@@ -484,7 +484,7 @@ function predictionDrift({ clients, scenario }) {
|
|
|
484
484
|
// Компоненты player-блока — игровая раскладка, движок знает только их
|
|
485
485
|
// порядок, поэтому нарушение адресуется индексом. Исключение — уровень 0
|
|
486
486
|
// (source 'camera'): там движок сравнивает ровно мировые x/y, и называть их
|
|
487
|
-
// «#0/#1» значит прятать причину (см. docs/en/debugging
|
|
487
|
+
// «#0/#1» значит прятать причину (см. docs.vimp.lgick.dev/en/debugging).
|
|
488
488
|
const CAMERA_COMPONENTS = ['x', 'y'];
|
|
489
489
|
|
|
490
490
|
function formatDivergence(record) {
|
|
@@ -12,9 +12,9 @@ import { loadGamePackage } from '../lib/loadGamePackage.js';
|
|
|
12
12
|
// 3. ничего — фикстура miniGame из этого репозитория.
|
|
13
13
|
//
|
|
14
14
|
// entries.wasmNode — новое ОПЦИОНАЛЬНОЕ поле манифеста: конвенция
|
|
15
|
-
// core/pkg-node/ уже описана в docs/en/packaging
|
|
16
|
-
// пути в манифесте. Его отсутствие не ошибка — просто игра не
|
|
17
|
-
// headless своим настоящим ядром.
|
|
15
|
+
// core/pkg-node/ уже описана в docs.vimp.lgick.dev/en/packaging, не хватало
|
|
16
|
+
// только пути в манифесте. Его отсутствие не ошибка — просто игра не
|
|
17
|
+
// прогоняется headless своим настоящим ядром.
|
|
18
18
|
|
|
19
19
|
const FIXTURE_DIR = new URL('../../tests/fixtures/miniGame/', import.meta.url);
|
|
20
20
|
|
package/src/host/PortMachine.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Хендшейк-машина клиента, вынутая из host.worker.js: автомат по клиентским
|
|
2
|
-
// портам
|
|
3
|
-
// ни DOM: всё, что знает про транспорт, приходит через
|
|
4
|
-
// крутит и Worker браузерного хоста, и inline-хост
|
|
5
|
-
// Node-процесс dedicated-сервера: копий автомата быть не
|
|
6
|
-
// разъедутся ровно так, как разъезжались бы копии
|
|
2
|
+
// портам (таблица портов — docs.vimp.lgick.dev/en/snapshot). Изоморфна — ни
|
|
3
|
+
// self, ни postMessage, ни DOM: всё, что знает про транспорт, приходит через
|
|
4
|
+
// makeSocket. Поэтому её крутит и Worker браузерного хоста, и inline-хост
|
|
5
|
+
// standalone SDK, и Node-процесс dedicated-сервера: копий автомата быть не
|
|
6
|
+
// должно, иначе они разъедутся ровно так, как разъезжались бы копии
|
|
7
|
+
// createHostRuntime.
|
|
7
8
|
|
|
8
9
|
import wsports from '../config/wsports.js';
|
|
9
10
|
import closeCodes from '../config/closeCodes.js';
|
|
@@ -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
|
|
15
|
+
// docs.vimp.lgick.dev/en/packaging). Для боевой игры она обязательна: без ядра
|
|
16
16
|
// авторитетную симуляцию крутить нечем.
|
|
17
17
|
|
|
18
18
|
/**
|
package/src/lib/packageLink.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Ссылка на проект для футеров лобби и формы входа: движок показывает рядом
|
|
2
|
-
// npm-версию, и клик по соседней ячейке ведёт
|
|
2
|
+
// npm-версию, и клик по соседней ячейке ведёт на страницу проекта этого пакета.
|
|
3
3
|
//
|
|
4
|
-
// Источник — `
|
|
4
|
+
// Источник — `homepage` его package.json (иначе `repository`). Фолбэка нет:
|
|
5
5
|
// пакет, не объявивший ни того, ни другого, ссылки не получает, и ячейка
|
|
6
6
|
// футера остаётся пустой. Это сознательно — прежний фолбэк на страницу пакета
|
|
7
7
|
// в npm давал разнобой («GitHub» у одной игры, «npm» у другой) и молча
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
// (client/lib/engineVersion.js).
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Адрес проекта из package.json, приведённый к https:
|
|
20
|
-
*
|
|
19
|
+
* Адрес проекта из package.json, приведённый к https: homepage, иначе
|
|
20
|
+
* repository.
|
|
21
21
|
* @param {Object} pkg - разобранный package.json
|
|
22
22
|
* @returns {string|null} https-URL либо null, если поля нет или это не адрес
|
|
23
23
|
*/
|
|
@@ -32,10 +32,10 @@ export function resolveProjectUrl(pkg) {
|
|
|
32
32
|
const declared =
|
|
33
33
|
typeof repository === 'string' ? repository : repository?.url;
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
return
|
|
35
|
+
// homepage важнее repository: поле repository обязано называть
|
|
36
|
+
// репозиторий публикации (его сверяет npm Trusted Publishing), и у пакета
|
|
37
|
+
// из закрытого репозитория такая ссылка отдаёт игроку 404
|
|
38
|
+
return normalize(homepage) ?? normalizeRepository(declared);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
/**
|
|
@@ -94,8 +94,9 @@ function normalize(raw) {
|
|
|
94
94
|
|
|
95
95
|
url = url.replace(/^git\+/, '');
|
|
96
96
|
|
|
97
|
-
// scp-форма git@host:user/repo
|
|
98
|
-
|
|
97
|
+
// scp-форма git@host:user/repo (в том числе с лишним ssh://, как её
|
|
98
|
+
// нередко пишет npm) — двоеточие здесь разделитель, не порт
|
|
99
|
+
const scp = /^(?:ssh:\/\/)?git@([^:/]+):(.+)$/.exec(url);
|
|
99
100
|
|
|
100
101
|
if (scp) {
|
|
101
102
|
url = `https://${scp[1]}/${scp[2]}`;
|
package/src/standalone/index.js
CHANGED
|
@@ -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/
|
|
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
|
|
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' —
|
package/bin/vimp-surface.js
DELETED
|
@@ -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
|
-
}
|
package/src/config/master.js
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import { fileURLToPath } from 'url';
|
|
3
|
-
|
|
4
|
-
// корень репозитория — якорь от расположения файла, не от cwd
|
|
5
|
-
const rootDir = path.resolve(
|
|
6
|
-
fileURLToPath(import.meta.url),
|
|
7
|
-
'..',
|
|
8
|
-
'..',
|
|
9
|
-
'..',
|
|
10
|
-
'..',
|
|
11
|
-
'..',
|
|
12
|
-
);
|
|
13
|
-
|
|
14
|
-
export default {
|
|
15
|
-
name: 'VIMP Master Server',
|
|
16
|
-
protocol: 'https:',
|
|
17
|
-
domain: 'localhost',
|
|
18
|
-
// 3000 — игровой сервер, 3001 — Vite HMR (vite.config.js)
|
|
19
|
-
port: 3002,
|
|
20
|
-
|
|
21
|
-
// сертификаты для локальной разработки — в .certs корня репозитория
|
|
22
|
-
// (в продакшене обычный HTTP за Nginx)
|
|
23
|
-
httpsOptions: {
|
|
24
|
-
key: path.join(rootDir, '.certs', 'key.pem'),
|
|
25
|
-
cert: path.join(rootDir, '.certs', 'cert.pem'),
|
|
26
|
-
},
|
|
27
|
-
|
|
28
|
-
// Статический каталог игр-плагинов. ПУСТ по умолчанию
|
|
29
|
-
// (master-game-registry, этап 3): штатный источник каталога — реестр игр
|
|
30
|
-
// auth-сервиса, откуда мастер скачивает одобренные пакеты сам
|
|
31
|
-
// (master/GameSync.js). Движок игр не знает и зависимостями их не тянет.
|
|
32
|
-
//
|
|
33
|
-
// Массив остаётся двум потребителям, и env-переопределения у него нет:
|
|
34
|
-
// локальной разработке (каталог наполняют собранные пакеты @vimp-games/*
|
|
35
|
-
// из node_modules, master/localGames.js — прилинкованная игра важнее
|
|
36
|
-
// реестра) и dedicated-серверу, который берёт отсюда игру, названную
|
|
37
|
-
// VIMP_DEDICATED_GAME. `package` — имя npm-пакета игры, резолвится через
|
|
38
|
-
// node_modules; версию задаёт сама установленная зависимость.
|
|
39
|
-
// maxGameScore (snakes-v3 этап 3.3, необязательное поле рядом с
|
|
40
|
-
// id/package) — потолок результата ОДНОЙ игры для этой игры; мастер
|
|
41
|
-
// клампит им `best`/`points` PUT /auth/rank. Для игр реестра его задаёт
|
|
42
|
-
// админ при модерации, здесь — только запасной путь. Не задан — дефолт
|
|
43
|
-
// master:playerData:maxGameScore
|
|
44
|
-
games: [],
|
|
45
|
-
|
|
46
|
-
// Хранилище игровых пакетов (направление master-game-registry): мастер
|
|
47
|
-
// качает одобренные игры из npm registry и раздаёт их с диска, вместо того
|
|
48
|
-
// чтобы получать их npm-зависимостью на этапе сборки образа.
|
|
49
|
-
gameStore: {
|
|
50
|
-
// корень хранилища; null → <repoRoot>/.games (локальная разработка).
|
|
51
|
-
// В проде задаётся VIMP_GAMES_DIR и монтируется томом
|
|
52
|
-
dir: null,
|
|
53
|
-
registryUrl: 'https://registry.npmjs.org',
|
|
54
|
-
// период опроса реестра auth за изменениями каталога
|
|
55
|
-
refreshInterval: 60000,
|
|
56
|
-
// потолки распаковки недоверенного архива
|
|
57
|
-
maxTarballBytes: 64 * 1024 * 1024,
|
|
58
|
-
maxFiles: 5000,
|
|
59
|
-
// сколько версий одной игры держать на диске: активная + стейджевая
|
|
60
|
-
keepVersions: 2,
|
|
61
|
-
// потолок ожидания ответа реестра
|
|
62
|
-
timeout: 30000,
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
// список серверов (GET /servers)
|
|
66
|
-
servers: {
|
|
67
|
-
// если всего комнат <= порога — региональный фильтр
|
|
68
|
-
// и пагинация отключаются, отдаётся весь список
|
|
69
|
-
regionThreshold: 15,
|
|
70
|
-
defaultLimit: 10, // размер страницы по умолчанию
|
|
71
|
-
maxLimit: 50, // максимальный размер страницы
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
// ограничения регистрируемых комнат
|
|
75
|
-
host: {
|
|
76
|
-
// санитарная рамка вместимости комнаты для случая, когда игра комнаты
|
|
77
|
-
// неизвестна мастеру (gameId: null у старых хостов или id не из
|
|
78
|
-
// каталога). Потолок известной игры задаёт её манифест
|
|
79
|
-
// (roomDefaults.maxPlayers) — движок его не ограничивает
|
|
80
|
-
maxPlayersLimit: 8,
|
|
81
|
-
heartbeatTimeout: 30000, // нет heartbeat дольше — хост потерян
|
|
82
|
-
sweepInterval: 10000, // период уборки реестра комнат
|
|
83
|
-
},
|
|
84
|
-
|
|
85
|
-
// жизнь комнаты (host-migration, этап 2): комната живёт, пока в ней есть
|
|
86
|
-
// участник
|
|
87
|
-
room: {
|
|
88
|
-
// отсоединённый участник ещё считается в комнате (реконнект сигналинга)
|
|
89
|
-
memberGraceMs: 15000,
|
|
90
|
-
// обрыв WS хоста запускает миграцию не сразу, а через столько, если хост
|
|
91
|
-
// не вернулся reclaim_host'ом и ни один гость не сообщил host_unreachable
|
|
92
|
-
// (host_leaving мигрирует сразу); reclaim_host после начала миграции, до
|
|
93
|
-
// регистрации преемника, её отменяет (этап 7.0)
|
|
94
|
-
hostDisconnectGraceMs: 2000,
|
|
95
|
-
// повышать некого — комната ждёт reclaim_host столько, потом уборка
|
|
96
|
-
// мигрирует или закрывает её
|
|
97
|
-
hostReclaimGraceMs: 10000,
|
|
98
|
-
// потолок строки карточки комнаты в лобби (gameConfig.lobbyInfo игры)
|
|
99
|
-
maxInfoLength: 48,
|
|
100
|
-
// RTT участников (host-migration этап 6): ws.ping() каждой сессии с
|
|
101
|
-
// этим периодом; EMA RTT и джиттера — единая линейка связности для
|
|
102
|
-
// хоста и кандидатов
|
|
103
|
-
rttProbeIntervalMs: 5000,
|
|
104
|
-
// сессия без pong дольше — terminate(): «тихо умерший» хост находится
|
|
105
|
-
// быстрее, чем по heartbeatTimeout
|
|
106
|
-
wsDeadAfterMs: 12000,
|
|
107
|
-
// преемник (бета): кандидат должен пробыть в комнате не меньше этого
|
|
108
|
-
minMemberAgeMs: 10000,
|
|
109
|
-
// плановый пересчёт преемника (помимо join/leave/member_update)
|
|
110
|
-
successorReviewMs: 15000,
|
|
111
|
-
// гистерезис смены беты: лучший кандидат держит score не хуже
|
|
112
|
-
// successorSwitchRatio × score текущего дольше successorSwitchSustainMs
|
|
113
|
-
successorSwitchSustainMs: 30000,
|
|
114
|
-
successorSwitchRatio: 0.65,
|
|
115
|
-
// аварийная миграция (этап 7): бета — преемник с точкой, если она
|
|
116
|
-
// получила её не раньше checkpointMaxAgeMs назад (по standby_status.ageMs;
|
|
117
|
-
// статус идёт раз в 5 с — окно больше двух периодов), иначе cold-промоушен любого
|
|
118
|
-
// способного; преемник не занял комнату за promotionTimeoutMs
|
|
119
|
-
// (coldPromotionTimeoutMs — он перезагружает страницу) — следующий
|
|
120
|
-
checkpointMaxAgeMs: 12000,
|
|
121
|
-
promotionTimeoutMs: 10000,
|
|
122
|
-
coldPromotionTimeoutMs: 25000,
|
|
123
|
-
// отчёты host_unreachable: хост не ответил на probe за probeTimeoutMs —
|
|
124
|
-
// потерян; ответил, но за reportWindowMs отчиталась половина гостей —
|
|
125
|
-
// принудительная миграция, не чаще forcedMigrationCooldownMs на комнату.
|
|
126
|
-
// Гости и отчёты считаются по аккаунтам; кворум — не меньше
|
|
127
|
-
// minUnreachableReporters разных пользователей (в паре с плохим P2P один
|
|
128
|
-
// гость не снимает хоста — иначе роль «пинг-понгом» ходит по кругу)
|
|
129
|
-
probeTimeoutMs: 2000,
|
|
130
|
-
reportWindowMs: 5000,
|
|
131
|
-
forcedMigrationCooldownMs: 30000,
|
|
132
|
-
minUnreachableReporters: 2,
|
|
133
|
-
// плановая передача (этап 8): преемник не занял комнату за
|
|
134
|
-
// handoffTimeoutMs — передача отменяется, хост размораживает матч
|
|
135
|
-
handoffTimeoutMs: 8000,
|
|
136
|
-
// host_migrating.waitMs — дедлайн текущей попытки промоушена (передачи)
|
|
137
|
-
// плюс migrationNoticeMarginMs: столько гость ждёт host_changed
|
|
138
|
-
migrationNoticeMarginMs: 5000,
|
|
139
|
-
// автотриггеры (этап 9c). Сетевой лаг хоста: медиана его RTT до гостей
|
|
140
|
-
// (host_health) выше lagRttThresholdMs непрерывно lagSustainMs, а score
|
|
141
|
-
// беты лучше score хоста хотя бы на lagImprovementRatio — хосту
|
|
142
|
-
// request_handoff. Не раньше autoMigrationCooldownMs с прошлой
|
|
143
|
-
// авто-смены хоста в комнате (overload/hidden/network — кулдаун общий)
|
|
144
|
-
// и с получения роли текущим хостом
|
|
145
|
-
lagRttThresholdMs: 250,
|
|
146
|
-
lagSustainMs: 10000,
|
|
147
|
-
lagImprovementRatio: 0.35,
|
|
148
|
-
autoMigrationCooldownMs: 90000,
|
|
149
|
-
// бета — не ниже этого FPS рендера (caps.fps гостя; неизвестный FPS не
|
|
150
|
-
// отсеивает). Аварийный промоушен порог не применяет
|
|
151
|
-
minSuccessorFps: 30,
|
|
152
|
-
// голосование «Change host» (этап 10, /changehost; только лобби-режим):
|
|
153
|
-
// голоса считает мастер. Длительность голосования; повторный старт в
|
|
154
|
-
// комнате и тем же пользователем — не раньше кулдаунов; прошедшее
|
|
155
|
-
// голосование просит хоста отдать роль, не начал за voteForceAfterMs —
|
|
156
|
-
// принудительная миграция; снятый хост не бета и не хост комнаты
|
|
157
|
-
// demotedCooldownMs (кроме случая, когда больше принять некому).
|
|
158
|
-
// Один голос на аккаунт; голосует и начинает участник, пробывший в
|
|
159
|
-
// комнате не меньше minVoterAgeMs (свежие вкладки не накручивают голоса).
|
|
160
|
-
// После смены хоста голосование ждёт первого room_peers нового хоста —
|
|
161
|
-
// до него подтверждёнными считаются все, и фантомы попали бы в число
|
|
162
|
-
// голосующих, — но не дольше peersReportGraceMs (больше
|
|
163
|
-
// lobby.migration.peersReportIntervalMs: потерянный первый отчёт
|
|
164
|
-
// повторится внутри окна; хост старше room_peers отчёта не пришлёт)
|
|
165
|
-
vote: {
|
|
166
|
-
hostVoteDurationMs: 15000,
|
|
167
|
-
roomVoteCooldownMs: 120000,
|
|
168
|
-
userStartCooldownMs: 60000,
|
|
169
|
-
voteForceAfterMs: 5000,
|
|
170
|
-
demotedCooldownMs: 600000,
|
|
171
|
-
minVoterAgeMs: 30000,
|
|
172
|
-
peersReportGraceMs: 20000,
|
|
173
|
-
},
|
|
174
|
-
// GET /rooms/:roomId (прямая ссылка, этап 3) на IP: открытие ссылки —
|
|
175
|
-
// один запрос, лимит только против перебора roomId
|
|
176
|
-
lookupRateLimit: {
|
|
177
|
-
limit: 20,
|
|
178
|
-
windowMs: 1000,
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
// GET /auth/leaderboard (code review L2): TTL кэша на мастере (мс) и
|
|
183
|
-
// верхняя граница ?limit= — публичный анонимный эндпоинт, самый частый
|
|
184
|
-
// запрос лобби, выборка меняется медленно
|
|
185
|
-
leaderboard: {
|
|
186
|
-
cacheTtl: 15000,
|
|
187
|
-
maxLimit: 100,
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
// GET /auth/placement + агрегирующий GET /auth/placements (snakes-v3
|
|
191
|
-
// этап 3.3): место меняется медленно, а каждый вход участника стоит трёх
|
|
192
|
-
// срезов. Кэш здесь про round-trip до auth; стоимость самого запроса
|
|
193
|
-
// снята на стороне auth (RankDistribution)
|
|
194
|
-
placement: {
|
|
195
|
-
cacheTtl: 30000,
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
// пределы записи профилей в БД (snakes-v3 этап 3, решение пользователя 9):
|
|
199
|
-
// «игр сотни, серверов сотни» — минимальный интервал держит движок на
|
|
200
|
-
// стороне хоста, а мастер держит потолок для сломанного или злонамеренного
|
|
201
|
-
// сервера, который этот интервал обошёл
|
|
202
|
-
playerData: {
|
|
203
|
-
// PUT /auth/rank + /auth/state на комнату (проверенный roomId) в минуту.
|
|
204
|
-
// Честная комната на 32 при lobbyConfig.playerData.minFlushInterval в
|
|
205
|
-
// 5 минут пишет 64 запроса за эти 5 минут, то есть ~13/мин; остальное —
|
|
206
|
-
// запас на срочные границы (уход участника обходит интервал), и его
|
|
207
|
-
// хватает даже комнате, полностью сменившей состав дважды за минуту.
|
|
208
|
-
// Потолок держит не честную комнату, а сломанную или злонамеренную,
|
|
209
|
-
// поэтому запас считается от честной, а не «пусть будет побольше»
|
|
210
|
-
writesPerMinute: 120,
|
|
211
|
-
// потолок результата ОДНОЙ игры, если игра не объявила свой
|
|
212
|
-
// (master:games[].maxGameScore): обоснование — plan/snakes-v3/stage_2.md
|
|
213
|
-
maxGameScore: 10000,
|
|
214
|
-
},
|
|
215
|
-
|
|
216
|
-
// заголовки безопасности (гигиена среды, Этап 5.4). CSP на статику/.wasm в
|
|
217
|
-
// проде ставит Nginx (см. docs/deployment.md) — здесь single source of truth
|
|
218
|
-
// политики; мастер применяет её к своим ответам только в проде (в dev CSP
|
|
219
|
-
// сломала бы Vite HMR). WASM требует 'wasm-unsafe-eval', Worker — 'blob:';
|
|
220
|
-
// connect-src data: — PixiJS фетчит тестовый data:-URL для проверки ImageBitmap.
|
|
221
|
-
// authServiceUrl — домен central auth-сервиса (packages/auth): лобби
|
|
222
|
-
// делает туда прямой fetch (браузерные ручки auth-сервиса), поэтому
|
|
223
|
-
// connect-src должен его разрешать; сам OAuth-редирект (location.href на
|
|
224
|
-
// auth-сервис/провайдера) CSP не ограничивает — это навигация верхнего
|
|
225
|
-
// уровня, не fetch/XHR. challenges.cloudflare.com в script-src и frame-src
|
|
226
|
-
// — скрипт и iframe виджета Turnstile на экране регистрации.
|
|
227
|
-
// script-src несёт sha256-хэш инлайнового importmap из index.html (для
|
|
228
|
-
// pixi.js — src="..." на <script type="importmap"> браузеры не
|
|
229
|
-
// поддерживают, инлайн обязателен). Хэш посчитан по факту собранного
|
|
230
|
-
// packages/engine/dist/index.html скриптом
|
|
231
|
-
// scripts/check-importmap-csp-hash.mjs (запускается postbuild) — vite
|
|
232
|
-
// build минифицирует HTML и может изменить байты скрипта, поэтому хэш
|
|
233
|
-
// нельзя брать из исходника/консоли браузера; при правке importmap или
|
|
234
|
-
// апгрейде Vite смотреть на вывод postbuild-проверки.
|
|
235
|
-
security: {
|
|
236
|
-
authServiceUrl: 'http://localhost:3010',
|
|
237
|
-
csp: authServiceUrl =>
|
|
238
|
-
[
|
|
239
|
-
"default-src 'self'",
|
|
240
|
-
"script-src 'self' 'wasm-unsafe-eval' 'sha256-XJmzkFBLHYpcM8KgGRFztTJTwfMb5xIFKAmqlgTpobo=' https://challenges.cloudflare.com",
|
|
241
|
-
"worker-src 'self' blob:",
|
|
242
|
-
'frame-src https://challenges.cloudflare.com',
|
|
243
|
-
`connect-src 'self' wss: data:${authServiceUrl ? ` ${authServiceUrl}` : ''}`,
|
|
244
|
-
"img-src 'self' data: blob:",
|
|
245
|
-
"style-src 'self' 'unsafe-inline'",
|
|
246
|
-
"object-src 'none'",
|
|
247
|
-
"base-uri 'self'",
|
|
248
|
-
"frame-ancestors 'none'",
|
|
249
|
-
].join('; '),
|
|
250
|
-
referrerPolicy: 'no-referrer',
|
|
251
|
-
},
|
|
252
|
-
|
|
253
|
-
// журнал клиентских ошибок (plan/client-reports): приём POST
|
|
254
|
-
// /client-reports на боксе и пересылка пачками в auth-сервис. Пустой
|
|
255
|
-
// token — пересылка выключена, остаётся строка в журнале процесса
|
|
256
|
-
clientReports: {
|
|
257
|
-
token: '',
|
|
258
|
-
flushIntervalMs: 30000,
|
|
259
|
-
forwardBatch: 50,
|
|
260
|
-
forwardTimeoutMs: 5000,
|
|
261
|
-
maxPending: 500, // потолок разных отпечатков в буфере
|
|
262
|
-
logSeenMax: 5000, // сколько отпечатков процесс помнит «уже печатал»
|
|
263
|
-
// бюджет НОВЫХ отпечатков на бокс (решение 9 плана): распределённый спам
|
|
264
|
-
// обходит лимит по IP, а новая строка в auth — главный ресурс. Повторы
|
|
265
|
-
// известных отпечатков бюджет не тратят
|
|
266
|
-
newFingerprintsPerMinute: 60,
|
|
267
|
-
// ключ — адрес, для IPv6 — подсеть /64 (lib/clientIp.js → rateLimitKey)
|
|
268
|
-
// клиент шлёт не чаще раза в 10 с — запас на несколько вкладок за одним
|
|
269
|
-
// адресом (NAT); главная защита — бюджет новых отпечатков
|
|
270
|
-
rateLimit: { limit: 30, windowMs: 60000 },
|
|
271
|
-
bodyLimit: '16kb',
|
|
272
|
-
maxItemsPerRequest: 10,
|
|
273
|
-
},
|
|
274
|
-
|
|
275
|
-
// заголовок с регионом хоста от Nginx/CDN (например, CF-IPCountry);
|
|
276
|
-
// выбран вместо geoip-lite — бесплатнее по памяти
|
|
277
|
-
regionHeader: 'x-region',
|
|
278
|
-
|
|
279
|
-
// лимит сигнальных ping-запросов с одного IP (защита от DDOS)
|
|
280
|
-
pingRateLimit: {
|
|
281
|
-
limit: 10,
|
|
282
|
-
windowMs: 1000,
|
|
283
|
-
},
|
|
284
|
-
|
|
285
|
-
// ICE-конфигурация для установки P2P-соединений:
|
|
286
|
-
// STUN обязателен; TURN — опциональный релей по итогам Этапа 0
|
|
287
|
-
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
|
|
288
|
-
};
|
package/src/lib/clientIp.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
// Адрес клиента для rate-limit'ов серверных контуров.
|
|
2
|
-
//
|
|
3
|
-
// `X-Forwarded-For` для этого не годится: Nginx деплоя ставит его через
|
|
4
|
-
// `$proxy_add_x_forwarded_for` (.github/deployment/install-system.sh), то есть
|
|
5
|
-
// ДОПИСЫВАЕТ реальный адрес к тому, что прислал клиент. Первый элемент списка
|
|
6
|
-
// задаёт сам клиент — на нём лимит и обходится (свой заголовок на каждое
|
|
7
|
-
// соединение), и выбирается чужой бакет (заголовок с адресом жертвы закрывает
|
|
8
|
-
// ей вход). Тот же Nginx перезаписывает `X-Real-IP` значением `$remote_addr`,
|
|
9
|
-
// поэтому за прокси доверяем только ему, а без прокси — адресу сокета.
|
|
10
|
-
|
|
11
|
-
// один раз на процесс: предупреждение о прокси без X-Real-IP на каждое
|
|
12
|
-
// соединение превратилось бы в лог-флуд
|
|
13
|
-
let proxyHeaderWarned = false;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* @param {Object} req - Запрос (http.IncomingMessage или express Request).
|
|
17
|
-
* @param {Object} [options]
|
|
18
|
-
* @param {boolean} [options.trustProxy] - Стоит ли перед процессом обратный
|
|
19
|
-
* прокси, перезаписывающий `X-Real-IP` (в этом репозитории — прод-Nginx).
|
|
20
|
-
* @returns {string} Адрес клиента или '' (сокет уже разорван).
|
|
21
|
-
*/
|
|
22
|
-
export function clientIp(req, { trustProxy = false } = {}) {
|
|
23
|
-
const header = trustProxy ? req.headers['x-real-ip'] : undefined;
|
|
24
|
-
|
|
25
|
-
// за прокси без X-Real-IP ключом станет адрес самого прокси — один общий
|
|
26
|
-
// бакет на всех клиентов сразу: правило «одна комната на IP» пустило бы
|
|
27
|
-
// одну комнату на весь мастер, а лимит пингов стал бы общим. Падать на
|
|
28
|
-
// первом запросе нельзя (сервер должен подняться), молчать — тоже
|
|
29
|
-
if (trustProxy && !header && !proxyHeaderWarned) {
|
|
30
|
-
proxyHeaderWarned = true;
|
|
31
|
-
console.warn(
|
|
32
|
-
'[clientIp] trustProxy is on but no X-Real-IP header arrived: every ' +
|
|
33
|
-
"client now keys on the proxy's own address, one shared rate-limit " +
|
|
34
|
-
'bucket. Check `proxy_set_header X-Real-IP $remote_addr`.',
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return String(header || req.socket?.remoteAddress || '').trim();
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Ключ rate-limit'а по адресу: IPv4 — сам адрес, IPv6 — подсеть /64.
|
|
42
|
-
// Провайдер выдаёт абоненту /64 целиком, то есть у одного человека
|
|
43
|
-
// 2^64 адресов, и лимит «на адрес» для IPv6 не лимит вовсе
|
|
44
|
-
export function rateLimitKey(ip) {
|
|
45
|
-
const addr = String(ip ?? '').trim();
|
|
46
|
-
|
|
47
|
-
if (!addr) {
|
|
48
|
-
return '';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const mapped = /^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i.exec(addr);
|
|
52
|
-
|
|
53
|
-
if (mapped) {
|
|
54
|
-
return mapped[1];
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!addr.includes(':')) {
|
|
58
|
-
return addr;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const groups = expandIpv6(addr.split('%')[0]);
|
|
62
|
-
|
|
63
|
-
if (!groups) {
|
|
64
|
-
return addr;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return `v6:${groups
|
|
68
|
-
.slice(0, 4)
|
|
69
|
-
.map(g => parseInt(g, 16).toString(16))
|
|
70
|
-
.join(':')}::/64`;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// '2001:db8::1' → 8 hex-групп или null, если запись непарсима
|
|
74
|
-
function expandIpv6(addr) {
|
|
75
|
-
const halves = addr.split('::');
|
|
76
|
-
|
|
77
|
-
if (halves.length > 2) {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const head = halves[0] ? halves[0].split(':') : [];
|
|
82
|
-
const tail = halves.length === 2 && halves[1] ? halves[1].split(':') : [];
|
|
83
|
-
const missing = 8 - head.length - tail.length;
|
|
84
|
-
|
|
85
|
-
if (halves.length === 1 ? missing !== 0 : missing < 1) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const groups = [
|
|
90
|
-
...head,
|
|
91
|
-
...Array(halves.length === 2 ? missing : 0).fill('0'),
|
|
92
|
-
...tail,
|
|
93
|
-
];
|
|
94
|
-
|
|
95
|
-
return groups.every(g => /^[0-9a-f]{1,4}$/i.test(g)) ? groups : null;
|
|
96
|
-
}
|
package/src/lib/rateLimiter.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
// Rate limiter с фиксированным окном: не более `limit` событий
|
|
2
|
-
// на ключ (например, IP) за `windowMs` миллисекунд.
|
|
3
|
-
export default class RateLimiter {
|
|
4
|
-
constructor({ limit, windowMs }) {
|
|
5
|
-
this._limit = limit;
|
|
6
|
-
this._windowMs = windowMs;
|
|
7
|
-
this._buckets = new Map(); // key -> { count, windowStart }
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
// регистрирует событие; false — лимит для ключа исчерпан
|
|
11
|
-
consume(key, now = Date.now()) {
|
|
12
|
-
const bucket = this._buckets.get(key);
|
|
13
|
-
|
|
14
|
-
if (!bucket || now - bucket.windowStart >= this._windowMs) {
|
|
15
|
-
this._buckets.set(key, { count: 1, windowStart: now });
|
|
16
|
-
return true;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
if (bucket.count >= this._limit) {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
bucket.count += 1;
|
|
24
|
-
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// удаляет истёкшие окна (периодическая уборка памяти)
|
|
29
|
-
sweep(now = Date.now()) {
|
|
30
|
-
for (const [key, bucket] of this._buckets) {
|
|
31
|
-
if (now - bucket.windowStart >= this._windowMs) {
|
|
32
|
-
this._buckets.delete(key);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
clear() {
|
|
38
|
-
this._buckets.clear();
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/lib/security.js
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import config from './config.js';
|
|
2
|
-
|
|
3
|
-
// создаёт проверку origin для заданных protocol/domain/port
|
|
4
|
-
// (используется и игровым сервером, и мастер-сервером)
|
|
5
|
-
const createOriginValidator = function ({ protocol, domain, port }) {
|
|
6
|
-
return function (requestOrigin, cb) {
|
|
7
|
-
const isProduction = process.env.NODE_ENV === 'production';
|
|
8
|
-
|
|
9
|
-
const allowedOrigins = [
|
|
10
|
-
// origin'ы для локальной разработки
|
|
11
|
-
`${protocol}//localhost:${port}`,
|
|
12
|
-
`${protocol}//127.0.0.1:${port}`,
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
if (isProduction) {
|
|
16
|
-
// в продакшене разрешен только настоящий домен по стандартному HTTPS
|
|
17
|
-
// порт не требуется, так как браузер не добавляет его
|
|
18
|
-
// для стандартного порта 443
|
|
19
|
-
allowedOrigins.push(`https://${domain}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let err = null;
|
|
23
|
-
|
|
24
|
-
if (!allowedOrigins.includes(requestOrigin)) {
|
|
25
|
-
err = `Blocked connection from invalid origin: ${requestOrigin}`;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
process.nextTick(() => cb(err));
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const origin = function (requestOrigin, cb) {
|
|
33
|
-
const validator = createOriginValidator({
|
|
34
|
-
protocol: config.get('server:protocol'),
|
|
35
|
-
domain: config.get('server:domain'),
|
|
36
|
-
port: config.get('server:port'),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
validator(requestOrigin, cb);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export default {
|
|
43
|
-
origin,
|
|
44
|
-
createOriginValidator,
|
|
45
|
-
};
|