@truelab/trueserver 0.0.4 → 0.1.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 (137) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/MIGRATION.md +508 -0
  3. package/README.md +114 -94
  4. package/dist/application.d.ts.map +1 -1
  5. package/dist/application.js +9 -5
  6. package/dist/application.js.map +1 -1
  7. package/dist/examples.d.ts +1 -0
  8. package/dist/examples.d.ts.map +1 -1
  9. package/dist/examples.js +3 -1
  10. package/dist/examples.js.map +1 -1
  11. package/dist/helpers/config.helper.d.ts +9 -1
  12. package/dist/helpers/config.helper.d.ts.map +1 -1
  13. package/dist/helpers/config.helper.js +29 -0
  14. package/dist/helpers/config.helper.js.map +1 -1
  15. package/dist/helpers/errors.helper.d.ts +19 -0
  16. package/dist/helpers/errors.helper.d.ts.map +1 -0
  17. package/dist/helpers/errors.helper.js +21 -0
  18. package/dist/helpers/errors.helper.js.map +1 -0
  19. package/dist/helpers/fastify.helper.d.ts +5 -1
  20. package/dist/helpers/fastify.helper.d.ts.map +1 -1
  21. package/dist/helpers/fastify.helper.js +3 -1
  22. package/dist/helpers/fastify.helper.js.map +1 -1
  23. package/dist/helpers/handlers.helper.d.ts +10 -1
  24. package/dist/helpers/handlers.helper.d.ts.map +1 -1
  25. package/dist/helpers/handlers.helper.js +2 -1
  26. package/dist/helpers/handlers.helper.js.map +1 -1
  27. package/dist/helpers/money.helper.d.ts +13 -0
  28. package/dist/helpers/money.helper.d.ts.map +1 -0
  29. package/dist/helpers/money.helper.js +29 -0
  30. package/dist/helpers/money.helper.js.map +1 -0
  31. package/dist/index.d.ts +7 -4
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +11 -4
  34. package/dist/index.js.map +1 -1
  35. package/dist/plugins/404.plugin.d.ts.map +1 -1
  36. package/dist/plugins/404.plugin.js +2 -1
  37. package/dist/plugins/404.plugin.js.map +1 -1
  38. package/dist/plugins/apikey.plugin.d.ts.map +1 -1
  39. package/dist/plugins/apikey.plugin.js +6 -7
  40. package/dist/plugins/apikey.plugin.js.map +1 -1
  41. package/dist/plugins/error.plugin.d.ts.map +1 -1
  42. package/dist/plugins/error.plugin.js +6 -2
  43. package/dist/plugins/error.plugin.js.map +1 -1
  44. package/dist/plugins/sentry.plugin.d.ts.map +1 -1
  45. package/dist/plugins/sentry.plugin.js +1 -2
  46. package/dist/plugins/sentry.plugin.js.map +1 -1
  47. package/dist/plugins/swagger.plugin.d.ts.map +1 -1
  48. package/dist/plugins/swagger.plugin.js +5 -1
  49. package/dist/plugins/swagger.plugin.js.map +1 -1
  50. package/dist/routes/checksum/checksum.route.d.ts +3 -0
  51. package/dist/routes/checksum/checksum.route.d.ts.map +1 -0
  52. package/dist/routes/checksum/checksum.route.js +21 -0
  53. package/dist/routes/checksum/checksum.route.js.map +1 -0
  54. package/dist/routes/checksum/index.d.ts +3 -0
  55. package/dist/routes/checksum/index.d.ts.map +1 -0
  56. package/dist/routes/checksum/index.js +12 -0
  57. package/dist/routes/checksum/index.js.map +1 -0
  58. package/dist/routes/client/client.route.d.ts +3 -0
  59. package/dist/routes/client/client.route.d.ts.map +1 -0
  60. package/dist/routes/{client.route.js → client/client.route.js} +5 -5
  61. package/dist/routes/client/client.route.js.map +1 -0
  62. package/dist/routes/client/index.d.ts +3 -0
  63. package/dist/routes/client/index.d.ts.map +1 -0
  64. package/dist/routes/client/index.js +12 -0
  65. package/dist/routes/client/index.js.map +1 -0
  66. package/dist/routes/config/config.route.js +1 -1
  67. package/dist/routes/config/config.route.js.map +1 -1
  68. package/dist/routes/config/config.route.validate.plugin.d.ts.map +1 -1
  69. package/dist/routes/config/config.route.validate.plugin.js +48 -17
  70. package/dist/routes/config/config.route.validate.plugin.js.map +1 -1
  71. package/dist/routes/play/play.route.validate.plugin.d.ts +10 -9
  72. package/dist/routes/play/play.route.validate.plugin.d.ts.map +1 -1
  73. package/dist/routes/play/play.route.validate.plugin.js +90 -39
  74. package/dist/routes/play/play.route.validate.plugin.js.map +1 -1
  75. package/dist/routes/validate/validate.route.validate.plugin.d.ts.map +1 -1
  76. package/dist/routes/validate/validate.route.validate.plugin.js +5 -4
  77. package/dist/routes/validate/validate.route.validate.plugin.js.map +1 -1
  78. package/dist/routes-public/healthcheck.route.d.ts.map +1 -0
  79. package/dist/{routes → routes-public}/healthcheck.route.js +4 -10
  80. package/dist/routes-public/healthcheck.route.js.map +1 -0
  81. package/dist/schemas/checksum.schema.d.ts +11 -0
  82. package/dist/schemas/checksum.schema.d.ts.map +1 -0
  83. package/dist/schemas/checksum.schema.js +26 -0
  84. package/dist/schemas/checksum.schema.js.map +1 -0
  85. package/dist/schemas/client.schema.d.ts +1 -1
  86. package/dist/schemas/client.schema.js +4 -4
  87. package/dist/schemas/client.schema.js.map +1 -1
  88. package/dist/schemas/config.schema.d.ts +17 -20
  89. package/dist/schemas/config.schema.d.ts.map +1 -1
  90. package/dist/schemas/config.schema.js +42 -43
  91. package/dist/schemas/config.schema.js.map +1 -1
  92. package/dist/schemas/examples/checksum.schema.examples.d.ts +3 -0
  93. package/dist/schemas/examples/checksum.schema.examples.d.ts.map +1 -0
  94. package/dist/schemas/examples/checksum.schema.examples.js +26 -0
  95. package/dist/schemas/examples/checksum.schema.examples.js.map +1 -0
  96. package/dist/schemas/examples/client.schema.examples.d.ts.map +1 -1
  97. package/dist/schemas/examples/client.schema.examples.js +15 -18
  98. package/dist/schemas/examples/client.schema.examples.js.map +1 -1
  99. package/dist/schemas/examples/config.schema.examples.d.ts.map +1 -1
  100. package/dist/schemas/examples/config.schema.examples.js +65 -33
  101. package/dist/schemas/examples/config.schema.examples.js.map +1 -1
  102. package/dist/schemas/examples/healthcheck.schema.examples.d.ts.map +1 -1
  103. package/dist/schemas/examples/healthcheck.schema.examples.js +3 -14
  104. package/dist/schemas/examples/healthcheck.schema.examples.js.map +1 -1
  105. package/dist/schemas/examples/play.schema.examples.d.ts.map +1 -1
  106. package/dist/schemas/examples/play.schema.examples.js +94 -130
  107. package/dist/schemas/examples/play.schema.examples.js.map +1 -1
  108. package/dist/schemas/examples/validate.schema.examples.d.ts.map +1 -1
  109. package/dist/schemas/examples/validate.schema.examples.js +51 -41
  110. package/dist/schemas/examples/validate.schema.examples.js.map +1 -1
  111. package/dist/schemas/healthcheck.schema.d.ts +2 -20
  112. package/dist/schemas/healthcheck.schema.d.ts.map +1 -1
  113. package/dist/schemas/healthcheck.schema.js +10 -41
  114. package/dist/schemas/healthcheck.schema.js.map +1 -1
  115. package/dist/schemas/play.schema.d.ts +22 -17
  116. package/dist/schemas/play.schema.d.ts.map +1 -1
  117. package/dist/schemas/play.schema.js +37 -45
  118. package/dist/schemas/play.schema.js.map +1 -1
  119. package/dist/schemas/shared.d.ts +13 -21
  120. package/dist/schemas/shared.d.ts.map +1 -1
  121. package/dist/schemas/shared.js +56 -72
  122. package/dist/schemas/shared.js.map +1 -1
  123. package/dist/schemas/validate.schema.d.ts +11 -7
  124. package/dist/schemas/validate.schema.d.ts.map +1 -1
  125. package/dist/schemas/validate.schema.js +14 -16
  126. package/dist/schemas/validate.schema.js.map +1 -1
  127. package/package.json +5 -2
  128. package/dist/plugins/metrics.plugin.d.ts +0 -9
  129. package/dist/plugins/metrics.plugin.d.ts.map +0 -1
  130. package/dist/plugins/metrics.plugin.js +0 -24
  131. package/dist/plugins/metrics.plugin.js.map +0 -1
  132. package/dist/routes/client.route.d.ts +0 -3
  133. package/dist/routes/client.route.d.ts.map +0 -1
  134. package/dist/routes/client.route.js.map +0 -1
  135. package/dist/routes/healthcheck.route.d.ts.map +0 -1
  136. package/dist/routes/healthcheck.route.js.map +0 -1
  137. /package/dist/{routes → routes-public}/healthcheck.route.d.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,73 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.1.0] - Unreleased
