npm package: @i-dot-ai-npm/core-utilities
Description: Utility functions for i-dot-ai applications
Author: i.ai packages@cabinetoffice.gov.uk
Latest version: 1.6.0

# @i-dot-ai-npm/core-utilities

A comprehensive TypeScript utility library for building applications with LangChain and i.AI's centralised authentication service.

## Features

- 🔐 **Authentication Utilities** - Framework-agnostic auth client for i.AI's centralised auth API
- 💬 **LangChain Chat** - Stateful chat management with session persistence and Langfuse observability
- 🎯 **Type-Safe** - Full TypeScript support with strict typing
- 🔌 **Framework Agnostic** - Works with Express, Fastify, or any Node.js framework

## Installation

```bash
npm install @i-dot-ai-npm/core-utilities
```

## Quick Start

### Authentication

```typescript
import { AuthApiClient, createAuthUtils } from '@i-dot-ai-npm/core-utilities';

// Initialise the auth client
const authClient = new AuthApiClient({
  appName: 'my-app',
  authApiUrl: 'https://auth-api.i.ai.gov.uk',
  timeout: 5000 // optional, defaults to 5000ms
});

// Create auth utility functions
const { isAuthorisedUser, getUserInfo, handleUnauthorisedResponse } = createAuthUtils(authClient);

// Use in your route handlers
app.get('/protected', async (req, res) => {
  const token = req.header('Authorization')?.replace('Bearer ', '');
  
  if (!token || !(await isAuthorisedUser(token))) {
    return res.status(401).json({ error: 'Unauthorised' });
  }
  
  res.json({ message: 'Success' });
});
```

### LangChain Chat

```typescript
import { LangChainChat } from '@i-dot-ai-npm/core-utilities';

// Initialise chat with basic options
const chat = new LangChainChat({
  model: 'gpt-3.5-turbo',
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0.7
});

// Send a message
const response = await chat.chat('Hello, how are you?');
console.log(response.message);
```

---

## Authentication API

### AuthApiClient

The `AuthApiClient` communicates with i.AI's centralised authentication service to validate user tokens.

#### Constructor Options

```typescript
interface AuthApiClientOptions {
  appName: string;        // Your application name
  authApiUrl: string;     // Base URL of the auth API
  logger?: Logger;        // Optional logger (defaults to console)
  timeout?: number;       // Request timeout in ms (defaults to 5000)
}
```

#### Methods

##### `getUserAuthorisationInfo(token: string): Promise<UserAuthorisationResult>`

Validates a user token and returns authorisation details.

```typescript
const result = await authClient.getUserAuthorisationInfo(token);
console.log(result.email);         // user@i.ai.gov.uk
console.log(result.isAuthorised);  // true/false
console.log(result.authReason);    // "VALID" or error reason
```

**Returns:**
```typescript
interface UserAuthorisationResult {
  email: string;
  isAuthorised: boolean;
  authReason: string;
}
```

### Auth Utilities

The `createAuthUtils` factory function creates three helper functions for common auth patterns.

```typescript
const authUtils = createAuthUtils(authClient, logger?);
```

#### `isAuthorisedUser(authToken: string): Promise<boolean>`

Simple boolean check for authorisation. Returns `false` on any error.

```typescript
if (await isAuthorisedUser(token)) {
  // User is authorised
}
```

#### `getUserInfo(authToken: string): Promise<UserAuthorisationResult>`

Returns full user information. Returns safe defaults on error:
```typescript
{
  email: 'unknown',
  isAuthorised: false,
  authReason: 'unknown'
}
```

#### `handleUnauthorisedResponse(res: HttpResponse, userInfo: UserAuthorisationResult): HttpResponse`

Sends a standardised 401 response with appropriate error messages.

```typescript
const userInfo = await getUserInfo(token);
if (!userInfo.isAuthorised) {
  return handleUnauthorisedResponse(res, userInfo);
}
```

**Token Expiry Handling:**
- If `authReason === 'TOKEN_EXPIRED'`: Returns "Your login token has expired. Please log in again"
- Otherwise: Returns "{email} is not authorised to access this app"

### Full Authentication Example

