npm package: @ovixa/email-client
Description: Client SDK for Ovixa Email service
Latest version: 0.2.0

# @ovixa/email-client

Client SDK for the Ovixa Email service. Send transactional emails using templates.

## Installation

```bash
npm install @ovixa/email-client
# or
pnpm add @ovixa/email-client
```

## Quick Start

```typescript
import { OvixaEmail } from '@ovixa/email-client';

const email = new OvixaEmail({
  apiKey: process.env.OVIXA_EMAIL_API_KEY,
  defaultFrom: 'noreply@yourapp.com',
});

// Send a templated email
await email.send({
  to: 'user@example.com',
  template: 'welcome',
  data: { name: 'John' },
});

// Get available templates for this API key
const { templates } = await email.getTemplates();
```

## API Reference

### Constructor

```typescript
new OvixaEmail(config: EmailClientConfig)
```

| Option           | Type     | Required | Default                  | Description              |
| ---------------- | -------- | -------- | ------------------------ | ------------------------ |
| `apiKey`         | `string` | Yes      | -                        | Your API key             |
| `emailUrl`       | `string` | No       | `https://email.ovixa.io` | Service URL              |
| `defaultFrom`    | `string` | No       | -                        | Default sender address   |
| `defaultReplyTo` | `string` | No       | -                        | Default reply-to address |
| `timeout`        | `number` | No       | `30000`                  | Request timeout (ms)     |

### Methods

#### `send(options)` - Send Templated Email

```typescript
const result = await email.send({
  to: 'user@example.com', // string or string[]
  template: 'verification', // template ID
  data: {
    // template data
    name: 'John',
    verifyUrl: 'https://...',
  },
  from: 'custom@example.com', // optional, overrides default
  replyTo: 'reply@example.com', // optional, overrides default
});

// Returns: { success: true, messageId: 'msg-xxx' }
```

#### `getTemplates()` - List Available Templates

Returns templates available for your API key. If your API key has template restrictions configured, only the allowed templates will be returned.

```typescript
const { templates } = await email.getTemplates();

for (const template of templates) {
  console.log(`${template.id}: requires ${template.requiredData.join(', ')}`);
}
```

#### `rateLimitInfo` - Get Rate Limit Status

```typescript
// After any API call, check rate limit status
console.log(email.rateLimitInfo);
// { limit: 100, remaining: 95, reset: 1700000000 }
```

## Available Templates

| Template           | Required Data                        | Description                   |
| ------------------ | ------------------------------------ | ----------------------------- |
| `verification`     | `name`, `verifyUrl`                  | Email verification link       |
| `password-reset`   | `name`, `resetUrl`                   | Password reset link           |
| `welcome`          | `name`                               | Welcome message               |
| `login-alert`      | `name`, `device`, `location`, `time` | New login notification        |
| `account-deletion` | `name`, `deletedAt`                  | Account deletion confirmation |

All templates also accept optional `appName` (defaults to "Ovixa").

## Template Restrictions

API keys can be configured with template restrictions to limit which templates they can use:

- **No restrictions**: API key can use all templates (default)
- **Restricted**: API key can only use specific templates (e.g., `welcome,verification`)
- **Disabled**: API key cannot use any templates (empty string)

### Client vs Server Validation

The SDK validates that a template name is valid (i.e., it exists as a known template type), but it **does not** check whether your API key is authorized to use that template. Server-side validation is authoritative — a request may pass client-side validation but still be rejected by the server with a `TEMPLATE_NOT_ALLOWED` error (HTTP 403) if your API key doesn't have access.

| Validation Layer | Checks                             | Error Code             |
| ---------------- | ---------------------------------- | ---------------------- |
| Client (SDK)     | Template name is a known type      | `INVALID_TEMPLATE`     |
| Server           | API key is authorized for template | `TEMPLATE_NOT_ALLOWED` |

### Best Practices for Restricted Keys

If your API key may have template restrictions, call `getTemplates()` first to discover which templates are available:

```typescript
const email = new OvixaEmail({
  apiKey: process.env.OVIXA_EMAIL_API_KEY,
  defaultFrom: 'noreply@yourapp.com',
});

// Discover available templates for this API key
const { templates } = await email.getTemplates();
const availableIds = templates.map((t) => t.id);

// Check before sending
if (availableIds.includes('welcome')) {
  await email.send({
    to: 'user@example.com',
    template: 'welcome',
    data: { name: 'John' },
  });
}
```

