npm package: @nuvoni/maes-sdk
Description: Official Node.js SDK for MAES Platform API
Author: Nuvoni
Latest version: 0.1.7

# MAES SDK

Official Node.js SDK for [MAES Platform](https://maes-platform.nuvoni.eu) API.

[![npm version](https://img.shields.io/npm/v/@nuvoni/maes-sdk.svg)](https://www.npmjs.com/package/@nuvoni/maes-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## Installation

```bash
npm install @nuvoni/maes-sdk
# or
yarn add @nuvoni/maes-sdk
# or
pnpm add @nuvoni/maes-sdk
```

## Quick Start

```typescript
import { MaesClient } from '@nuvoni/maes-sdk';

const client = new MaesClient({
  apiKey: 'sk_live_xxxxx',
  projectId: 'your-project-id',
});

// List all active cards
const { docs, total } = await client.cards.list({ status: 'active' });
console.log(`Found ${total} active cards`);

// Enable fuel authorizations for a card
await client.cards.enable('card-id');
```

## Configuration

```typescript
const client = new MaesClient({
  // Required: Your API key
  apiKey: 'sk_live_xxxxx',

  // Required for most operations: Your project ID
  projectId: 'your-project-id',

  // Optional: Custom base URL (for self-hosted instances)
  baseUrl: 'https://api.maes-platform.nuvoni.eu',

  // Optional: Request timeout in milliseconds (default: 30000)
  timeout: 30000,
});
```

### Environment Detection

The SDK automatically detects the environment from your API key prefix:

- `sk_sandbox_*` → Sandbox environment
- `sk_live_*` → Production environment

```typescript
console.log(client.environment); // 'sandbox' or 'production'
```

## Cards

### List Cards

```typescript
// List all cards
const { docs, total } = await client.cards.list();

// With pagination
const { docs } = await client.cards.list({
  page: 2,
  limit: 20,
});

// Filter by status
const { docs } = await client.cards.list({
  status: 'active', // 'new' | 'active' | 'inactive'
});

// Search by license plate
const { docs } = await client.cards.list({
  licensePlate: 'AB-123',
});

// Full-text search
const { docs } = await client.cards.list({
  search: 'John',
});
```

### Get Card

```typescript
const card = await client.cards.get('card-id');

console.log(card.cardNumber); // '782521009000153700'
console.log(card.status); // 'active'
console.log(card.licensePlate); // 'AB-123-CD'
console.log(card.driver); // 'John Doe'
console.log(card.internalReference); // 'TRUCK-042'
console.log(card.authGasoline); // true
console.log(card.authDiesel); // true
```

### Activate Card

Activate a new card with license plate and driver information:

```typescript
const card = await client.cards.activate('card-id', {
  licensePlate: 'AB-123-CD',
  driver: 'John Doe',
  internalReference: 'TRUCK-042', // optional
});

// Card is now active with all fuel authorizations enabled
console.log(card.status); // 'active'
console.log(card.authGasoline); // true
console.log(card.authDiesel); // true
console.log(card.internalReference); // 'TRUCK-042'
```

> **Note:** Only cards with `status: new` can be activated. In production, this triggers a background job to update the MAES portal. The API responds immediately with the activated card (optimistic update), and the MAES portal sync runs in the background.

### Enable Card

Enable all fuel authorizations for a card:

```typescript
const card = await client.cards.enable('card-id');

console.log(card.authGasoline); // true
console.log(card.authDiesel); // true
console.log(card.authLpg); // true
console.log(card.authHeatingOil); // true
```

### Disable Card

Disable all fuel authorizations for a card:

```typescript
const card = await client.cards.disable('card-id');

console.log(card.status); // 'active' (status remains active)
console.log(card.authGasoline); // false
console.log(card.authDiesel); // false
console.log(card.authLpg); // false
console.log(card.authHeatingOil); // false
```

> **Note:** Disabling a card only turns off fuel authorizations. The card status remains `active`.

## Webhooks

### Verify Signature

Verify webhook signatures to ensure requests are from MAES:

```typescript
import { MaesClient, MaesWebhookSignatureError } from '@nuvoni/maes-sdk';

const client = new MaesClient({ apiKey: 'sk_live_xxxxx' });

// Express.js example
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = client.webhooks.verifySignature(
      req.body.toString(),
      req.headers['x-webhook-signature'] as string,
      process.env.WEBHOOK_SECRET!,
    );

    // Handle the event
    switch (event.event) {
      case 'card.enabled':
        console.log('Card enabled:', event.data.card_id);
        break;
      case 'card.disabled':
        console.log('Card disabled:', event.data.card_id);
        break;
      case 'card.activated':
        console.log('Card activated:', event.data.card_id);
        break;
      case 'sync.completed':
        console.log('Sync completed');
        break;
    }

    res.status(200).send('OK');
  } catch (error) {
    if (error instanceof MaesWebhookSignatureError) {
      console.error('Invalid signature:', error.message);
      return res.status(400).send('Invalid signature');
    }
    throw error;
  }
});
```

### Webhook Events

| Event            | Description                  |
| ---------------- | ---------------------------- |
| `card.created`   | New card synced from MAES    |
| `card.activated` | Card was activated           |
| `card.enabled`   | Fuel authorizations enabled  |
| `card.disabled`  | Fuel authorizations disabled |
| `card.synced`    | Card data synced from MAES   |
| `sync.completed` | Full sync completed          |
| `sync.failed`    | Sync operation failed        |
| `test`           | Test event for verification  |

### Generate Signature (Testing)

For testing your webhook handlers:

```typescript
const payload = JSON.stringify({
  id: 'evt_test123',
  event: 'card.enabled',
  created_at: new Date().toISOString(),
  data: {
    card_id: 'card-123',
    card_number: '1234567890',
    status: 'active',
  },
});

const signature = client.webhooks.generateSignature(
  payload,
  'whsec_your_test_secret',
);

// Use signature in X-Webhook-Signature header for testing
```

## Error Handling

The SDK provides typed errors for different scenarios:

```typescript
import {
  MaesClient,
  MaesApiError,
  MaesAuthenticationError,
  MaesPermissionError,
  MaesNotFoundError,
  MaesRateLimitError,
  MaesValidationError,
  MaesConnectionError,
  MaesSDKOutdatedError,
} from '@nuvoni/maes-sdk';

try {
  await client.cards.get('non-existent-id');
} catch (error) {
  if (error instanceof MaesSDKOutdatedError) {
    console.log(
      `SDK outdated! Current: ${error.currentVersion}, Required: ${error.minVersion}`,
    );
    console.log(`Upgrade: npm install @nuvoni/maes-sdk@latest`);
  } else if (error instanceof MaesNotFoundError) {
    console.log('Card not found');
  } else if (error instanceof MaesAuthenticationError) {
    console.log('Invalid API key');
  } else if (error instanceof MaesPermissionError) {
    console.log('No access to this resource');
  } else if (error instanceof MaesRateLimitError) {
    console.log('Rate limited, try again later');
  } else if (error instanceof MaesValidationError) {
    console.log('Invalid request:', error.message);
  } else if (error instanceof MaesConnectionError) {
    console.log('Network error:', error.message);
  } else if (error instanceof MaesApiError) {
    console.log(`API error ${error.status}:`, error.code, error.message);
  }
}
```

### Error Properties

All API errors (`MaesApiError` and subclasses) have:

```typescript
error.status; // HTTP status code (401, 403, 404, etc.)
error.code; // Error code from API ('error.not_found', etc.)
error.message; // Human-readable message
error.body; // Raw API response
```

## Request Options

Override settings per request:

```typescript
// Override project ID for a single request
const card = await client.cards.get('card-id', {
  projectId: 'different-project',
});

// Add custom headers
const cards = await client.cards.list(undefined, {
  headers: { 'X-Custom-Header': 'value' },
});

// Custom timeout
const card = await client.cards.get('card-id', {
  timeout: 5000, // 5 seconds
});

// Abort request
const controller = new AbortController();
setTimeout(() => controller.abort(), 1000);

const card = await client.cards.get('card-id', {
  signal: controller.signal,
});
```

## TypeScript

The SDK is written in TypeScript and exports all types:

```typescript
import type {
  Card,
  CardStatus,
  ListCardsParams,
  ActivateCardParams,
  PaginatedResponse,
  WebhookEvent,
  WebhookEventType,
} from '@nuvoni/maes-sdk';

// Use types in your code
function processCard(card: Card) {
  if (card.status === 'active') {
    // ...
  }
}

function handleWebhook(event: WebhookEvent) {
  // TypeScript knows all possible event types
}
```

## Version Management

The SDK automatically checks version compatibility with the API:

- **Deprecation Warning**: If your SDK version is deprecated, you'll see a console warning
- **Outdated Error**: If your SDK version is below the minimum required, requests will throw `MaesSDKOutdatedError`

```typescript
import { SDK_VERSION, MaesSDKOutdatedError } from '@nuvoni/maes-sdk';

console.log(`Using SDK version: ${SDK_VERSION}`);

// Or access from client instance
console.log(`SDK version: ${client.sdkVersion}`);
```

Always keep your SDK up to date:

```bash
npm install @nuvoni/maes-sdk@latest
```

## Publishing / Deploying

To publish a new version of the SDK to npm:

```bash
cd maes-sdk

# 1. Update version in both files (keep them in sync!)
#    - package.json → "version": "x.y.z"
#    - src/version.ts → SDK_VERSION = 'x.y.z'

# 2. Build the SDK
pnpm build

# 3. Publish to npm (requires npm login + OTP for 2FA)
npm publish --access public --otp=<your-otp-code>
```

> **Important:** Always keep `package.json` version and `src/version.ts` `SDK_VERSION` in sync. The `SDK_VERSION` constant is sent as a header with every API request for telemetry and version checking.

## Requirements

- Node.js >= 18.0.0 (uses native `fetch`)

## License

MIT © [Nuvoni](https://nuvoni.eu)