@omnitronix/game-engine-sdk 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +483 -0
- package/dist/bootstrap/index.d.ts +2 -0
- package/dist/bootstrap/index.d.ts.map +1 -1
- package/dist/bootstrap/index.js +4 -2
- package/dist/common/api-docs/setup-documentation.d.ts.map +1 -1
- package/dist/common/api-docs/setup-documentation.js +4 -0
- package/dist/common/error-handling/all-exceptions.filter.js +1 -1
- package/dist/common/logger/logging.middleware.d.ts.map +1 -1
- package/dist/common/logger/logging.middleware.js +25 -7
- package/dist/esm/bootstrap/index.js +4 -2
- package/dist/esm/common/api-docs/setup-documentation.js +4 -0
- package/dist/esm/common/error-handling/all-exceptions.filter.js +1 -1
- package/dist/esm/common/logger/logging.middleware.js +25 -7
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# @omnitronix/game-engine-sdk
|
|
2
|
+
|
|
3
|
+
Shared NestJS infrastructure SDK for Omnitronix game engine services. Provides health checks, gRPC adapters, auto-registration, metrics, logging, secrets management, error handling, and application bootstrap — eliminating duplicated infrastructure code across game engine services.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @omnitronix/game-engine-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependencies** (your service must provide these):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @nestjs/common @nestjs/core @nestjs/config @nestjs/platform-express @omnitronix/game-engine-contract reflect-metadata rxjs
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Optional peer dependencies:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @aws-sdk/client-secrets-manager # For AWS Secrets Manager
|
|
21
|
+
npm install @nestjs/swagger # For Swagger API docs
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
A minimal game engine service using the SDK:
|
|
27
|
+
|
|
28
|
+
### `main.ts`
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { createGameEngineApp } from '@omnitronix/game-engine-sdk';
|
|
32
|
+
import { AppModule } from './app.module';
|
|
33
|
+
|
|
34
|
+
async function bootstrap() {
|
|
35
|
+
await createGameEngineApp({
|
|
36
|
+
serviceName: 'My Game Engine Service',
|
|
37
|
+
appModule: AppModule,
|
|
38
|
+
bootstrapOptions: { driver: 'database' },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
bootstrap();
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### `app.module.ts`
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { MiddlewareConsumer, Module } from '@nestjs/common';
|
|
49
|
+
import { ConfigModule } from '@nestjs/config';
|
|
50
|
+
import {
|
|
51
|
+
HealthModule,
|
|
52
|
+
LoggerModule,
|
|
53
|
+
LoggingMiddleware,
|
|
54
|
+
MetricsModule,
|
|
55
|
+
SecretsModule,
|
|
56
|
+
} from '@omnitronix/game-engine-sdk';
|
|
57
|
+
import { PrometheusModule } from '@willsoto/nestjs-prometheus';
|
|
58
|
+
|
|
59
|
+
import { MyGameEngineModule } from './modules/my-game-engine.module';
|
|
60
|
+
|
|
61
|
+
@Module({})
|
|
62
|
+
export class AppModule {
|
|
63
|
+
configure(consumer: MiddlewareConsumer) {
|
|
64
|
+
consumer.apply(LoggingMiddleware).forRoutes('*');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static register() {
|
|
68
|
+
return {
|
|
69
|
+
module: AppModule,
|
|
70
|
+
imports: [
|
|
71
|
+
ConfigModule.forRoot({ isGlobal: true, envFilePath: '.env' }),
|
|
72
|
+
SecretsModule.register(),
|
|
73
|
+
LoggerModule,
|
|
74
|
+
PrometheusModule.register({
|
|
75
|
+
path: '/metrics',
|
|
76
|
+
defaultMetrics: { enabled: true },
|
|
77
|
+
}),
|
|
78
|
+
HealthModule,
|
|
79
|
+
MetricsModule,
|
|
80
|
+
MyGameEngineModule,
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### `my-game-engine.module.ts`
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
import { Module } from '@nestjs/common';
|
|
91
|
+
import {
|
|
92
|
+
GAME_ENGINE,
|
|
93
|
+
GAME_ENGINE_IN_PORT,
|
|
94
|
+
GAME_ENGINE_REGISTRY_OUT_PORT,
|
|
95
|
+
GameEngineAutoRegisterService,
|
|
96
|
+
GameEngineGrpcInAdapter,
|
|
97
|
+
GameEngineRegistryGrpcOutAdapter,
|
|
98
|
+
MetricsModule,
|
|
99
|
+
} from '@omnitronix/game-engine-sdk';
|
|
100
|
+
import { MyGameEngine } from '@omnitronix/my-game-engine';
|
|
101
|
+
|
|
102
|
+
import { MyGameEngineService } from './services/game-engine.service';
|
|
103
|
+
|
|
104
|
+
@Module({
|
|
105
|
+
imports: [MetricsModule],
|
|
106
|
+
providers: [
|
|
107
|
+
GameEngineGrpcInAdapter,
|
|
108
|
+
GameEngineAutoRegisterService,
|
|
109
|
+
MyGameEngineService,
|
|
110
|
+
{
|
|
111
|
+
provide: GAME_ENGINE_IN_PORT,
|
|
112
|
+
useExisting: MyGameEngineService,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
provide: GAME_ENGINE,
|
|
116
|
+
useClass: MyGameEngine,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
provide: GAME_ENGINE_REGISTRY_OUT_PORT,
|
|
120
|
+
useClass: GameEngineRegistryGrpcOutAdapter,
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
exports: [GameEngineGrpcInAdapter],
|
|
124
|
+
})
|
|
125
|
+
export class MyGameEngineModule {}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Package Hierarchy
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
@omnitronix/game-engine-contract -- Pure TS interfaces, zero framework deps
|
|
132
|
+
|
|
|
133
|
+
@omnitronix/game-engine-sdk -- NestJS modules, gRPC adapters, infrastructure (this package)
|
|
134
|
+
|
|
|
135
|
+
game-engine-service-* -- Individual game engine services
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The SDK depends on the contract. Game engine services depend on the SDK. Game engine packages (pure math/logic) are unaffected.
|
|
139
|
+
|
|
140
|
+
## Sub-path Exports
|
|
141
|
+
|
|
142
|
+
The SDK exposes sub-path exports for selective imports:
|
|
143
|
+
|
|
144
|
+
| Import Path | Contents |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `@omnitronix/game-engine-sdk` | Everything (recommended for services) |
|
|
147
|
+
| `@omnitronix/game-engine-sdk/common` | Logger, error handling, secrets, retry, metrics, API docs |
|
|
148
|
+
| `@omnitronix/game-engine-sdk/health` | Health check module, readiness probes, shutdown |
|
|
149
|
+
| `@omnitronix/game-engine-sdk/registration` | Auto-register service, registry gRPC adapter |
|
|
150
|
+
| `@omnitronix/game-engine-sdk/grpc` | gRPC in-adapter, Connect RPC router |
|
|
151
|
+
| `@omnitronix/game-engine-sdk/bootstrap` | `createGameEngineApp()` factory |
|
|
152
|
+
|
|
153
|
+
## Modules
|
|
154
|
+
|
|
155
|
+
### Bootstrap
|
|
156
|
+
|
|
157
|
+
Factory function that creates a fully configured NestJS application.
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
import { createGameEngineApp } from '@omnitronix/game-engine-sdk';
|
|
161
|
+
|
|
162
|
+
await createGameEngineApp({
|
|
163
|
+
serviceName: 'Happy Panda Game Engine Service',
|
|
164
|
+
appModule: AppModule,
|
|
165
|
+
bootstrapOptions: { driver: 'database' },
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**What it does:**
|
|
170
|
+
|
|
171
|
+
1. Creates NestJS app with custom logger and CORS enabled
|
|
172
|
+
2. Sets up global validation pipe (whitelist + transform)
|
|
173
|
+
3. Configures global exception filter (`ExceptionsFilter`)
|
|
174
|
+
4. Sets `/api` prefix (excludes `/health`, `/ready`, `/metrics`)
|
|
175
|
+
5. Registers Swagger docs at `/docs`
|
|
176
|
+
6. Creates a separate HTTP/2 cleartext (h2c) server for Connect RPC on `GRPC_PORT`
|
|
177
|
+
7. Handles graceful shutdown on `uncaughtException` / `unhandledRejection`
|
|
178
|
+
|
|
179
|
+
**Environment variables:**
|
|
180
|
+
|
|
181
|
+
| Variable | Default | Description |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `PORT` | `3000` | HTTP server port |
|
|
184
|
+
| `GRPC_PORT` | `50051` | gRPC (Connect RPC) server port |
|
|
185
|
+
|
|
186
|
+
### Health Module
|
|
187
|
+
|
|
188
|
+
Provides `/health` and `/ready` HTTP endpoints with system health checks.
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { HealthModule } from '@omnitronix/game-engine-sdk';
|
|
192
|
+
|
|
193
|
+
@Module({
|
|
194
|
+
imports: [HealthModule],
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Endpoints:**
|
|
199
|
+
|
|
200
|
+
| Endpoint | Purpose | Response |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `GET /health` | Liveness probe | `200` if healthy, `500` if unhealthy |
|
|
203
|
+
| `GET /ready` | Readiness probe | `200` if accepting traffic, `503` if draining |
|
|
204
|
+
|
|
205
|
+
**Health checks:**
|
|
206
|
+
- Heap memory usage (configurable threshold)
|
|
207
|
+
- System load average
|
|
208
|
+
|
|
209
|
+
**Environment variables:**
|
|
210
|
+
|
|
211
|
+
| Variable | Default | Description |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| `HEALTH_MAX_HEAP_USAGE_PERCENTAGE` | `98` | Max heap usage before unhealthy |
|
|
214
|
+
| `HEALTH_MAX_MEMORY_USAGE_PART` | `10` | Max load average divisor |
|
|
215
|
+
|
|
216
|
+
### Registration Module
|
|
217
|
+
|
|
218
|
+
Auto-registers the game engine with the RGS Game Engine Registry on startup.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import {
|
|
222
|
+
GAME_ENGINE_REGISTRY_OUT_PORT,
|
|
223
|
+
GameEngineAutoRegisterService,
|
|
224
|
+
GameEngineRegistryGrpcOutAdapter,
|
|
225
|
+
} from '@omnitronix/game-engine-sdk';
|
|
226
|
+
|
|
227
|
+
@Module({
|
|
228
|
+
providers: [
|
|
229
|
+
GameEngineAutoRegisterService,
|
|
230
|
+
{
|
|
231
|
+
provide: GAME_ENGINE_REGISTRY_OUT_PORT,
|
|
232
|
+
useClass: GameEngineRegistryGrpcOutAdapter,
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
})
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Registration happens asynchronously on module init (non-blocking). Uses exponential backoff retry (5 attempts, 1s initial delay, 2x multiplier, 30s max).
|
|
239
|
+
|
|
240
|
+
**Environment variables:**
|
|
241
|
+
|
|
242
|
+
| Variable | Required | Description |
|
|
243
|
+
|---|---|---|
|
|
244
|
+
| `HOST_NAME` | Yes | Hostname for the gRPC URL advertised to the registry |
|
|
245
|
+
| `GRPC_PORT` | No (default `50051`) | gRPC port advertised to the registry |
|
|
246
|
+
| `GAME_ENGINE_REGISTRY_GRPC_URL` | Yes | URL of the Game Engine Registry service |
|
|
247
|
+
|
|
248
|
+
### gRPC Module
|
|
249
|
+
|
|
250
|
+
Connect RPC adapter that exposes `ProcessCommand` and `GetGameEngineInfo` RPCs.
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
import {
|
|
254
|
+
GAME_ENGINE_IN_PORT,
|
|
255
|
+
GameEngineGrpcInAdapter,
|
|
256
|
+
} from '@omnitronix/game-engine-sdk';
|
|
257
|
+
|
|
258
|
+
@Module({
|
|
259
|
+
providers: [
|
|
260
|
+
GameEngineGrpcInAdapter,
|
|
261
|
+
{
|
|
262
|
+
provide: GAME_ENGINE_IN_PORT,
|
|
263
|
+
useExisting: MyGameEngineService,
|
|
264
|
+
},
|
|
265
|
+
],
|
|
266
|
+
exports: [GameEngineGrpcInAdapter],
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
The gRPC adapter handles:
|
|
271
|
+
- Deserializing protobuf requests into domain commands
|
|
272
|
+
- Converting RNG seed types (`number` internally, `string` on wire)
|
|
273
|
+
- Error handling with state preservation on failure
|
|
274
|
+
|
|
275
|
+
### Logger Module
|
|
276
|
+
|
|
277
|
+
Structured Winston-based logger implementing NestJS `LoggerService`.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
import { Logger, LoggerModule, LoggingMiddleware } from '@omnitronix/game-engine-sdk';
|
|
281
|
+
|
|
282
|
+
// As a module
|
|
283
|
+
@Module({ imports: [LoggerModule] })
|
|
284
|
+
|
|
285
|
+
// Direct usage
|
|
286
|
+
const logger = new Logger('MyService');
|
|
287
|
+
logger.log('Processing command', { commandId: '123' });
|
|
288
|
+
logger.error('Failed to process', error);
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Features:**
|
|
292
|
+
- JSON or pretty-print output (configurable)
|
|
293
|
+
- HTTP request/response logging middleware
|
|
294
|
+
- Sensitive field sanitization (`password`, `token` redacted)
|
|
295
|
+
- `DomainException`-aware (includes `errorCode` in logs)
|
|
296
|
+
|
|
297
|
+
**Environment variables:**
|
|
298
|
+
|
|
299
|
+
| Variable | Default | Description |
|
|
300
|
+
|---|---|---|
|
|
301
|
+
| `LOG_FORMAT` | `json` | Log format: `json` or `pretty` |
|
|
302
|
+
|
|
303
|
+
### Secrets Module
|
|
304
|
+
|
|
305
|
+
Dynamic module for secrets management with local and AWS providers.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { SecretsModule, SECRETS_PROVIDER, SecretsProvider } from '@omnitronix/game-engine-sdk';
|
|
309
|
+
|
|
310
|
+
// In module
|
|
311
|
+
@Module({ imports: [SecretsModule.register()] })
|
|
312
|
+
|
|
313
|
+
// In service
|
|
314
|
+
@Injectable()
|
|
315
|
+
class MyService {
|
|
316
|
+
constructor(@Inject(SECRETS_PROVIDER) private secrets: SecretsProvider) {}
|
|
317
|
+
|
|
318
|
+
async getApiKey() {
|
|
319
|
+
return this.secrets.get('API_KEY');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
**Environment variables:**
|
|
325
|
+
|
|
326
|
+
| Variable | Default | Description |
|
|
327
|
+
|---|---|---|
|
|
328
|
+
| `LOCAL_SECRETS` | `false` | `true` to load from `.secrets.env` file |
|
|
329
|
+
| `AWS_SECRET_ID` | - | AWS Secrets Manager secret ID |
|
|
330
|
+
| `AWS_REGION` | `eu-central-1` | AWS region |
|
|
331
|
+
|
|
332
|
+
### Metrics Module
|
|
333
|
+
|
|
334
|
+
Prometheus metrics collection using `prom-client` and `@willsoto/nestjs-prometheus`.
|
|
335
|
+
|
|
336
|
+
```typescript
|
|
337
|
+
import { MetricsModule, METRICS_PORT, MetricsPort } from '@omnitronix/game-engine-sdk';
|
|
338
|
+
|
|
339
|
+
@Module({ imports: [MetricsModule] })
|
|
340
|
+
|
|
341
|
+
// In a service
|
|
342
|
+
@Injectable()
|
|
343
|
+
class MyMetricsHandler {
|
|
344
|
+
constructor(@Inject(METRICS_PORT) private metrics: MetricsPort) {}
|
|
345
|
+
|
|
346
|
+
recordCommand(duration: number) {
|
|
347
|
+
this.metrics.getHistogram('game_engine_command_duration_ms').observe(duration);
|
|
348
|
+
this.metrics.getCounter('game_engine_commands_total').inc();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
**Pre-configured metrics:** HTTP request duration, WebSocket connections, session tracking, wallet operations, game engine commands, RNG buffer stats, database connection pool, event store operations, and more (40+ metrics).
|
|
354
|
+
|
|
355
|
+
### Error Handling
|
|
356
|
+
|
|
357
|
+
Global exception filter and domain exception hierarchy.
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
import {
|
|
361
|
+
DomainException,
|
|
362
|
+
InternalErrorCode,
|
|
363
|
+
ExceptionsFilter,
|
|
364
|
+
} from '@omnitronix/game-engine-sdk';
|
|
365
|
+
|
|
366
|
+
// Throw domain exceptions
|
|
367
|
+
throw new DomainException(
|
|
368
|
+
InternalErrorCode.GAME_NOT_FOUND,
|
|
369
|
+
'Game engine not registered',
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
// The ExceptionsFilter (auto-configured by createGameEngineApp) maps
|
|
373
|
+
// InternalErrorCode to HTTP status codes:
|
|
374
|
+
// 400 - BAD_REQUEST, INVALID_*
|
|
375
|
+
// 401 - UNAUTHORIZED
|
|
376
|
+
// 403 - FORBIDDEN
|
|
377
|
+
// 404 - *_NOT_FOUND
|
|
378
|
+
// 409 - CONCURRENT_MODIFICATION
|
|
379
|
+
// 422 - INSUFFICIENT_FUNDS
|
|
380
|
+
// 503 - SERVICE_UNAVAILABLE, HEALTH_CHECK_FAILED
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
### Retry Policies
|
|
384
|
+
|
|
385
|
+
Configurable retry with exponential backoff.
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import { RetryPolicy, RetryPolicies } from '@omnitronix/game-engine-sdk';
|
|
389
|
+
|
|
390
|
+
// Pre-built policies
|
|
391
|
+
const result = await RetryPolicies.externalApi().execute(() =>
|
|
392
|
+
fetch('https://api.example.com/data'),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
// Custom policy
|
|
396
|
+
const policy = new RetryPolicy({
|
|
397
|
+
maxAttempts: 5,
|
|
398
|
+
delayMs: 200,
|
|
399
|
+
backoffMultiplier: 2,
|
|
400
|
+
maxDelayMs: 5000,
|
|
401
|
+
retryableErrors: ['ECONNREFUSED', 'TIMEOUT'],
|
|
402
|
+
operationName: 'fetchData',
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
await policy.execute(() => riskyOperation());
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Built-in policies:**
|
|
409
|
+
|
|
410
|
+
| Policy | Attempts | Initial Delay | Backoff | Max Delay |
|
|
411
|
+
|---|---|---|---|---|
|
|
412
|
+
| `walletOperation()` | 5 | 200ms | 2x | 3s |
|
|
413
|
+
| `externalApi()` | 3 | 100ms | 2x | 1s |
|
|
414
|
+
| `database()` | 3 | 50ms | 2x | 500ms |
|
|
415
|
+
|
|
416
|
+
## Core Types
|
|
417
|
+
|
|
418
|
+
| Type | Description |
|
|
419
|
+
|---|---|
|
|
420
|
+
| `GameEngine<P, S, O>` | Main engine interface (re-exported from contract) |
|
|
421
|
+
| `GameEngineInfo` | Engine metadata (code, version, RTP, type, name, provider) |
|
|
422
|
+
| `GameActionCommand<T>` | Command sent to the engine |
|
|
423
|
+
| `CommandProcessingResult<P, S, O>` | Result of processing a command |
|
|
424
|
+
| `RngOutcome` | Collection of RNG call records |
|
|
425
|
+
| `GameEngineInPort` | Port interface for the engine service layer |
|
|
426
|
+
|
|
427
|
+
## Dependency Injection Tokens
|
|
428
|
+
|
|
429
|
+
| Token | Type | Description |
|
|
430
|
+
|---|---|---|
|
|
431
|
+
| `GAME_ENGINE` | `GameEngine` | The game engine implementation |
|
|
432
|
+
| `GAME_ENGINE_IN_PORT` | `GameEngineInPort` | Engine service (business logic layer) |
|
|
433
|
+
| `GAME_ENGINE_REGISTRY_OUT_PORT` | `GameEngineRegistryOutPort` | Registry client adapter |
|
|
434
|
+
| `HEALTH_IN_PORT` | `HealthInPort` | Health check service |
|
|
435
|
+
| `METRICS_PORT` | `MetricsPort` | Prometheus metrics service |
|
|
436
|
+
| `SECRETS_PROVIDER` | `SecretsProvider` | Secrets management provider |
|
|
437
|
+
| `SYSTEM_HEALTH_OUT_PORT` | `SystemHealthOutPort` | System health adapter |
|
|
438
|
+
| `SHUTDOWN_OUT_PORT` | `ShutdownOutPort` | Graceful shutdown handler |
|
|
439
|
+
|
|
440
|
+
## Environment Variables
|
|
441
|
+
|
|
442
|
+
Complete reference of all environment variables used by the SDK:
|
|
443
|
+
|
|
444
|
+
| Variable | Default | Module | Description |
|
|
445
|
+
|---|---|---|---|
|
|
446
|
+
| `PORT` | `3000` | Bootstrap | HTTP server port |
|
|
447
|
+
| `GRPC_PORT` | `50051` | Bootstrap / Registration | gRPC server port |
|
|
448
|
+
| `HOST_NAME` | - | Registration | Hostname for registry registration |
|
|
449
|
+
| `GAME_ENGINE_REGISTRY_GRPC_URL` | - | Registration | Registry service URL |
|
|
450
|
+
| `LOG_FORMAT` | `json` | Logger | `json` or `pretty` |
|
|
451
|
+
| `LOCAL_SECRETS` | `false` | Secrets | Use local `.secrets.env` file |
|
|
452
|
+
| `AWS_SECRET_ID` | - | Secrets | AWS Secrets Manager secret ID |
|
|
453
|
+
| `AWS_REGION` | `eu-central-1` | Secrets | AWS region |
|
|
454
|
+
| `HEALTH_MAX_HEAP_USAGE_PERCENTAGE` | `98` | Health | Max heap usage threshold |
|
|
455
|
+
| `HEALTH_MAX_MEMORY_USAGE_PART` | `10` | Health | Max load average divisor |
|
|
456
|
+
|
|
457
|
+
## Proto Definitions
|
|
458
|
+
|
|
459
|
+
The SDK includes proto files and generated TypeScript code for:
|
|
460
|
+
|
|
461
|
+
- **`GameEngineService`** - `ProcessCommand`, `GetGameEngineInfo` RPCs
|
|
462
|
+
- **`GameEngineRegistryService`** - `RegisterGameEngine` RPC
|
|
463
|
+
|
|
464
|
+
Proto generation is handled at SDK build time. Consumers do **not** need to run `protoc`.
|
|
465
|
+
|
|
466
|
+
## Development
|
|
467
|
+
|
|
468
|
+
```bash
|
|
469
|
+
npm install # Install dependencies
|
|
470
|
+
npm run build # Build CJS + ESM
|
|
471
|
+
npm run test # Run tests
|
|
472
|
+
npm run lint # Lint with auto-fix
|
|
473
|
+
npm run format:check # Check formatting
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
## Related Packages
|
|
477
|
+
|
|
478
|
+
- [`@omnitronix/game-engine-contract`](https://www.npmjs.com/package/@omnitronix/game-engine-contract) - Pure TypeScript interfaces (zero framework deps)
|
|
479
|
+
- [`@omnitronix/service-client`](https://www.npmjs.com/package/@omnitronix/service-client) - Service client for non-game services
|
|
480
|
+
|
|
481
|
+
## License
|
|
482
|
+
|
|
483
|
+
PROPRIETARY
|
|
@@ -5,6 +5,8 @@ export interface GameEngineAppOptions {
|
|
|
5
5
|
bootstrapOptions?: {
|
|
6
6
|
driver: 'database' | 'in-memory';
|
|
7
7
|
};
|
|
8
|
+
/** Allowed CORS origins. Defaults to false (CORS disabled). */
|
|
9
|
+
corsOrigins?: string[] | boolean;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Creates and bootstraps a game engine NestJS application with
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bootstrap/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAsD,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAK1F,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,gBAAgB,CAAC,EAAE;QAAE,MAAM,EAAE,UAAU,GAAG,WAAW,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/bootstrap/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAsD,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAK1F,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC;IACrB,gBAAgB,CAAC,EAAE;QAAE,MAAM,EAAE,UAAU,GAAG,WAAW,CAAA;KAAE,CAAC;IACxD,+DAA+D;IAC/D,WAAW,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;CAClC;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,2DA0HtE"}
|
package/dist/bootstrap/index.js
CHANGED
|
@@ -51,7 +51,7 @@ const http2 = __importStar(require("http2"));
|
|
|
51
51
|
* Swagger docs, Connect RPC gRPC server, and graceful shutdown.
|
|
52
52
|
*/
|
|
53
53
|
async function createGameEngineApp(options) {
|
|
54
|
-
const { serviceName, appModule, bootstrapOptions } = options;
|
|
54
|
+
const { serviceName, appModule, bootstrapOptions, corsOrigins = false } = options;
|
|
55
55
|
const logger = new logger_1.Logger(serviceName);
|
|
56
56
|
const moduleArg = bootstrapOptions
|
|
57
57
|
? appModule.register
|
|
@@ -61,7 +61,9 @@ async function createGameEngineApp(options) {
|
|
|
61
61
|
const app = await core_1.NestFactory.create(moduleArg, {
|
|
62
62
|
logger: logger,
|
|
63
63
|
});
|
|
64
|
-
|
|
64
|
+
if (corsOrigins) {
|
|
65
|
+
app.enableCors({ origin: corsOrigins });
|
|
66
|
+
}
|
|
65
67
|
const configService = app.get(config_1.ConfigService);
|
|
66
68
|
const shutdownService = app.get(shutdown_out_port_1.SHUTDOWN_OUT_PORT);
|
|
67
69
|
let isShuttingDown = false;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup-documentation.d.ts","sourceRoot":"","sources":["../../../src/common/api-docs/setup-documentation.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAMlD,eAAO,MAAM,kBAAkB,GAC7B,KAAK,gBAAgB,CAAC,GAAG,CAAC,EAC1B,MAAM,MAAM,EACZ,cAAc,MAAM,
|
|
1
|
+
{"version":3,"file":"setup-documentation.d.ts","sourceRoot":"","sources":["../../../src/common/api-docs/setup-documentation.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAMlD,eAAO,MAAM,kBAAkB,GAC7B,KAAK,gBAAgB,CAAC,GAAG,CAAC,EAC1B,MAAM,MAAM,EACZ,cAAc,MAAM,kBA0BrB,CAAC"}
|
|
@@ -5,6 +5,10 @@ const swagger_1 = require("@nestjs/swagger");
|
|
|
5
5
|
const logger_1 = require("../logger/logger");
|
|
6
6
|
const logger = new logger_1.Logger('ApiDocs');
|
|
7
7
|
const setupDocumentation = async (app, port, serviceName) => {
|
|
8
|
+
if (process.env.NODE_ENV === 'production' && process.env.ENABLE_SWAGGER !== 'true') {
|
|
9
|
+
logger.log('Swagger docs disabled in production (set ENABLE_SWAGGER=true to override)');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
8
12
|
const config = new swagger_1.DocumentBuilder()
|
|
9
13
|
.setTitle(serviceName ? `${serviceName} API` : 'Game Engine API')
|
|
10
14
|
.setDescription('API documentation')
|
|
@@ -100,7 +100,7 @@ let ExceptionsFilter = ExceptionsFilter_1 = class ExceptionsFilter {
|
|
|
100
100
|
return this.buildResponsePayload({
|
|
101
101
|
status: common_1.HttpStatus.INTERNAL_SERVER_ERROR,
|
|
102
102
|
internalCode: internal_error_code_1.InternalErrorCode.INTERNAL_SERVER_ERROR,
|
|
103
|
-
message:
|
|
103
|
+
message: 'Internal server error',
|
|
104
104
|
path: request.url,
|
|
105
105
|
});
|
|
106
106
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logging.middleware.d.ts","sourceRoot":"","sources":["../../../src/common/logger/logging.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"logging.middleware.d.ts","sourceRoot":"","sources":["../../../src/common/logger/logging.middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA4B1D,qBACa,iBAAkB,YAAW,cAAc;IACtD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;;IAMhC,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY;CAkBpD"}
|
|
@@ -14,19 +14,37 @@ exports.LoggingMiddleware = void 0;
|
|
|
14
14
|
const common_1 = require("@nestjs/common");
|
|
15
15
|
const logger_1 = require("./logger");
|
|
16
16
|
const BLOCKED_ENDPOINTS = ['/api/races/active'];
|
|
17
|
+
const SENSITIVE_FIELDS = new Set([
|
|
18
|
+
'password', 'token', 'secret', 'authorization',
|
|
19
|
+
'apikey', 'api_key', 'accesstoken', 'refreshtoken',
|
|
20
|
+
'creditcard', 'ssn', 'private_key', 'privatekey',
|
|
21
|
+
]);
|
|
22
|
+
function sanitize(obj) {
|
|
23
|
+
if (obj === null || obj === undefined || typeof obj !== 'object')
|
|
24
|
+
return obj;
|
|
25
|
+
if (Array.isArray(obj))
|
|
26
|
+
return obj.map(sanitize);
|
|
27
|
+
const result = {};
|
|
28
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
29
|
+
if (SENSITIVE_FIELDS.has(key.toLowerCase())) {
|
|
30
|
+
result[key] = '***';
|
|
31
|
+
}
|
|
32
|
+
else if (typeof value === 'object' && value !== null) {
|
|
33
|
+
result[key] = sanitize(value);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
result[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
}
|
|
17
41
|
let LoggingMiddleware = LoggingMiddleware_1 = class LoggingMiddleware {
|
|
18
42
|
constructor() {
|
|
19
43
|
this.logger = new logger_1.Logger(LoggingMiddleware_1.name);
|
|
20
44
|
}
|
|
21
45
|
use(req, res, next) {
|
|
22
46
|
const { method, originalUrl, body } = req;
|
|
23
|
-
const sanitizedBody =
|
|
24
|
-
if (sanitizedBody.password) {
|
|
25
|
-
sanitizedBody.password = '***';
|
|
26
|
-
}
|
|
27
|
-
if (sanitizedBody.token) {
|
|
28
|
-
sanitizedBody.token = '***';
|
|
29
|
-
}
|
|
47
|
+
const sanitizedBody = sanitize(body);
|
|
30
48
|
res.on('finish', () => {
|
|
31
49
|
const isBlockedEndpoint = BLOCKED_ENDPOINTS.some(endpoint => originalUrl.includes(endpoint));
|
|
32
50
|
const isSuccessfulRequest = res.statusCode < 400;
|
|
@@ -15,7 +15,7 @@ import * as http2 from 'http2';
|
|
|
15
15
|
* Swagger docs, Connect RPC gRPC server, and graceful shutdown.
|
|
16
16
|
*/
|
|
17
17
|
export async function createGameEngineApp(options) {
|
|
18
|
-
const { serviceName, appModule, bootstrapOptions } = options;
|
|
18
|
+
const { serviceName, appModule, bootstrapOptions, corsOrigins = false } = options;
|
|
19
19
|
const logger = new Logger(serviceName);
|
|
20
20
|
const moduleArg = bootstrapOptions
|
|
21
21
|
? appModule.register
|
|
@@ -25,7 +25,9 @@ export async function createGameEngineApp(options) {
|
|
|
25
25
|
const app = await NestFactory.create(moduleArg, {
|
|
26
26
|
logger: logger,
|
|
27
27
|
});
|
|
28
|
-
|
|
28
|
+
if (corsOrigins) {
|
|
29
|
+
app.enableCors({ origin: corsOrigins });
|
|
30
|
+
}
|
|
29
31
|
const configService = app.get(ConfigService);
|
|
30
32
|
const shutdownService = app.get(SHUTDOWN_OUT_PORT);
|
|
31
33
|
let isShuttingDown = false;
|
|
@@ -2,6 +2,10 @@ import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
|
2
2
|
import { Logger } from '../logger/logger';
|
|
3
3
|
const logger = new Logger('ApiDocs');
|
|
4
4
|
export const setupDocumentation = async (app, port, serviceName) => {
|
|
5
|
+
if (process.env.NODE_ENV === 'production' && process.env.ENABLE_SWAGGER !== 'true') {
|
|
6
|
+
logger.log('Swagger docs disabled in production (set ENABLE_SWAGGER=true to override)');
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
5
9
|
const config = new DocumentBuilder()
|
|
6
10
|
.setTitle(serviceName ? `${serviceName} API` : 'Game Engine API')
|
|
7
11
|
.setDescription('API documentation')
|
|
@@ -97,7 +97,7 @@ let ExceptionsFilter = ExceptionsFilter_1 = class ExceptionsFilter {
|
|
|
97
97
|
return this.buildResponsePayload({
|
|
98
98
|
status: HttpStatus.INTERNAL_SERVER_ERROR,
|
|
99
99
|
internalCode: InternalErrorCode.INTERNAL_SERVER_ERROR,
|
|
100
|
-
message:
|
|
100
|
+
message: 'Internal server error',
|
|
101
101
|
path: request.url,
|
|
102
102
|
});
|
|
103
103
|
}
|
|
@@ -11,19 +11,37 @@ var LoggingMiddleware_1;
|
|
|
11
11
|
import { Injectable } from '@nestjs/common';
|
|
12
12
|
import { Logger } from './logger';
|
|
13
13
|
const BLOCKED_ENDPOINTS = ['/api/races/active'];
|
|
14
|
+
const SENSITIVE_FIELDS = new Set([
|
|
15
|
+
'password', 'token', 'secret', 'authorization',
|
|
16
|
+
'apikey', 'api_key', 'accesstoken', 'refreshtoken',
|
|
17
|
+
'creditcard', 'ssn', 'private_key', 'privatekey',
|
|
18
|
+
]);
|
|
19
|
+
function sanitize(obj) {
|
|
20
|
+
if (obj === null || obj === undefined || typeof obj !== 'object')
|
|
21
|
+
return obj;
|
|
22
|
+
if (Array.isArray(obj))
|
|
23
|
+
return obj.map(sanitize);
|
|
24
|
+
const result = {};
|
|
25
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
26
|
+
if (SENSITIVE_FIELDS.has(key.toLowerCase())) {
|
|
27
|
+
result[key] = '***';
|
|
28
|
+
}
|
|
29
|
+
else if (typeof value === 'object' && value !== null) {
|
|
30
|
+
result[key] = sanitize(value);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
result[key] = value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return result;
|
|
37
|
+
}
|
|
14
38
|
let LoggingMiddleware = LoggingMiddleware_1 = class LoggingMiddleware {
|
|
15
39
|
constructor() {
|
|
16
40
|
this.logger = new Logger(LoggingMiddleware_1.name);
|
|
17
41
|
}
|
|
18
42
|
use(req, res, next) {
|
|
19
43
|
const { method, originalUrl, body } = req;
|
|
20
|
-
const sanitizedBody =
|
|
21
|
-
if (sanitizedBody.password) {
|
|
22
|
-
sanitizedBody.password = '***';
|
|
23
|
-
}
|
|
24
|
-
if (sanitizedBody.token) {
|
|
25
|
-
sanitizedBody.token = '***';
|
|
26
|
-
}
|
|
44
|
+
const sanitizedBody = sanitize(body);
|
|
27
45
|
res.on('finish', () => {
|
|
28
46
|
const isBlockedEndpoint = BLOCKED_ENDPOINTS.some(endpoint => originalUrl.includes(endpoint));
|
|
29
47
|
const isSuccessfulRequest = res.statusCode < 400;
|