npm.io
0.3.6 • Published 5d ago

@xemahq/identity-client

Licence
MIT
Version
0.3.6
Deps
0
Size
96 kB
Vulns
0
Weekly
4.4K

@xemahq/identity-client

TypeScript client library and NestJS module for integrating microservices with Identity API. Handles service registration, credential management, and JWT token lifecycle automatically.

Features

  • Automatic Service Registration — Background registration with identity-api on startup
  • Token Management — Automatic caching, refresh at 90% lifetime, concurrent request deduplication
  • NestJS Module — Drop-in IdentityBootstrapModule with async configuration
  • Standalone ClientIdentityApiClient for direct API calls outside NestJS
  • Non-Blocking Startup — Services start immediately; registration happens in the background
  • Exponential Backoff — Resilient retry logic (5s → 5min cap) if identity-api is unavailable
  • Pre-configured Credentials — Optional bypass for registration when credentials are provided via env vars
  • Zero Dependencies — Uses native fetch (no axios)

Installation

npm install @xemahq/identity-client

Requires .npmrc with GitHub Packages registry:

@xemahq:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}

Quick Start (NestJS)

1. Import the Module
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { IdentityBootstrapModule } from '@xemahq/identity-client';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    IdentityBootstrapModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        identityApiUrl: config.getOrThrow('IDENTITY_API_URL'),
        identityApiInternalToken: config.getOrThrow('IDENTITY_API_INTERNAL_TOKEN'),
        serviceName: 'my-service',
        serviceDisplayName: 'My Service',
      }),
      inject: [ConfigService],
    }),
  ],
})
export class AppModule {}
2. Environment Variables
IDENTITY_API_URL=http://identity-api:3053
IDENTITY_API_INTERNAL_TOKEN=your-identity-api-token
3. Use the Service
import { Injectable } from '@nestjs/common';
import { IdentityBootstrapService } from '@xemahq/identity-client';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';

@Injectable()
export class MyService {
  constructor(
    private readonly identityBootstrap: IdentityBootstrapService,
    private readonly httpService: HttpService,
  ) {}

  async callOtherService() {
    // Get a valid JWT access token (cached, auto-refreshed)
    const token = await this.identityBootstrap.getAccessToken();

    const response = await firstValueFrom(
      this.httpService.get('http://other-service/resource', {
        headers: { Authorization: `Bearer ${token}` },
      }),
    );

    return response.data;
  }
}

Integration with Event Hub

The primary use case is providing JWT tokens to @xemahq/event-hub-client via the tokenProvider option:

import { IdentityBootstrapModule, IdentityBootstrapService } from '@xemahq/identity-client';
import { EventHubModule } from '@xemahq/event-hub-client';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),

    // 1. Register with identity-api
    IdentityBootstrapModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService) => ({
        identityApiUrl: config.getOrThrow('IDENTITY_API_URL'),
        identityApiInternalToken: config.getOrThrow('IDENTITY_API_INTERNAL_TOKEN'),
        serviceName: 'my-service',
        serviceDisplayName: 'My Service',
      }),
      inject: [ConfigService],
    }),

    // 2. Register with event-hub, using identity-api for tokens
    EventHubModule.forRootAsync({
      imports: [ConfigModule],
      useFactory: (config: ConfigService, identityBootstrap: IdentityBootstrapService) => ({
        eventHubUrl: config.getOrThrow('EVENT_HUB_URL'),
        eventHubInternalToken: config.getOrThrow('EVENT_HUB_INTERNAL_TOKEN'),
        serviceId: 'my-service',
        serviceName: 'My Service',
        schemaVersion: '1.0.0',
        dataTypes: ['order'],
        tokenProvider: () => identityBootstrap.getAccessToken(),
      }),
      inject: [ConfigService, IdentityBootstrapService],
    }),
  ],
})
export class AppModule {}

API Reference

IdentityBootstrapModule

NestJS module for automatic service registration and token management.

IdentityBootstrapModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    // Required
    identityApiUrl: string;            // Base URL of identity-api
    identityApiInternalToken: string;  // Internal bearer token for registration
    serviceName: string;               // Service ID (becomes OAuth2 client_id)
    serviceDisplayName: string;        // Human-readable name

    // Optional
    serviceDescription?: string;       // Service description
    maxRegistrationAttempts?: number;   // Max retries (default: Infinity)
    baseRetryDelay?: number;           // Backoff base delay in ms (default: 5000)
    maxRetryDelay?: number;            // Backoff cap in ms (default: 300000)

    // Pre-configured credentials (skip auto-registration)
    clientId?: string;                 // OAuth2 client_id
    clientSecret?: string;             // OAuth2 client_secret
    tokenEndpoint?: string;            // Keycloak token endpoint URL
  }),
  inject: [ConfigService],
});

