npm package: @latentsearch/license-client
Description: TypeScript client for Táillí License Manager - offline seat-based licensing
Author: Latent Search
Latest version: 0.2.0

# Táillí License Client for TypeScript

A friendly TypeScript/JavaScript client for the Táillí License Manager. Manage seats and API keys for offline/air-gapped deployments with minimal code.

## Installation

```bash
npm install @latentsearch/license-client
```

Or with yarn:

```bash
yarn add @latentsearch/license-client
```

## Quick Start

```typescript
import { LicenseClient } from '@latentsearch/license-client';

// Connect to your License Manager
const client = new LicenseClient('http://localhost:8787', {
  productId: 'my-product'
});

// Use a seat with automatic cleanup
await client.withSeat(async (seat) => {
  console.log(`Allocated seat: ${seat.seatId}`);
  // Do your licensed work here
  await runMyApplication();
});
// Seat automatically released when done
```

## Features

- **Simple callback API** - Seats are automatically released, even on exceptions
- **Auto-refresh** - Tokens are refreshed in the background
- **Full TypeScript support** - Complete type definitions included
- **Zero dependencies** - Uses only the Fetch API
- **Works everywhere** - Node.js, browsers, Deno, Bun, edge runtimes

## Usage Examples

### Check License Status

```typescript
import { LicenseClient } from '@latentsearch/license-client';

const client = new LicenseClient('http://localhost:8787', {
  productId: 'video-processor'
});

const status = await client.status();
console.log(`Product: ${status.productId}`);
console.log(`Seats: ${status.seatsInUse}/${status.seats} in use`);
console.log(`Expires: ${status.expiry}`);
console.log(`Features: ${status.features.join(', ')}`);
```

### Use a Seat (Recommended)

The `withSeat` method is the cleanest way to use seats:

```typescript
import { LicenseClient, SeatLimitError, LicenseExpiredError } from '@latentsearch/license-client';

const client = new LicenseClient('http://localhost:8787', {
  productId: 'video-processor'
});

try {
  const result = await client.withSeat(async (seat) => {
    console.log(`Got seat ${seat.seatId}`);

    // Your licensed code here
    return await processVideos();
  });

  console.log('Processing complete:', result);

} catch (e) {
  if (e instanceof SeatLimitError) {
    console.log('All seats are in use. Please try again later.');
  } else if (e instanceof LicenseExpiredError) {
    console.log('Your license has expired. Please contact support.');
  } else {
    throw e;
  }
}
```

### Add Context for Auditing

Track who's using seats for compliance and debugging:

```typescript
await client.withSeat(
  async (seat) => {
    await runAnalysis();
  },
  {
    context: {
      user: 'alice',
      workstation: 'lab-pc-42'
    }
  }
);
```

### Manual Seat Management

For more control, manage seats directly:

```typescript
// Allocate a seat
const seat = await client.allocate({
  context: { user: 'bob' }
});

try {
  // Do work...
  for (const batch of dataBatches) {
    await process(batch);
  }
} finally {
  // Always release when done
  await seat.release();
}
```

### Disable Auto-Refresh

For short operations, you might not need background refresh:

```typescript
await client.withSeat(
  async (seat) => {
    await quickOperation(); // Done in < 1 minute
  },
  { autoRefresh: false }
);
```

### Check Feature Entitlements

See what features your license enables:

```typescript
const entitlements = await client.getEntitlements();

if (entitlements.features.includes('ai-enhancement')) {
  enableAiFeatures();
}

if (entitlements.features.includes('batch-processing')) {
  enableBatchMode();
}

console.log(`API keys: ${entitlements.keysIssued}/${entitlements.maxKeys} used`);
```

### Verify User API Keys

Validate JWTs issued by the License Manager:

```typescript
try {
  const payload = await client.verifyKey(userProvidedToken);
  console.log(`Valid token for product: ${payload.productId}`);
  console.log(`Features: ${payload.features}`);
} catch (e) {
  console.log('Invalid or expired token');
}
```

## Error Handling

The client provides specific error classes for common scenarios:

```typescript
import {
  LicenseClient,
  LicenseError,       // Base class for all errors
  SeatLimitError,     // All seats are in use
  LicenseExpiredError, // License has expired
  ConnectionError,    // Can't reach License Manager
} from '@latentsearch/license-client';

const client = new LicenseClient('http://localhost:8787', {
  productId: 'my-product'
});

try {
  await client.withSeat(async (seat) => {
    await doWork();
  });
} catch (e) {
  if (e instanceof SeatLimitError) {
    // All seats taken - maybe queue the request
    await queueForLater();
  } else if (e instanceof LicenseExpiredError) {
    // License expired - notify admin
    await notifyAdmin('License expired!');
  } else if (e instanceof ConnectionError) {
    // Can't reach LM - maybe it's down
    await useCachedStateOrFail();
  } else if (e instanceof LicenseError) {
    // Other license-related errors
    console.error(`License error: ${e.message}`);
  } else {
    throw e;
  }
}
```

## Configuration

### Client Options

```typescript
const client = new LicenseClient('http://localhost:8787', {
  productId: 'my-product',  // Your product ID (required)
  timeout: 30000,           // Request timeout in ms (default: 30000)
});
```

### Allocation Options

```typescript
await client.withSeat(callback, {
  context: { user: 'alice' },  // Metadata for auditing
  autoRefresh: true,           // Keep token fresh (default: true)
  refreshInterval: 30000,      // Refresh interval in ms (default: 30000)
});
```

## API Reference

### LicenseClient

| Method | Description |
|--------|-------------|
| `status()` | Get license status (seats, expiry, features) |
| `allocate(options?)` | Allocate a seat, returns `Seat` object |
| `withSeat(callback, options?)` | Use a seat with automatic cleanup |
| `verifyKey(token)` | Verify a user API key (JWT) |
| `getEntitlements()` | Get feature entitlements |

### Seat

| Property/Method | Description |
|-----------------|-------------|
| `seatId` | Unique identifier for this seat |
| `token` | Current authentication token |
| `expiresAt` | Token expiration time (ISO 8601) |
| `release()` | Release the seat back to the pool |
| `refresh()` | Manually refresh the token |

### Types

```typescript
interface LicenseStatus {
  productId: string;
  seats: number;
  seatsInUse: number;
  seatsAvailable: number;
  expiry: string;
  features: string[];
  valid: boolean;
}

interface Entitlements {
  features: string[];
  scopes: string[];
  maxKeys: number;
  keysIssued: number;
  keysAvailable: number;
  expiry: string;
}

interface SeatContext {
  user?: string;
  workstation?: string;
  [key: string]: unknown;
}
```

## Browser Usage

The client works in browsers out of the box:

```html
<script type="module">
  import { LicenseClient } from 'https://unpkg.com/@latentsearch/license-client';

  const client = new LicenseClient('http://localhost:8787', {
    productId: 'my-product'
  });

  const status = await client.status();
  console.log(status);
</script>
```

## Node.js Usage

Works with Node.js 18+ (which has built-in fetch):

```typescript
import { LicenseClient } from '@latentsearch/license-client';

const client = new LicenseClient('http://localhost:8787', {
  productId: 'my-product'
});

await client.withSeat(async (seat) => {
  // Your code here
});
```

For Node.js < 18, you'll need a fetch polyfill like `node-fetch`.

## License

MIT License - see the main Táillí repository for details.