@omnitronix/service-client
v1.6.2
Published
Client library for registering services with Omnitronix RGS Service Registry
Downloads
973
Maintainers
Readme
@omnitronix/service-client
TypeScript/JavaScript client library for registering services with Omnitronix RGS Service Registry.
TRUE Plug & Play
Deploy a new service and it automatically appears in the backoffice - no configuration changes needed!
// Register your service - backoffice discovers it automatically!
const client = new ServiceRegistryClient({
rgsUrl: 'https://rgs:50051', // HTTPS enforced by default
serviceName: 'my-service',
version: '1.0.0',
httpUrl: 'https://my-service:3000', // How backoffice reaches you
menuItems: [{ label: 'Dashboard', path: '/s/my-service' }],
// allowInsecure: true, // Set for local dev with http://
});
await client.register();Zero backoffice changes required - your service registers its URL and menu items with RGS, and backoffice discovers them dynamically.
Features
- Auto-Registration: Register services on startup with retry logic
- TRUE Plug & Play: Backoffice discovers services from registry (no env vars!)
- Health Monitoring: Periodic heartbeats with jitter to prevent thundering herd
- Service Token Auth: Secure service-to-service authentication for internal APIs
- Internal API Client: Typed HTTP client for all RGS internal endpoints
- Service Discovery: Discover peer services with TTL-cached lookups
- Circuit Breaker: Fault tolerance for internal API calls
- Graceful Shutdown: Automatic deregistration on process termination
- DEGRADED Recovery: Background retry from failed state with configurable limits
- TLS Enforcement: HTTPS required in production,
allowInsecurefor local dev - Structured Logging: JSON logger for CloudWatch/ECS, configurable log levels
- AWS Ready: Environment auto-detection for ECS, EKS, and Lambda
- TypeScript: Full type safety and IntelliSense support
Installation
npm install @omnitronix/service-clientQuick Start
import { ServiceRegistryClient } from '@omnitronix/service-client';
const client = new ServiceRegistryClient({
rgsUrl: 'https://rgs:50051', // HTTPS required in production
serviceName: 'my-service',
version: '1.0.0',
httpUrl: 'https://my-service:3000',
// allowInsecure: true, // Set for local dev with http://
});
// Register on startup
await client.register();
// Handle graceful shutdown
process.on('SIGTERM', async () => {
await client.deregister();
process.exit(0);
});Configuration
Required Options
| Option | Type | Description |
|--------|------|-------------|
| rgsUrl | string | URL of the RGS Service Registry |
| serviceName | string | Unique name of your service |
| version | string | Semantic version (e.g., "1.0.0") |
Optional Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| httpUrl | string | "" | HTTP endpoint for your service |
| grpcUrl | string | "" | gRPC endpoint for your service |
| basePath | string | "/{serviceName}" | Base path for routing (maps to proto baseUrl) |
| menuItems | MenuItem[] | [] | Menu items for service UI |
| capabilities | string[] | [] | Service capabilities |
| metadata | Record<string, string> | {} | Additional metadata |
| heartbeatIntervalMs | number | 20000 | Heartbeat interval (ms) |
| maxRetries | number | 5 | Maximum retry attempts |
| autoStartHeartbeat | boolean | true | Auto-start heartbeat after registration |
| logger | Logger | console | Custom logger implementation |
| rgsHttpUrl | string | "" | RGS HTTP URL for internal API client |
| requestTimeoutMs | number | 30000 | Request timeout (ms) |
| maxHeartbeatFailures | number | 3 | Max consecutive heartbeat failures before re-registration |
| autoReRegister | boolean | true | Auto re-register on heartbeat exhaustion |
| allowInsecure | boolean | false | Allow http:// URLs (set true for local dev) |
| serviceType | string | "DEV_TOOLS" | DEV_TOOLS, PROMO_TOOLS, or ANALYTICS |
| logLevel | string | "info" | debug, info, warn, or error |
| extraMetadataKeys | string[] | [] | Additional allowed metadata keys |
| onHeartbeat | function | - | Callback after each successful heartbeat |
| heartbeatTimeoutMs | number | 5000 | Heartbeat-specific timeout (ms) |
| degradedRetryIntervalMs | number | 60000 | Background retry interval from DEGRADED state |
| maxDegradedRetries | number | 0 | Max DEGRADED retries (0 = unlimited) |
| keepAliveMs | number | 30000 | (Deprecated) Not wired; heartbeat acts as keep-alive |
| refreshOnHeartbeat | boolean | false | Refresh discovery cache on heartbeat |
| handleSignals | boolean | false | Auto-handle SIGTERM/SIGINT for shutdown |
MenuItem Interface
interface MenuItem {
label: string; // Display text
path: string; // Route path (must start with /)
icon?: string; // Icon identifier
requiredRoles?: string[]; // Required user roles
type?: 'LINK' | 'IFRAME' | 'MODAL'; // Display type
order?: number; // Sort order
id?: string; // Custom ID (auto-generated if omitted)
}Usage Examples
Basic Registration
import { ServiceRegistryClient } from '@omnitronix/service-client';
const client = new ServiceRegistryClient({
rgsUrl: process.env.RGS_URL || 'https://rgs:50051',
serviceName: 'dev-tools',
version: '1.0.0',
httpUrl: 'https://dev-tools:4000',
// allowInsecure: true, // Set for local dev with http://
});
await client.register();
console.log(`Registered with ID: ${client.getServiceId()}`);With Menu Items
const client = new ServiceRegistryClient({
rgsUrl: 'https://rgs:50051',
serviceName: 'promo-tools',
version: '1.0.0',
httpUrl: 'https://promo-tools:3000',
menuItems: [
{
label: 'Promotions',
path: '/promotions',
icon: 'gift',
requiredRoles: ['admin', 'promo-manager'],
type: 'LINK',
order: 1,
},
{
label: 'Campaigns',
path: '/campaigns',
icon: 'campaign',
requiredRoles: ['admin'],
type: 'LINK',
order: 2,
},
],
capabilities: ['promotions', 'campaigns'],
metadata: {
environment: 'production',
region: 'us-east-1',
},
});
await client.register();With Custom Logger
import { ServiceRegistryClient, Logger } from '@omnitronix/service-client';
import pino from 'pino';
const pinoLogger = pino();
const customLogger: Logger = {
info: (msg, ...args) => pinoLogger.info({ msg, args }),
warn: (msg, ...args) => pinoLogger.warn({ msg, args }),
error: (msg, ...args) => pinoLogger.error({ msg, args }),
debug: (msg, ...args) => pinoLogger.debug({ msg, args }),
};
const client = new ServiceRegistryClient({
rgsUrl: 'https://rgs:50051',
serviceName: 'my-service',
version: '1.0.0',
logger: customLogger,
});
await client.register();NestJS Integration
// app.module.ts
import { Module, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { ServiceRegistryClient } from '@omnitronix/service-client';
@Module({
// ... your module config
})
export class AppModule implements OnModuleInit, OnModuleDestroy {
private serviceClient: ServiceRegistryClient;
constructor() {
this.serviceClient = new ServiceRegistryClient({
rgsUrl: process.env.RGS_URL,
serviceName: process.env.SERVICE_NAME,
version: process.env.SERVICE_VERSION,
httpUrl: process.env.HTTP_URL,
grpcUrl: process.env.GRPC_URL,
// allowInsecure: true, // Set for local dev with http:// URLs
});
}
async onModuleInit() {
await this.serviceClient.register();
console.log('Service registered with RGS');
}
async onModuleDestroy() {
await this.serviceClient.deregister();
console.log('Service deregistered from RGS');
}
}Express Integration
import express from 'express';
import { ServiceRegistryClient } from '@omnitronix/service-client';
const app = express();
const PORT = process.env.PORT || 3000;
const client = new ServiceRegistryClient({
rgsUrl: process.env.RGS_URL,
serviceName: 'my-express-service',
version: '1.0.0',
httpUrl: `http://my-express-service:${PORT}`,
allowInsecure: true, // Required when using http:// URLs (local dev only)
});
// Start server
const server = app.listen(PORT, async () => {
console.log(`Server running on port ${PORT}`);
// Register with service registry
await client.register();
console.log('Registered with RGS');
});
// Graceful shutdown
process.on('SIGTERM', async () => {
console.log('SIGTERM received, shutting down gracefully');
await client.deregister();
server.close(() => {
console.log('Server closed');
process.exit(0);
});
});Subpath Exports
Tree-shake by importing only the modules you need:
// Full client (default)
import { ServiceRegistryClient } from '@omnitronix/service-client';
// Types only (zero runtime cost)
import type { ServiceClientConfig, Logger, RegistrationResponse } from '@omnitronix/service-client/types';
// Service discovery only
import { ServiceDiscovery } from '@omnitronix/service-client/discovery';
// Internal API client only
import { InternalApiClient } from '@omnitronix/service-client/internal-api';API Reference
ServiceRegistryClient
constructor(config: ServiceClientConfig)
Create a new service registry client instance. Validates config and initializes subsystems.
async register(signal?: AbortSignal): Promise<RegistrationResponse>
Register the service with RGS. Returns registration details including serviceId and instanceToken. Pass an optional AbortSignal to cancel the retry loop externally.
Throws: Error if registration fails after all retries or is aborted.
startHeartbeat(intervalMs?: number): void
Start sending periodic heartbeats. Called automatically after registration if autoStartHeartbeat is true.
stopHeartbeat(): void
Stop sending heartbeats.
async deregister(): Promise<void>
Deregister the service from RGS. Called during graceful shutdown.
async shutdown(): Promise<void>
Graceful shutdown: stop heartbeat, deregister, destroy. Use in SIGTERM handlers.
waitForReady(timeoutMs?: number): Promise<void>
Wait for the service to reach ACTIVE state. Useful for services that need registration before making API calls.
isActive(): boolean
Check if service is currently registered and active.
isRegisteredState(): boolean
Check if service has a valid registration (ACTIVE or RECONNECTING).
isDegraded(): boolean
Check if service is in degraded mode.
getState(): ServiceState
Get current service state.
getServiceId(): string | undefined
Get the service ID assigned during registration.
getInstanceToken(): string | undefined
Get the instance token assigned during registration.
getServiceToken(): string | undefined
Get the service token in serviceId:instanceToken format for HTTP auth.
get internalApi: InternalApiClient
Get the internal API client. Requires rgsHttpUrl in config.
get discovery: ServiceDiscovery
Get the service discovery client. Requires rgsHttpUrl in config.
onShutdown(handler: (reason?: string) => void): void
Subscribe to shutdown events from RGS admin.
onStateChange(handler: (event: StateChangeEvent) => void): void
Subscribe to state changes.
destroy(): void
Destroy the client, stopping heartbeats and cleaning up all resources.
Internal API Client
When rgsHttpUrl is configured, the client provides a typed HTTP client for all RGS internal endpoints:
const client = new ServiceRegistryClient({
rgsUrl: 'https://rgs:50051',
rgsHttpUrl: 'https://rgs:3000',
serviceName: 'dev-tools',
version: '1.0.0',
});
await client.register();
// Typed API calls with automatic token injection
const games = await client.internalApi.getGames();
const session = await client.internalApi.getSession('session-id');
const players = await client.internalApi.getPlayers({ page: 1 });The internal API client handles:
- Auto token injection via
X-Service-Tokenheader - 401 retry — re-registers on auth failure, then retries
- Circuit breaker — prevents cascading failures
- Request timeouts via
AbortController
InternalApiClient Method Reference
| Category | Method | Returns | Description |
|----------|--------|---------|-------------|
| Games | getGames() | InternalGamesResponse | Fetch game catalogue |
| Players | getPlayers(query?) | InternalPlayersResponse | List players with filtering/pagination |
| | getPlayer(id, operatorCode?) | InternalPlayerDto | Fetch single player by ID |
| Sessions | getSessions(query?) | InternalSessionsResponse | List sessions with filtering/pagination |
| | getSession(sessionId) | InternalSessionDto | Fetch single session |
| | getSessionMetrics(sessionId) | InternalSessionMetrics | Session metrics (bets, wins, duration) |
| | getSessionEvents(sessionId, query?) | SessionEventsResponse | Paginated session event log |
| | terminateSession(sessionId, body) | ActionResponse | Terminate an active session |
| | triggerRealityCheck(sessionId) | ActionResponse | Trigger reality check prompt |
| | confirmRealityCheck(sessionId) | ActionResponse | Confirm reality check acknowledged |
| Free Rounds | getFreeRounds(query?) | FreeRoundsResponse | List free round grants |
| | grantFreeRounds(body) | GrantFreeRoundsResponse | Grant free rounds to players |
| Rounds | getRounds(query?) | RoundLogsResponse | List round logs |
| | getSessionRounds(sessionId, query?) | RoundLogsResponse | Round logs for a specific session |
| Debug | triggerDebug(body) | DebugTriggerResponse | Trigger debug command (admin, audit-logged) |
| | getDebugCommands(gameCode) | DebugCommandsResponse | Available debug commands for a game |
| | getPendingDebug(sessionId) | PendingDebugResponse | Pending debug commands for a session |
| | getDebugAudit(auditId) | DebugAuditDto | Audit trail for a debug execution |
| Services | getServices() | InternalServicesResponse | List all registered services |
| Utilities | circuitState | string | Current circuit breaker state |
| | getCircuitBreaker() | CircuitBreaker \| null | Access circuit breaker for event listening |
| | destroy() | void | Clean up resources |
Service Discovery
Discover peer services registered with the same RGS:
const services = await client.discovery.getRegisteredServices();
const devToolsUrl = await client.discovery.getServiceUrl('dev-tools');AWS Deployment
Auto-detect AWS environment metadata for ECS, EKS, and Lambda:
import {
ServiceRegistryClient,
createJsonLogger,
buildAwsMetadata,
detectComputePlatform,
isAwsEnvironment,
} from '@omnitronix/service-client';
const client = new ServiceRegistryClient({
rgsUrl: 'https://rgs:50051',
serviceName: 'my-service',
version: '1.0.0',
// Structured JSON logs for CloudWatch
logger: createJsonLogger('info', 'my-service'),
// Auto-detect region, hostname, cluster, namespace, taskArn, podUid, etc.
metadata: buildAwsMetadata(),
// Enable graceful shutdown for containerized environments
handleSignals: true,
});
// Check compute platform: 'ecs' | 'eks' | 'lambda' | 'ec2' | 'unknown'
console.log('Platform:', detectComputePlatform());Detected AWS Metadata
| Field | Source | Platform |
|-------|--------|----------|
| region | AWS_REGION / AWS_DEFAULT_REGION | All |
| hostname | HOSTNAME | ECS, EKS |
| cluster | ECS_CLUSTER / KUBE_CLUSTER | ECS, EKS |
| namespace | KUBE_NAMESPACE / POD_NAMESPACE | EKS |
| taskArn | ECS_TASK_ARN | ECS |
| lambdaFunction | AWS_LAMBDA_FUNCTION_NAME | Lambda |
| podUid | KUBE_POD_UID / POD_UID | EKS |
| computePlatform | Auto-detected | All |
| environment | NODE_ENV / ENVIRONMENT | All |
Structured Logging
Built-in structured JSON logger for log aggregation:
import { createJsonLogger } from '@omnitronix/service-client';
// Outputs one JSON line per log entry to stdout/stderr
const logger = createJsonLogger('info', 'my-service');
// {"timestamp":"2026-02-01T12:00:00.000Z","level":"info","service":"my-service","message":"..."}Or use the configurable console logger:
const client = new ServiceRegistryClient({
// ...
logLevel: 'debug', // debug | info | warn | error
});Service Token Authentication
When your service needs to call internal RGS APIs (or other internal services), use the service token for authentication.
Getting the Service Token
After registration, you can retrieve a formatted service token for HTTP authentication:
// In your service wrapper
class ServiceRegistryWrapper {
private client: ServiceRegistryClient;
/**
* Get the service token for internal service-to-service auth
* Returns format: "serviceId:instanceToken"
*/
getServiceToken(): string | undefined {
const serviceId = this.client.getServiceId();
const instanceToken = this.client.getInstanceToken();
if (!serviceId || !instanceToken) {
return undefined;
}
return `${serviceId}:${instanceToken}`;
}
}Using the Token in HTTP Requests
Include the token in the X-Service-Token header:
const serviceToken = serviceRegistry.getServiceToken();
const response = await fetch('http://rgs:3000/api/internal/games', {
headers: {
'X-Service-Token': serviceToken,
'Content-Type': 'application/json',
},
});Token Format
X-Service-Token: {serviceId}:{instanceToken}
Example:
X-Service-Token: promo-tools:abc123def456...Security Considerations
- Token Rotation: Tokens are assigned per registration and rotate on service restart
- bcrypt Hashed: Tokens are bcrypt-hashed in the service registry database
- Network + Application: Combine with VPC/network isolation for defense in depth
- Audit Trail: RGS logs which service made each internal API call
Error Handling
If the service is not registered, getServiceToken() returns undefined. Handle this case:
const token = serviceRegistry.getServiceToken();
if (!token) {
// Fallback behavior or throw error
throw new Error('Service not registered, cannot make internal API call');
}State Machine
The client uses a state machine to track the service lifecycle:
UNREGISTERED → REGISTERING → ACTIVE → DEREGISTERED
↓
RECONNECTING → ACTIVE | DEGRADED
↓
(background retry)| State | Meaning |
|-------|---------|
| UNREGISTERED | Initial state, not yet registered |
| REGISTERING | Registration in progress |
| ACTIVE | Registered and heartbeating |
| RECONNECTING | Heartbeat failed, attempting recovery |
| DEGRADED | All recovery attempts failed, background retry |
| DEREGISTERED | Gracefully shut down |
client.onStateChange((event) => {
console.log(`${event.from} → ${event.to}: ${event.reason}`);
});How It Works
Registration Flow
- Initial Registration: Client sends registration request with service details
- Retry Logic: Exponential backoff retry (1s, 2s, 4s, 8s, 16s, 30s max)
- Token Assignment: RGS assigns
serviceIdandinstanceToken - Heartbeat Start: Automatic heartbeat begins (default 20s interval)
Heartbeat Mechanism
- Periodic health checks sent to RGS
- Maintains service as "ACTIVE" in registry
- Detects admin-initiated shutdown signals
- Auto-recovery on transient failures
Graceful Shutdown
- Signal Received: SIGTERM/SIGINT or admin shutdown
- Stop Heartbeats: Prevent new heartbeat attempts
- Deregister: Notify RGS of graceful shutdown
- Exit: Allow process to terminate cleanly
Error Handling
The client handles errors gracefully:
- Registration Failures: Exponential backoff retry
- Heartbeat Failures: Logged but don't crash the service
- Deregistration Failures: Logged during shutdown (non-blocking)
- Admin Shutdown: Emits SIGTERM for graceful process termination
Development
Build
npm run buildTest
npm test
npm run test:watch
npm run test:covProto Generation
npm run proto:generateLicense
PROPRIETARY - Omnitronix
Guides
Creating Plug & Play Services
For a complete guide on creating services that automatically appear in the backoffice sidebar with their own embedded UI, see:
This guide covers:
- Architecture overview
- Menu item configuration
- Frontend integration
- Docker setup
- Troubleshooting
Internal API Authentication
For secure service-to-service API calls, see:
Internal API Authentication Guide
This guide covers:
- Getting and using service tokens
- Complete NestJS implementation example
- Available internal APIs
- Security features and error handling
- Checklist for new services
Changelog
See CHANGELOG.md for version history and changes.
Support
For issues and questions, contact the Omnitronix RGS team.