10
+
11
+ ### Breaking
12
+
13
+ - `/play` ответ **уплощён**: убрана обёртка `results`. Все поля теперь на верхнем уровне.
14
+ - `/play` ответ: `results.cashWin` → `winCash`; `results.coins` → `winCoins`.
15
+ - `/play` ответ: добавлено обязательное поле `hasMaxWin: boolean`. При достижении max-win обязательно `hasMaxWin: true` вместе с `finished: true`.
16
+ - `/play` ответ: `results.choices?: integer[]` → `choices?: (number | string)[]` — тип элемента расширен до `number | string`.
17
+ - `/play` ответ: добавлено поле `choice?: number | string` — эхо `choice` с continuation-шагов.
18
+ - `/play` запрос: `choice` — тип `integer` → `number | string`.
19
+ - `/config` ответ: удалено поле `gameTitle`.
20
+ - `/config` ответ: `hasVariableLines` → `hasVariableLevels`.
21
+ - `/config` ответ: добавлено обязательное поле `hasValidate: boolean`.
22
+ - `/config` инварианты: при `hasMultiStake === true` обязательно `freeroundsAllowed === false` и `hasValidate === true`.
23
+ - `TGameConfig` (`registerGameConfig`): удалено поле `gameTitle`; `hasVariableLines` → `hasVariableLevels`; `hasValidation` → `hasValidate` (обязательное при `hasMultiStake === true`).
24
+ - `/validate` запрос: `demo` → `isDemo`; `metaPublic`/`metaPrivate` (строки) удалены — заменены на `meta: { public, private }` (объект); `currencyDecimals` и `stakes` стали обязательными.
25
+ - `/validate` ответ: пустой `{}` → `{ valid: boolean, reason?: string }`. Хендлер игры теперь возвращает вердикт, а не пустой объект. `valid: false` — это `200` с отказом (раунд не открывается), не ошибка.
26
+ - `StakeMode.parametric` — стало опциональным (отсутствие эквивалентно `false`).
27
+
28
+ ### Added
29
+
30
+ - Money-хелперы: `Big`, `roundMoney`, `isExactDecimal` — экспортируются из `@truelab/trueserver`. Позволяют считать денежные значения в decimal-арифметике без подключения отдельной библиотеки.
31
+ - Зависимость `big.js` добавлена в `dependencies` пакета (передаётся транзитивно потребителям).
32
+
33
+ ### Changed
34
+
35
+ - Включает все изменения из 0.0.7 (multi-RTP режим: `rtpVersion` в `registerGameConfig` стал опциональным; сверка `rtpVersion` ответа с path-сегментом выполняется всегда).
36
+
37
+ ## [0.0.7] - 2026-06-06
38
+
39
+ ### Changed
40
+ - `TGameConfig.rtpVersion` теперь **опциональное**. Поддержан режим «один инстанс — все RTP»: игра не регистрирует `rtpVersion`, а берёт его из сегмента `{rtpVersion}` пути на каждый запрос.
41
+ - `config.route.validate.plugin`: сверка `rtpVersion` ответа с `gameConfig.rtpVersion` выполняется **только если** он задан (single-RTP). Сверка с path-сегментом `{rtpVersion}` выполняется всегда. См. [MIGRATION.md](MIGRATION.md).
42
+
43
+ ## [0.0.6] - 2026-06-03
44
+
45
+ ### Breaking
46
+ - `/config` reply: добавлено обязательное поле `rtpVersion: string` (лейбл RTP-бакета, совпадает с сегментом `{rtpVersion}` пути). См. [MIGRATION.md](MIGRATION.md).
47
+ - `TGameConfig.rtpVersion` — теперь обязательное поле для `registerGameConfig`.
48
+ - `Stake.amount` — семантика изменилась: теперь это **полная сумма ставки**, уже умноженная на `StakeMode.bets` и `qty`. Формула: `amount = baseBetAmount × StakeMode.bets × (qty ?? 1)`. Не валидируется пакетом — обновлены только описания.
49
+
50
+ ### Added
51
+ - `Stake.coinValue?: number` — опциональное поле для игр с coin-механикой (для рулеток и подобных — отсутствует).
52
+ - `config.route.validate.plugin` сверяет `rtpVersion` в трёх источниках: `request.params.rtpVersion`, `payload.rtpVersion`, `gameConfig.rtpVersion`. Несовпадение → 500 с `code=1006`.
53
+
54
+ ## [0.0.5] - 2026-05-29
55
+
56
+ ### Breaking
57
+ - Контракт API приведён к [GS Documentation 1.0.0](https://www.notion.so/truelab/GS-Documentation-36dd90f8c00680d485d4e4e3eba928f0). Полный список изменений и инструкция по переходу — в [MIGRATION.md](MIGRATION.md).
58
+ - Гейм-плей роуты под версионным префиксом `/:rtpVersion` (`/config`, `/client`, `/play`, `/validate`, `/checksum`). `/healthcheck` остаётся без версии.
59
+ - HTTP-методы: `GET /healthcheck`, `GET /:rtpVersion/config`, `GET /:rtpVersion/client`, `GET /:rtpVersion/checksum`. `/play` и `/validate` — `POST`.
60
+ - Обёртка ошибок: `{ status: false, code, message }` (вместо `{ ok: false, message }`).
61
+ - API-ключ проверяется на **всех** запросах (включая `GET`).
62
+ - `/play`: переименование полей (`demo` → `isDemo`, `winCash` → `cashWin`, `winCoins` → `coins`); `meta` — единый объект `{ public, private }`; `gameResponse` и `rng` в новом формате; `finished/choices/meta` перенесены в `results`. Добавлено обязательное `stepIndex`.
63
+ - `/config`: новая форма (`gameTitle`, `gameId`, `volatility: number`, `rtp: string`, `hasRestore`, `hasVariableLines`, `variableLevels: number[]`, `defaultLevel`). Удалены `category`, `hitrate`, `apiVersion`, `hasBuy`, `hasGamble`, `ECategory`, `EVolatility`.
64
+ - `/healthcheck`: ответ упрощён до `{ ok, gameId, environment, uptime, timestamp }`. Поле `service` → `gameId`. Удалены `metrics`, `services`, `code`, `status`, `message`.
65
+ - `/client`: убрано `ok`, добавлено опциональное поле `test` для cheat-tool.
66
+ - Удалён плагин `metrics`.
67
+
68
+ ### Added
69
+ - Новый эндпоинт `GET /:rtpVersion/checksum` для проверки целостности файлов.
70
+ - Хелпер `EErrorCode` + типизированный `httpError(message, statusCode, code)`.
71
+ - Инвариант `hasChoice ⇒ hasMeta` и `hasVariableLines ⇔ {variableLevels, defaultLevel}` проверяются в `registerGameConfig`.
72
+ - Экспорт `MetaSchema` / `TMeta`.
73
+
74
+ ## [Unreleased archive]
75
+
9
76
  ### Added
10
77
  - NPM-пакет `@truelab/trueserver` (exports, dist, prepack, publishConfig)
11
78
  - Bitbucket Pipelines: test / build / publish
package/MIGRATION.md ADDED
@@ -0,0 +1,508 @@
1
+ # Migration
2
+
3
+ - [0.0.7 → 0.1.0](#migration-007--010) (последнее)
4
+ - [0.0.6 → 0.0.7](#migration-006--007)
5
+ - [0.0.5 → 0.0.6](#migration-005--006)
6
+ - [0.0.4 → 0.0.5](#migration-004--005)
7
+
8
+ ---
9
+
10
+ ## Migration: 0.0.7 → 0.1.0
11
+
12
+ Ломающий релиз. Приводит пакет в полное соответствие контракту Game Server API v1.0.0.
13
+
14
+ > **TL;DR что менять в игре:**
15
+ > 1. `/play` хендлер: убрать обёртку `results`, переименовать поля, добавить `hasMaxWin`.
16
+ > 2. `/config` хендлер: убрать `gameTitle`, заменить `hasVariableLines` на `hasVariableLevels`, добавить `hasValidate`.
17
+ > 3. `registerGameConfig`: убрать `gameTitle`, `hasVariableLines` → `hasVariableLevels`, `hasValidation` → `hasValidate`.
18
+ > 4. `/validate` хендлер: обновить запрос и вернуть `{ valid: boolean, reason?: string }`.
19
+
20
+ ### 1. `/play` ответ — уплощение и переименования
21
+
22
+ Обёртка `results` удалена. Все поля возвращаются на верхнем уровне.
23
+
24
+ ```diff
25
+ // handlers.register('play', async (request): Promise<TPlayReply> => {
26
+ return {
27
+ gameResponse: {},
28
+ rng: [],
29
+ - results: {
30
+ - coins: 250,
31
+ - cashWin: 2.5,
32
+ - finished: true,
33
+ - },
34
+ + winCoins: 250,
35
+ + winCash: 2.5,
36
+ + finished: true,
37
+ + hasMaxWin: false,
38
+ }
39
+ ```
40
+
41
+ | Старое | Новое |
42
+ |---|---|
43
+ | `results.cashWin` | `winCash` |
44
+ | `results.coins` | `winCoins` |
45
+ | `results.finished` | `finished` |
46
+ | `results.choices` | `choices` |
47
+ | `results.meta` | `meta` |
48
+ | — | `hasMaxWin` (обязательное) |
49
+ | — | `choice` (эхо choice на continuation-шагах) |
50
+
51
+ **`hasMaxWin`** — обязательное поле в каждом ответе. При достижении max-win: `hasMaxWin: true` + `finished: true` одновременно. Незавершённый раунд с `hasMaxWin: true` является ошибкой.
52
+
53
+ ### 2. `/play` запрос — тип `choice`
54
+
55
+ `choice` расширен до `number | string` (раньше был только `integer`). Аналогично `choices` в ответе стал `(number | string)[]`.
56
+
57
+ ### 3. `/config` ответ
58
+
59
+ ```diff
60
+ {
61
+ - gameTitle: 'My Game',
62
+ gameId: 'my-game',
63
+ ...
64
+ - hasVariableLines: false,
65
+ + hasVariableLevels: false,
66
+ coinRatio: 10,
67
+ ...
68
+ + hasValidate: false,
69
+ }
70
+ ```
71
+
72
+ | Старое | Новое |
73
+ |---|---|
74
+ | `gameTitle` | удалено |
75
+ | `hasVariableLines` | `hasVariableLevels` |
76
+ | — | `hasValidate` (обязательное) |
77
+
78
+ **Новые инварианты** при `hasMultiStake === true`:
79
+ - `freeroundsAllowed` должен быть `false`
80
+ - `hasValidate` должен быть `true`
81
+
82
+ ### 4. `registerGameConfig`
83
+
84
+ ```diff
85
+ registerGameConfig({
86
+ - gameTitle: 'My Game',
87
+ gameId: 'my-game',
88
+ ...
89
+ - hasVariableLines: false,
90
+ + hasVariableLevels: false,
91
+ coinRatio: 10,
92
+ - hasValidation: true, // legacy
93
+ + hasValidate: true,
94
+ })
95
+ ```
96
+
97
+ | Старое | Новое |
98
+ |---|---|
99
+ | `gameTitle` | удалено |
100
+ | `hasVariableLines` | `hasVariableLevels` |
101
+ | `hasValidation` | `hasValidate` (обязательное при `hasMultiStake === true`) |
102
+
103
+ ### 5. `/validate` запрос и ответ
104
+
105
+ **Запрос** — поля `metaPublic`/`metaPrivate` (строки) удалены, заменены на `meta: { public, private }` (объект); `demo` → `isDemo`; `currencyDecimals` и `stakes` стали обязательными.
106
+
107
+ ```diff
108
+ -{
109
+ - demo: true,
110
+ - currencyDecimals: 2,
111
+ - stakes: [{ name: 'default', amount: 3 }],
112
+ - maxExposure: 100000,
113
+ - metaPublic: '{"someProgress": 15}',
114
+ - metaPrivate: '{"seed": "..."}',
115
+ -}
116
+ +{
117
+ + isDemo: true,
118
+ + currencyDecimals: 2,
119
+ + stakes: [{ name: 'default', amount: 3 }],
120
+ + maxExposure: 100000,
121
+ + meta: {
122
+ + public: { someProgress: 15 },
123
+ + private: { seed: '...' },
124
+ + },
125
+ +}
126
+ ```
127
+
128
+ **Ответ** — хендлер теперь возвращает вердикт, а не пустой объект:
129
+
130
+ ```diff
131
+ -handlers.register('validate', async (): Promise<TValidateReply> => ({}))
132
+ +handlers.register('validate', async (request): Promise<TValidateReply> => {
133
+ + // Validate stake set — e.g. check that max win cannot exceed maxExposure
134
+ + const isValid = checkStakes(request.body.stakes, request.body.maxExposure)
135
+ + if (!isValid) {
136
+ + return { valid: false, reason: 'potential win exceeds maxExposure' }
137
+ + }
138
+ + return { valid: true }
139
+ +})
140
+ ```
141
+
142
+ `valid: false` возвращается со статусом `200` — это деловой вердикт, не ошибка. RGS не откроет раунд при `valid: false`. Некорректный запрос (отсутствуют обязательные поля) — по-прежнему `400`.
143
+
144
+ ### 6. `StakeMode.parametric` — стало опциональным
145
+
146
+ ```diff
147
+ -{ name: 'default', type: 'default', bets: 1, parametric: false }
148
+ +{ name: 'default', type: 'default', bets: 1 }
149
+ ```
150
+
151
+ Отсутствие `parametric` эквивалентно `false`. Существующий код, передающий `parametric: false`, продолжает работать.
152
+
153
+ ### 7. Money-хелперы (новое)
154
+
155
+ Пакет теперь экспортирует decimal-хелперы. Используйте их для вычисления `winCash`/`winCoins` вместо нативной float-арифметики:
156
+
157
+ ```typescript
158
+ import { Big, roundMoney, isExactDecimal } from '@truelab/trueserver'
159
+
160
+ const winCash = roundMoney(new Big(coins).times(coinValue), currencyDecimals)
161
+ // winCash — точное decimal-число без float-дрейфа
162
+ ```
163
+
164
+ ### Чек-лист
165
+
166
+ 1. `npm install @truelab/trueserver@0.1.0`.
167
+ 2. `/play` хендлер: убрать `results`, переименовать `cashWin`→`winCash`, `coins`→`winCoins`, добавить `hasMaxWin`.
168
+ 3. `/config` хендлер: убрать `gameTitle`, `hasVariableLines`→`hasVariableLevels`, добавить `hasValidate`.
169
+ 4. `registerGameConfig`: убрать `gameTitle`, `hasVariableLines`→`hasVariableLevels`, `hasValidation`→`hasValidate`.
170
+ 5. `/validate` хендлер: обновить форму запроса (убрать `metaPublic`/`metaPrivate`, добавить `meta`; `demo`→`isDemo`); вернуть `{ valid: boolean, reason?: string }`.
171
+ 6. Проверить инварианты: если `hasMultiStake: true` — `freeroundsAllowed: false` и `hasValidate: true`.
172
+ 7. Обновить тесты: плоский ответ `/play`, новые поля, новый контракт `/validate`.
173
+ 8. Прогнать тесты.
174
+
175
+ ---
176
+
177
+ ## Migration: 0.0.6 → 0.0.7
178
+
179
+ Не ломающее изменение. `rtpVersion` в `registerGameConfig` стал опциональным, чтобы один инстанс мог обслуживать все RTP-версии, выбирая их по сегменту `{rtpVersion}` пути.
180
+
181
+ > **TL;DR:**
182
+ > - Хотите как раньше (один инстанс = один RTP) — ничего менять не нужно, `rtpVersion` в `registerGameConfig` продолжает работать и сверяется.
183
+ > - Хотите один инстанс на все RTP — **не передавайте** `rtpVersion` в `registerGameConfig`, а в reply `handlers.register('config', ...)` возвращайте `rtpVersion` из `request.params.rtpVersion`.
184
+
185
+ ### Режим «один инстанс — все RTP»
186
+
187
+ ```diff
188
+ registerGameConfig({
189
+ gameTitle: 'My Game',
190
+ gameId: 'my-game',
191
+ - rtpVersion: '96',
192
+ ...
193
+ })
194
+ ```
195
+
196
+ ```diff
197
+ handlers.register('config', async (request) => ({
198
+ ...
199
+ - rtpVersion: '96',
200
+ + rtpVersion: request.params.rtpVersion,
201
+ ...
202
+ }))
203
+ ```
204
+
205
+ Правила валидации `/config`:
206
+ - `rtpVersion` ответа **всегда** сверяется с сегментом `{rtpVersion}` пути (несовпадение → `500` `code=1006`);
207
+ - с `gameConfig.rtpVersion` — **только если** он задан (single-RTP режим).
208
+
209
+ Все остальные RTP-зависимые поля (`rtp`, `volatility`, при необходимости `coinRatio`/`variableLevels`) тоже выбирайте по `request.params.rtpVersion`. Помните, что `coinRatio`/`variableLevels`/`defaultLevel` по-прежнему сверяются с `gameConfig`, поэтому при их различии между RTP single-config сверка не подойдёт.
210
+
211
+ ---
212
+
213
+ ## Migration: 0.0.5 → 0.0.6
214
+
215
+ Документация GS Documentation обновлена после релиза 0.0.5. Изменения локальные, но breaking: новое обязательное поле в `/config` и опциональное в `Stake`.
216
+
217
+ > **TL;DR что менять в игре:**
218
+ > 1. В `registerGameConfig(...)` добавить `rtpVersion: '96'` (или ваш лейбл).
219
+ > 2. В `handlers.register('config', ...)` добавить `rtpVersion` в reply (тот же, что в `gameConfig`, и совпадающий с path-сегментом).
220
+ > 3. Опционально: добавить `coinValue` в `Stake` для игр с coin-механикой (запросы продолжают приниматься и без него).
221
+ > 4. Перепроверить `Stake.amount`: теперь это **полная сумма** (включая `StakeMode.bets` и `qty`), а не «на одну ставку».
222
+
223
+ ### 1. `/config` reply → новое обязательное `rtpVersion`
224
+
225
+ ```diff
226
+ {
227
+ "gameTitle": "Book of Odin",
228
+ "gameId": "book-of-odin",
229
+ "volatility": 2.5,
230
+ "rtp": "96.17",
231
+ + "rtpVersion": "96",
232
+ "hasMeta": true,
233
+ ...
234
+ }
235
+ ```
236
+
237
+ `rtpVersion` — лейбл RTP-бакета (`"96"`, `"97"`, …). Он:
238
+ - совпадает с сегментом `{rtpVersion}` пути, под которым смонтирован endpoint;
239
+ - 1:1 с `rtp` (одной RTP-версии — один точный `rtp` и один `rtpVersion`);
240
+ - сверяется с `gameConfig.rtpVersion` и с `request.params.rtpVersion`. Несовпадение → `500` с `code=1006`.
241
+
242
+ ### 2. `TGameConfig.rtpVersion`
243
+
244
+ ```diff
245
+ registerGameConfig({
246
+ gameTitle: 'My Game',
247
+ gameId: 'my-game',
248
+ + rtpVersion: '96',
249
+ ...
250
+ })
251
+ ```
252
+
253
+ `registerGameConfig` падает на старте, если `rtpVersion` пустой или не строка.
254
+
255
+ ### 3. `Stake.coinValue` (опционально)
256
+
257
+ ```diff
258
+ {
259
+ "name": "regular",
260
+ "amount": 1.0,
261
+ + "coinValue": 0.1,
262
+ "level": 9
263
+ }
264
+ ```
265
+
266
+ Игры без coin-механики (рулетки и т.п.) **не передают** это поле. Пакет не проверяет его наличие/отсутствие — формат остаётся на стороне игры.
267
+
268
+ ### 4. `Stake.amount` — новая семантика
269
+
270
+ Ничего в схеме менять не нужно, но **читать значение** теперь надо по-другому:
271
+
272
+ ```
273
+ amount = baseBetAmount × StakeMode.bets × (qty ?? 1)
274
+ ```
275
+
276
+ `baseBetAmount` — стоимость одной базовой ставки в валюте (`coinValue × coinRatio` для fixed-line; `coinValue × level` для variable-line).
277
+
278
+ `qty` **уже учтён** в `amount` — нельзя умножать ещё раз.
279
+
280
+ ### Чек-лист
281
+
282
+ 1. `npm install @truelab/trueserver@0.0.6`.
283
+ 2. Добавить `rtpVersion` в `registerGameConfig({...})`.
284
+ 3. Добавить `rtpVersion` в reply `handlers.register('config', ...)` — должен совпадать с тем, что в `gameConfig`, и с path-сегментом.
285
+ 4. Перепроверить расчёт `Stake.amount` в обработчике `/play`.
286
+ 5. Если используется coin-механика — добавить `coinValue` в формирование тестовых запросов / mocks.
287
+ 6. Прогнать тесты.
288
+
289
+ ---
290
+
291
+ ## Migration: 0.0.4 → 0.0.5
292
+
293
+ Этот документ описывает все изменения контракта между 0.0.4 и 0.0.5.
294
+ 0.0.5 приводит пакет в соответствие со спецификацией [GS Documentation 1.0.0](https://www.notion.so/truelab/GS-Documentation-36dd90f8c00680d485d4e4e3eba928f0).
295
+
296
+ Релиз помечен как **patch**, но **является ломающим**: меняются методы и пути роутов, формат ответов, имена и формы полей, удаляются enum'ы и флаги. Игры, использующие 0.0.4, не запустятся на 0.0.5 без правок.
297
+
298
+ > **TL;DR что менять в игре:**
299
+ > 1. `registerGameConfig(...)` — новые имена/набор полей.
300
+ > 2. `handlers.register('config', ...)` — новый ответ.
301
+ > 3. `handlers.register('play', ...)` — новый `request` и `reply`; в реквесте `request.params.rtpVersion` доступен.
302
+ > 4. `handlers.register('client', ...)` — убрать `ok: true`, добавить `test` если нужен cheat-tool.
303
+ > 5. Добавить `handlers.register('checksum', ...)`.
304
+ > 6. Везде, где раньше возвращали ошибку — теперь формат `{ status, code, message }`.
305
+
306
+ ## 1. Маршруты и методы
307
+
308
+ | Старое | Новое |
309
+ |---|---|
310
+ | `POST /healthcheck` | **`GET`** `/healthcheck` (без `:rtpVersion`) |
311
+ | `POST /config` | **`GET`** `/:rtpVersion/config` |
312
+ | `POST /client` | **`GET`** `/:rtpVersion/client` |
313
+ | — | **`GET`** `/:rtpVersion/checksum` (новый) |
314
+ | `POST /play` | `POST` `/:rtpVersion/play` |
315
+ | `POST /validate` | `POST` `/:rtpVersion/validate` |
316
+
317
+ - `{rtpVersion}` — путь-сегмент (`96`, `97`, `98` и т. п.). Игра получает его из `request.params.rtpVersion` (`TRtpParams` — см. `helpers/handlers.helper.ts`).
318
+ - `ROUTE_PREFIX` оборачивает оба варианта. Например, `ROUTE_PREFIX=/api/v1` даёт `GET /api/v1/healthcheck` и `POST /api/v1/96/play`.
319
+
320
+ ## 2. Аутентификация
321
+
322
+ API-ключ теперь проверяется на **всех** HTTP-методах (включая `GET`). Раньше — только на `POST`. Если у внешней инфраструктуры есть GET-пробы без заголовка `X-API-KEY`, их нужно обновить.
323
+
324
+ ## 3. Формат ошибок
325
+
326
+ Старое:
327
+
328
+ ```json
329
+ { "ok": false, "message": "...", "custom": null }
330
+ ```
331
+
332
+ Новое:
333
+
334
+ ```json
335
+ { "status": false, "code": 4001, "message": "..." }
336
+ ```
337
+
338
+ `code` — целое число. Диапазоны:
339
+
340
+ | Диапазон | Категория |
341
+ |---|---|
342
+ | `1xxx` | Валидация |
343
+ | `2xxx` | Аутентификация |
344
+ | `4xxx` | Бизнес-правила |
345
+ | `9xxx` | Internal / unhandled |
346
+
347
+ Константы — в `EErrorCode` (экспорт из корня пакета). Для собственных ошибок используйте `httpError(message, statusCode, code)`:
348
+
349
+ ```typescript
350
+ import { EErrorCode, httpError } from '@truelab/trueserver'
351
+
352
+ throw httpError('Unknown choice', 400, EErrorCode.BusinessChoiceUnknown)
353
+ ```
354
+
355
+ ## 4. `TGameConfig` (вход `registerGameConfig`)
356
+
357
+ | Старое | Новое |
358
+ |---|---|
359
+ | `name` | `gameTitle` |
360
+ | — | `gameId` (новое, общий ID для всех RTP) |
361
+ | `hasMeta` | `hasMeta` |
362
+ | `hasChoice` | `hasChoice` (теперь требует `hasMeta=true`) |
363
+ | `hasBuy` | — (удалено) |
364
+ | `hasGamble` | — (удалено) |
365
+ | — | `hasRestore` (новое) |
366
+ | `hasMultiStake` | `hasMultiStake` |
367
+ | `hasValidation` | `hasValidation` (опциональное, оставлено для `/validate`) |
368
+ | — | `hasVariableLines` (новое; раньше определялось по наличию `variableLevels`) |
369
+ | `variableLevels: number` (целое) | `variableLevels: number[]` (массив доступных уровней) |
370
+ | — | `defaultLevel: number` (обязателен при `hasVariableLines=true`) |
371
+ | `coinRatio?` | `coinRatio` (required при `hasVariableLines=false`) |
372
+
373
+ `registerGameConfig` теперь бросает при нарушении:
374
+ - `hasChoice=true` без `hasMeta=true`;
375
+ - `hasVariableLines=true` без непустого `variableLevels` или с `defaultLevel ∉ variableLevels`;
376
+ - `hasVariableLines=false` без `coinRatio`.
377
+
378
+ ## 5. `/config` reply
379
+
380
+ | Старое | Новое |
381
+ |---|---|
382
+ | `ok: true` | — (убрать, успех — plain JSON без обёртки) |
383
+ | `name` | `gameTitle` |
384
+ | — | `gameId` |
385
+ | `category: ECategory` | — (удалено) |
386
+ | `rtp: number` | `rtp: string` (десятичная строка, например `"96.17"`) |
387
+ | `volatility: EVolatility` (enum-строка, опционально) | `volatility: number` из `{0.5, 1, 1.5, …, 5}` (обязательное) |
388
+ | `hitrate?: number` | — (удалено) |
389
+ | `apiVersion: number` | — (удалено) |
390
+ | `hasBuy`, `hasGamble`, `hasValidation` | — (удалены из reply; `hasValidation` остался в `TGameConfig`) |
391
+ | — | `hasRestore: boolean` |
392
+ | — | `hasVariableLines: boolean` |
393
+ | `variableLevels: number` | `variableLevels?: number[]` |
394
+ | — | `defaultLevel?: number` |
395
+ | `minWin?: number` | `minWin: number` (обязательное, множитель ставки) |
396
+ | `maxWin: number` | `maxWin: number` (множитель ставки) |
397
+ | `coinRatio?: number` | `coinRatio?: number` |
398
+ | `stakeModes: StakeMode[]` | без изменений |
399
+
400
+ Удалены enum'ы: `ECategory`, `EVolatility`.
401
+
402
+ ## 6. `/play` request
403
+
404
+ | Старое | Новое |
405
+ |---|---|
406
+ | `demo: boolean` | `isDemo: boolean` |
407
+ | `currencyDecimals?: number` | `currencyDecimals: integer` (обязательное) |
408
+ | — | `stepIndex: integer` (новое; `0` для opening step, инкремент на каждое продолжение) |
409
+ | `stakes?: Stake[]` | `stakes?: Stake[]` — только на opening step; уникальные `name` |
410
+ | `choice?: integer` | `choice?: integer` — обязателен на continuation step при `hasChoice=true` |
411
+ | `metaPrivate?: string`, `metaPublic?: string` | `meta?: { public, private }` — один объект |
412
+ | `isGambleAllowed?: boolean` | — (удалено) |
413
+ | `test?: string` | `test?: string` — игнорируется в production |
414
+
415
+ ## 7. `/play` reply
416
+
417
+ | Старое | Новое |
418
+ |---|---|
419
+ | `ok: true` | — (убрать) |
420
+ | `rng: string` (stringified JSON) | `rng: Array<{ value: number, ... }>` |
421
+ | `finished: boolean` (top-level) | `results.finished: boolean` |
422
+ | `results.response: string` (stringified JSON) | `gameResponse: object` (top-level) |
423
+ | `results.winCash` | `results.cashWin` |
424
+ | `results.winCoins?` | `results.coins?` |
425
+ | `metaPrivate?`, `metaPublic?` (top-level, strings) | `results.meta?: { public, private }` |
426
+ | `choices?: number[]` (top-level) | `results.choices?: integer[]` (non-empty, не `null`/`[]`) |
427
+ | `analytics?` | — (удалено) |
428
+
429
+ Контракт `choices`: должен присутствовать тогда и только тогда, когда `hasChoice=true` и `results.finished=false`.
430
+
431
+ ## 8. `/client` reply
432
+
433
+ | Старое | Новое |
434
+ |---|---|
435
+ | `ok: true` | — (убрать) |
436
+ | `data: object` | `data: object` |
437
+ | — | `test?: object` (опциональное; RGS фильтрует для не-test-сессий) |
438
+
439
+ ## 9. `/healthcheck` reply
440
+
441
+ | Старое | Новое |
442
+ |---|---|
443
+ | `code: 200` | — |
444
+ | `status: true` | — |
445
+ | `service: string` | `gameId: string` |
446
+ | `environment` | `environment` |
447
+ | `uptime: number` | `uptime: number` |
448
+ | `message: 'OK'` | — |
449
+ | `timestamp: integer` | `timestamp: integer` (ms) |
450
+ | `metrics: {...}` | — (плагин `metrics` удалён) |
451
+ | `services: { sentry }` | — |
452
+ | — | `ok: true` (новое) |
453
+
454
+ ## 10. `/checksum` (новый эндпоинт)
455
+
456
+ `GET /:rtpVersion/checksum`:
457
+
458
+ ```json
459
+ {
460
+ "files": [
461
+ { "path": "src/math/probabilities.json", "hash": "9f86..." },
462
+ { "path": "src/math/paytable.json", "hash": "6c1b..." }
463
+ ]
464
+ }
465
+ ```
466
+
467
+ Алгоритм хэширования и набор файлов согласуются с RGS-командой при деплое.
468
+
469
+ ## 11. `/validate` (без изменений, но патч)
470
+
471
+ `/validate` сохранён как был (вне новой спеки) для обратной совместимости. Изменилось только:
472
+ - Reply теперь пустой объект `{}` (раньше `{ ok: true }`).
473
+ - Endpoint, как и остальные версионированные, доступен под `/:rtpVersion/validate`.
474
+
475
+ `hasValidation` остался во входе `registerGameConfig` как опциональный флаг. Если `false`, плагин валидации `/validate` отбивает запросы 400-кодом.
476
+
477
+ ## 12. Удалённые экспорты
478
+
479
+ - `ECategory`, `EVolatility` — удалены полностью.
480
+ - `TMetrics`, `MetricsSchema`, `metricsPlugin` — удалены.
481
+ - `winCash`/`winCoins`/`metaPrivate`/`metaPublic` в `/play` — удалены (см. п. 7).
482
+ - `apiVersion`, `hitrate`, `category`, `hasBuy`, `hasGamble` — удалены из `/config`.
483
+
484
+ ## 13. Что появилось
485
+
486
+ - `EErrorCode`, `TErrorCode` — нумерация ошибок.
487
+ - `MetaSchema`, `TMeta` — схема `{ public, private }`.
488
+ - `ChecksumReplySchema`, `ChecksumRequestSchema`, `TChecksumReply`, `TChecksumRequest`.
489
+ - `TRtpParams = { rtpVersion: string }` — тип path-параметра.
490
+ - `FastifyError$Coded` — расширение `FastifyError` с полем `errorCode`.
491
+ - `httpError(message, statusCode, code)` — третий параметр для машинного кода ошибки.
492
+
493
+ ## 14. Чек-лист обновления игры
494
+
495
+ 1. `npm install @truelab/trueserver@0.0.5`.
496
+ 2. Привести `registerGameConfig({...})` к новой форме (см. п. 4).
497
+ 3. Переписать `handlers.register('config', ...)` под новый reply (см. п. 5).
498
+ 4. Переписать `handlers.register('play', ...)` — request/reply (см. пп. 6, 7). `meta` теперь объект, а не два string'а.
499
+ 5. В `handlers.register('client', ...)` убрать `ok: true`. При необходимости добавить поле `test`.
500
+ 6. Зарегистрировать `handlers.register('checksum', ...)` (если интегрируется проверка целостности).
501
+ 7. Если был `handlers.register('validate', ...)` с `ok: true` — теперь возвращать `{}`.
502
+ 8. Сменить все собственные ошибки на формат `{ status, code, message }` или использовать `httpError(..., code)`.
503
+ 9. Обновить тесты: пути с `/:rtpVersion`, методы GET для `/config`/`/client`/`/checksum`, GET для `/healthcheck`, новые имена полей.
504
+ 10. Обновить URL-ы на стороне RGS-конфига для каждой игры.
505
+
506
+ ## Вопросы
507
+
508
+ Регрессии и нерешённые ситуации — в Slack `#truelab-games-platform` или к командe RGS.