```typescript
import express from 'express';
import { AuthApiClient, createAuthUtils } from '@i-dot-ai-npm/core-utilities';

const authClient = new AuthApiClient({
  appName: 'my-app',
  authApiUrl: process.env.AUTH_API_URL!,
});

const { isAuthorisedUser, getUserInfo, handleUnauthorisedResponse } = createAuthUtils(authClient);

const app = express();

// Extract token from Authorization header
const getToken = (req: express.Request): string | null => {
  const authHeader = req.header('Authorization');
  if (!authHeader) return null;
  return authHeader.startsWith('Bearer ') ? authHeader.substring(7) : authHeader;
};

// Simple authorisation check
app.get('/api/data', async (req, res) => {
  const token = getToken(req);
  if (!token || !(await isAuthorisedUser(token))) {
    return res.status(401).json({ error: 'Unauthorised' });
  }
  
  res.json({ data: 'protected data' });
});

// Authorisation with user info
app.get('/api/profile', async (req, res) => {
  const token = getToken(req);
  if (!token) {
    return res.status(401).json({ error: 'Authorization header required' });
  }
  
  const userInfo = await getUserInfo(token);
  if (!userInfo.isAuthorised) {
    return handleUnauthorisedResponse(res, userInfo);
  }
  
  res.json({ email: userInfo.email });
});

app.listen(3000);
```

---

## LangChain Chat API

### LangChainChat

A stateful chat manager that maintains conversation history with optional Langfuse observability integration.

#### Constructor Options

```typescript
interface ChatOptions {
  model?: string;              // Model name (default: 'gpt-3.5-turbo')
  temperature?: number;         // 0-1 (default: 0.7)
  maxTokens?: number;          // Maximum tokens (default: 1000)
  apiKey?: string;             // OpenAI API key
  baseUrl?: string;            // Custom API base URL
  langfuseSecretKey?: string;  // Langfuse secret key
  langfusePublicKey?: string;  // Langfuse public key
  langfuseHost?: string;       // Langfuse host URL
  sessionId?: string;          // Session ID for tracking
  userId?: string;             // User ID for tracking
}
```

**Constructor:**
```typescript
const chat = new LangChainChat(globalOptions?: ChatOptions);
```

Global options are merged with per-call options (per-call options take precedence).

#### Methods

##### `chat(message: string, options?: ChatOptions, sessionId?: string): Promise<ChatResponse>`

Send a message and get a response. Sessions are automatically created if not provided.

```typescript
const response = await chat.chat('What is TypeScript?');
console.log(response.message);
console.log(response.usage);      // Token usage stats
console.log(response.sessionId);  // Auto-generated or provided
```

**Returns:**
```typescript
interface ChatResponse {
  message: string;
  model: string;
  sessionId?: string;
  usage?: {
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
  };
}
```

##### `getSession(sessionId: string): ChatSession | undefined`

Retrieve a session's complete history and metadata.

```typescript
const session = chat.getSession('session-123');
if (session) {
  console.log(session.messages);    // Full message history
  console.log(session.createdAt);   // Session creation time
  console.log(session.updatedAt);   // Last activity time
}
```

##### `clearSession(sessionId: string): boolean`

Delete a session and its history. Returns `true` if deleted, `false` if not found.

```typescript
if (chat.clearSession('session-123')) {
  console.log('Session cleared');
}
```

##### `listSessions(): string[]`

Get all active session IDs.

```typescript
const sessions = chat.listSessions();
console.log(`Active sessions: ${sessions.length}`);
```

### Chat Examples

#### Basic Usage

```typescript
import { LangChainChat } from '@i-dot-ai-npm/core-utilities';

const chat = new LangChainChat({
  apiKey: process.env.OPENAI_API_KEY,
  temperature: 0.7,
  maxTokens: 1000
});

const response = await chat.chat('Explain quantum computing in simple terms');
console.log(response.message);
```

#### Session Management

```typescript
// Create a persistent conversation
const sessionId = 'user-123-conversation';

const response1 = await chat.chat('My name is Alice', {}, sessionId);
const response2 = await chat.chat('What is my name?', {}, sessionId);
// Response: "Your name is Alice"

// View conversation history
const session = chat.getSession(sessionId);
console.log(session?.messages); // All messages in order

// Clear when done
chat.clearSession(sessionId);
```

#### With Langfuse Observability

