# @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 Client** — `IdentityApiClient` 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

```bash
npm install @xemahq/identity-client
```

The package is published to the public npm registry — no additional registry
configuration is required.

## Quick Start (NestJS)

### 1. Import the Module

```typescript
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

```env
IDENTITY_API_URL=http://identity-api:3053
IDENTITY_API_INTERNAL_TOKEN=your-identity-api-token
```

### 3. Use the Service

```typescript
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:

```typescript
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.

```typescript
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`.

```typescript
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.

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

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

**Which credential a call presents.** With only `internalToken` configured, every
call presents it. That is fine for routes that do not bind a decision to caller
identity, but in a deployment where several services share one secret it proves
a realm rather than a caller, so routes that DO bind on caller identity reject
it. To be identified as yourself, supply `serviceTokenProvider` — a function
returning this service's own access token:

```typescript
const client = new IdentityApiClient({
  baseUrl: 'http://identity-api:3053',
  internalToken: 'your-token',
  serviceTokenProvider: async () => myServiceAccessToken(),
});
```

Registration and credential recovery stay pinned to `internalToken` — they are
the calls that grant the service its own client, so they never consult the
provider. On every other call, a provider that yields no token (or throws)
fails the call with a local error naming the missing credential; it is NOT
downgraded to `internalToken`. Pass `allowInternalTokenFallback: true` to accept
the weaker credential deliberately.

```typescript

// 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:

```typescript
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.

```typescript
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

```typescript
// 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

Source-available under the Xema Business Source License 1.1 (BSL-1.1) —
use-but-not-sell; each released version converts to the Mozilla Public License
2.0 (MPL-2.0) four years after its first public release. See
[LICENSE](../../LICENSE).
