@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
IdentityBootstrapModulewith async configuration - Standalone Client —
IdentityApiClientfor 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-clientRequires .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:
- If
clientId,clientSecret, andtokenEndpointare provided → uses them directly (no registration) - Otherwise, starts background registration with identity-api:
- Calls
POST /internal/services/registerwithregenerateSecret: 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
- Calls
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