```typescript
const chat = new LangChainChat({
  model: 'gpt-4',
  apiKey: process.env.OPENAI_API_KEY,
  langfuseSecretKey: process.env.LANGFUSE_SECRET_KEY,
  langfusePublicKey: process.env.LANGFUSE_PUBLIC_KEY,
  langfuseHost: 'https://cloud.langfuse.com',
  userId: 'user-123'
});

const response = await chat.chat('Hello!', {}, 'session-123');
// Automatically tracked in Langfuse dashboard
```

#### Per-Call Options

```typescript
// Global defaults
const chat = new LangChainChat({
  model: 'gpt-3.5-turbo',
  temperature: 0.7
});

// Override for specific call
const response = await chat.chat('Be creative!', {
  temperature: 1.0,  // Higher temperature for this call
  maxTokens: 2000
});
```

#### Custom OpenAI Compatible API

```typescript
const chat = new LangChainChat({
  model: 'llama-3-70b',
  apiKey: 'your-api-key',
  baseUrl: 'https://api.your-provider.com/v1'
});
```

---

## Error Handling

### Authentication Errors

```typescript
import { AuthApiRequestError } from '@i-dot-ai-npm/core-utilities';

try {
  const result = await authClient.getUserAuthorisationInfo(token);
} catch (error) {
  if (error instanceof AuthApiRequestError) {
    console.error('Auth API failed:', error.message);
    // Handle auth service being down
  }
}
```

The auth utilities (`isAuthorisedUser`, `getUserInfo`) catch errors internally and return safe defaults, so try-catch is optional.

### Chat Errors

```typescript
try {
  const response = await chat.chat('Hello');
} catch (error) {
  console.error('Chat failed:', error);
  // Handle API errors, network issues, etc.
}
```

---

## TypeScript Support

Full TypeScript definitions are included. Import types as needed:

```typescript
import {
  // Auth types
  UserAuthorisationResult,
  AuthApiResponse,
  Logger,
  
  // Chat types
  ChatMessage,
  ChatOptions,
  ChatResponse,
  ChatSession
} from '@i-dot-ai-npm/core-utilities';
```

---

## Development

```bash
# Install dependencies
npm install

# Build the package
npm run build

# Run tests
npm test

# Run tests in watch mode
npm run test:watch

# Lint code
npm run lint

# Fix linting issues
npm run lint:fix

# Development mode (watch)
npm run dev
```

### Project Structure

```
src/
├── auth/                 # Authentication utilities
│   ├── authApiClient.ts  # HTTP client for auth API
│   ├── auth.ts          # Utility functions factory
│   └── types.ts         # Auth type definitions
├── llm/                 # LangChain utilities
│   └── langchain/
│       ├── chat/        # Chat implementation
│       └── types/       # Chat type definitions
└── index.ts             # Main exports
```

---

## Versioning

This package uses [semantic-release](https://github.com/semantic-release/semantic-release) with [conventional commits](https://www.conventionalcommits.org/):

- `feat:` → Minor version bump
- `fix:`, `perf:`, `refactor:` → Patch version bump
- `BREAKING CHANGE:` in commit body → Major version bump
- `docs:`, `test:`, `chore:` → No release

**Example commits:**
```bash
git commit -m "feat: add custom logger support"
git commit -m "fix: handle empty auth token"
git commit -m "feat: change auth API interface

BREAKING CHANGE: AuthApiClient constructor now requires options object"
```

---

## Publishing

This package is automatically published to npm using [NPM Trusted Publishers](https://docs.npmjs.com/trusted-publishers), which provides secure, token-free publishing from GitHub Actions.

### How It Works

When code is pushed to the `main` branch:

1. GitHub Actions runs tests and builds the package
2. Semantic-release determines the version based on commit messages
3. The package is published to npm using OpenID Connect (OIDC) authentication
4. No long-lived npm tokens are stored in GitHub secrets

### Setup Requirements

The GitHub Actions workflow requires the `id-token: write` permission to generate OIDC tokens for authentication with npm. The npm registry must be configured with this repository as a trusted publisher.

For more information, see the [npm Trusted Publishers documentation](https://docs.npmjs.com/trusted-publishers).

---

## License

MIT

---

## Support

For issues and feature requests, please use the [GitHub issue tracker](https://github.com/i-dot-ai/core-utilities/issues).

- This is to test the `chore` commits. Semantic release might not publish new versions to chores. As expected, chore did not work.
- Now testing for `fix` commits
- testing publishing again