npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@omnitronix/service-client

v1.6.2

Published

Client library for registering services with Omnitronix RGS Service Registry

Downloads

973

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, allowInsecure for 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-client

Quick 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-Token header
  • 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

  1. Initial Registration: Client sends registration request with service details
  2. Retry Logic: Exponential backoff retry (1s, 2s, 4s, 8s, 16s, 30s max)
  3. Token Assignment: RGS assigns serviceId and instanceToken
  4. 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

  1. Signal Received: SIGTERM/SIGINT or admin shutdown
  2. Stop Heartbeats: Prevent new heartbeat attempts
  3. Deregister: Notify RGS of graceful shutdown
  4. 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 build

Test

npm test
npm run test:watch
npm run test:cov

Proto Generation

npm run proto:generate

License

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:

Plug & Play Service Guide

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.