The module registers globally and exports IdentityBootstrapService.

IdentityBootstrapService

Manages service account lifecycle. Injected automatically when using IdentityBootstrapModule.

class IdentityBootstrapService {
  // Get a valid JWT access token (cached, auto-refreshed)
  getAccessToken(): Promise<string>;

  // Wait for registration to complete (with optional timeout)
  waitForReady(timeoutMs?: number): Promise<boolean>;

  // Check if credentials are available
  hasCredentials(): boolean;

  // Get the service's OAuth2 client_id
  getClientId(): string | null;
}

Startup behavior:

  1. If clientId, clientSecret, and tokenEndpoint are provided → uses them directly (no registration)
  2. Otherwise, starts background registration with identity-api:
    • Calls POST /internal/services/register with regenerateSecret: false
    • If the service already exists and no secret is returned, retries with regenerateSecret: true
    • Verifies credentials by obtaining an initial token
    • Retries with exponential backoff on failure
IdentityApiClient

Standalone HTTP client for direct identity-api calls. Does not require NestJS.

import { IdentityApiClient } from '@xemahq/identity-client';

const client = new IdentityApiClient({
  baseUrl: 'http://identity-api:3053',
  internalToken: 'your-token',
  timeoutMs: 10000, // optional, default: 10000
});

// Register a service and get credentials
const credentials = await client.registerService({
  name: 'my-service',
  displayName: 'My Service',
  serviceType: 'BACKEND',
  regenerateSecret: false,
});
// → { clientId, clientSecret, tokenEndpoint, roles }

// Full response with metadata
const response = await client.registerServiceFull({
  name: 'my-service',
  displayName: 'My Service',
  serviceType: 'BACKEND',
  regenerateSecret: false,
});
// → { id, clientSecret, tokenEndpoint, roles, alreadyExisted, secretRegenerated }

// Verify organization membership
const membership = await client.verifyMembership(userId, orgId);
// → { isMember, role }

// Create system organization (idempotent)
const org = await client.ensureSystemOrg({
  name: 'system',
  displayName: 'System Organization',
});

// Get system organization by name
const systemOrg = await client.getSystemOrg('system');
Error Handling

All client methods throw IdentityApiError on failure:

import { IdentityApiError } from '@xemahq/identity-client';

try {
  await client.registerService({ ... });
} catch (error) {
  if (error instanceof IdentityApiError) {
    console.error(error.statusCode); // HTTP status
    console.error(error.message);    // Error message
  }
}

Pre-configured Credentials

For production multi-replica deployments, you can skip auto-registration by providing credentials via environment variables. This avoids the secret regeneration race condition when multiple pods start simultaneously.

IdentityBootstrapModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: (config: ConfigService) => ({
    identityApiUrl: config.getOrThrow('IDENTITY_API_URL'),
    identityApiInternalToken: config.getOrThrow('IDENTITY_API_INTERNAL_TOKEN'),
    serviceName: 'my-service',
    serviceDisplayName: 'My Service',
    // Pre-configured credentials — skip registration entirely
    clientId: config.get('SERVICE_CLIENT_ID'),
    clientSecret: config.get('SERVICE_CLIENT_SECRET'),
    tokenEndpoint: config.get('SERVICE_TOKEN_ENDPOINT'),
  }),
  inject: [ConfigService],
});

When all three (clientId, clientSecret, tokenEndpoint) are provided, IdentityBootstrapService validates them on startup and enters ready state immediately. If validation fails, the module throws (hard fail).

Exports

// Classes
export { IdentityApiClient } from './client';
export { IdentityApiError } from './client';

// NestJS module
export { IdentityBootstrapModule } from './nestjs';
export { IdentityBootstrapService } from './nestjs';
export { IDENTITY_BOOTSTRAP_CONFIG } from './nestjs';

// Types
export type {
  IdentityApiClientOptions,
  IdentityBootstrapConfig,
  IdentityBootstrapModuleAsyncOptions,
  RegisterServiceRequest,
  RegisterServiceResponse,
  ServiceCredentials,
  ServiceType,
  VerifyMembershipParams,
  VerifyMembershipResponse,
  EnsureSystemOrgRequest,
  Organization,
  ApiEnvelope,
} from './types';

License

MIT

Keywords