### Handling Restricted Template Errors

When calling `send()` with a template your API key cannot access, the server returns a `TEMPLATE_NOT_ALLOWED` error:

```typescript
try {
  await email.send({
    to: 'user@example.com',
    template: 'login-alert', // Valid template, but key may not have access
    data: { name: 'John', device: 'Chrome', location: 'NY', time: 'now' },
  });
} catch (error) {
  if (error instanceof OvixaEmailError && error.code === 'TEMPLATE_NOT_ALLOWED') {
    console.log('This API key cannot send login-alert emails');
  }
}
```

## Webhook Verification

Verify webhook signatures from email delivery events:

```typescript
import { verifyWebhook, extractWebhookHeaders } from '@ovixa/email-client/webhooks';

// In your webhook handler
async function handleWebhook(request: Request) {
  const payload = await request.text();
  const headers = extractWebhookHeaders(request.headers);

  try {
    const event = await verifyWebhook({
      payload,
      signature: headers.signature,
      timestamp: headers.timestamp,
      messageId: headers.messageId,
      secret: process.env.WEBHOOK_SECRET,
    });

    switch (event.type) {
      case 'email.delivered':
        console.log('Email delivered:', event.data.email_id);
        break;
      case 'email.bounced':
        console.log('Email bounced:', event.data.email_id);
        break;
      case 'email.complained':
        console.log('Spam complaint:', event.data.email_id);
        break;
    }

    return new Response('OK', { status: 200 });
  } catch (error) {
    if (error instanceof WebhookVerificationError) {
      return new Response('Invalid signature', { status: 401 });
    }
    throw error;
  }
}
```

### Webhook Event Types

| Event              | Description                        |
| ------------------ | ---------------------------------- |
| `email.sent`       | Email accepted by provider         |
| `email.delivered`  | Email delivered to recipient       |
| `email.bounced`    | Email bounced (hard/soft)          |
| `email.complained` | Recipient marked as spam           |
| `email.opened`     | Email opened (if tracking enabled) |
| `email.clicked`    | Link clicked (if tracking enabled) |

## Error Handling

```typescript
import { OvixaEmail, OvixaEmailError } from '@ovixa/email-client';

try {
  await email.send({ ... });
} catch (error) {
  if (error instanceof OvixaEmailError) {
    console.error(`Error [${error.code}]: ${error.message}`);

    if (error.code === 'RATE_LIMITED' && error.retryAfter) {
      console.log(`Retry after ${error.retryAfter} seconds`);
    }
  }
}
```

### Error Codes

| Code                   | Description                                    |
| ---------------------- | ---------------------------------------------- |
| `MISSING_API_KEY`      | API key not provided                           |
| `INVALID_API_KEY`      | API key is invalid                             |
| `INVALID_TEMPLATE`     | Template ID not found                          |
| `TEMPLATE_NOT_ALLOWED` | API key is not authorized to use this template |
| `MISSING_DATA`         | Required template data missing                 |
| `INVALID_EMAIL`        | Invalid email address format                   |
| `RATE_LIMITED`         | Too many requests (check `retryAfter`)         |
| `PROVIDER_ERROR`       | Email provider error                           |
| `NETWORK_ERROR`        | Network connectivity error                     |
| `TIMEOUT`              | Request timed out                              |
| `REQUEST_FAILED`       | Generic request failure                        |

## Security Notes

- **API keys** are never logged or included in error messages
- **HTTPS is enforced** - HTTP URLs trigger a warning (except localhost)
- **Email format validation** is performed client-side before sending
- **Webhook signatures** use HMAC-SHA256 with constant-time comparison
- **Timestamp validation** prevents replay attacks (5-minute window)
- **Rate limit info** is exposed for client-side handling

## TypeScript Support

Full TypeScript support with exported types:

```typescript
import type {
  EmailClientConfig,
  EmailTemplate,
  SendEmailOptions,
  SendEmailResponse,
  TemplateInfo,
  TemplatesResponse,
  RateLimitInfo,
  EmailErrorCode,
} from '@ovixa/email-client';

import type {
  WebhookVerifyOptions,
  WebhookEvent,
  WebhookEventType,
  WebhookEventData,
} from '@ovixa/email-client/webhooks';
```

## License

MIT