@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 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;CACzD;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,oBAAoB,2DAwHtE"}
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"}
@@ -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
- app.enableCors({ origin: true });
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,kBAqBrB,CAAC"}
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: exception.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;AAM1D,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;CAwBpD"}
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 = { ...body };
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
- app.enableCors({ origin: true });
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: exception.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 = { ...body };
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitronix/game-engine-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Shared NestJS infrastructure SDK for Omnitronix game engine services",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",