# @omnifolio/rate-governor

[![npm version](https://img.shields.io/npm/v/@omnifolio/rate-governor)](https://www.npmjs.com/package/@omnifolio/rate-governor)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)

Production-grade rate limiter with **in-memory + Redis** support, **subscription tiers**, automatic cleanup, and IP extraction helpers. Battle-tested at scale on Google Cloud Run.

Built by [OmniFolio](https://omnifolio.app) — Financial Intelligence Platform.

---

## Features

- 🚀 **Zero dependencies** — works out of the box with just Node.js
- 🧠 **In-memory by default** — no Redis needed for single-instance deployments
- 🔌 **Pluggable Redis adapter** — bring your own Redis client (Upstash, ioredis, etc.)
- ⚡ **Atomic Redis operations** — single INCR+EXPIRE round-trip on the hot path
- 🛡️ **Block mode** — temporarily ban abusive clients after limit exceeded
- 💳 **Subscription tiers** — scale limits by plan (free → pro → enterprise)
- 📊 **Standard headers** — `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `Retry-After`
- 🧹 **Auto-cleanup** — stale entries evicted every 60s, emergency eviction at 10K entries
- 🌐 **IP extraction** — handles Cloudflare, AWS ALB, GCP, nginx proxy headers

## Install

```bash
npm install @omnifolio/rate-governor
```

## Quick Start

### Basic (in-memory)

```typescript
import { checkRateLimit, Presets, getClientIP } from '@omnifolio/rate-governor';

// In your API route handler:
const ip = getClientIP(request.headers);
const result = checkRateLimit(`ip:${ip}`, Presets.STANDARD);

if (!result.success) {
  return new Response(JSON.stringify({ error: 'Too many requests' }), {
    status: 429,
    headers: result.headers,
  });
}

// Request allowed — continue processing
```

### With Redis (multi-instance)

```typescript
import { checkRateLimitAsync, Presets, buildIdentifier } from '@omnifolio/rate-governor';
import type { RedisAdapter } from '@omnifolio/rate-governor';
import { Redis } from '@upstash/redis';

// Create your Redis adapter
const redis = new Redis({ url: '...', token: '...' });

const adapter: RedisAdapter = {
  async incrWithExpire(key, ttl) {
    const pipeline = redis.pipeline();
    pipeline.incr(key);
    pipeline.expire(key, ttl);
    const results = await pipeline.exec();
    return (results[0] as number) ?? null;
  },
  async get<T>(key: string) {
    return redis.get<T>(key);
  },
  async set<T>(key: string, value: T, ttl: number) {
    await redis.set(key, value, { ex: ttl });
  },
};

// In your handler:
const id = buildIdentifier(request.headers, session?.userId);
const result = await checkRateLimitAsync(id, Presets.API_DATA, adapter);

if (!result.success) {
  return new Response('Rate limited', { status: 429, headers: result.headers });
}
```

### Subscription Tiers

```typescript
import { Presets, getSubscriptionRateLimit, checkRateLimit } from '@omnifolio/rate-governor';

// Scale limits based on user's plan
const config = getSubscriptionRateLimit(Presets.API_DATA, user.plan);
// Free: 100 req/min | Plus: 200 | Pro: 500 | Enterprise: 1000

const result = checkRateLimit(`user:${user.id}`, config);
```

## Presets

| Preset | Limit | Window | Block | Use Case |
|--------|-------|--------|-------|----------|
| `STANDARD` | 60/min | 1 min | 1 min | General API |
| `GENEROUS` | 120/min | 1 min | — | Read-heavy |
| `API_DATA` | 100/min | 1 min | 1 min | Authenticated data |
| `AI` | 30/min | 1 min | 2 min | LLM / AI ops |
| `STRICT` | 10/min | 1 min | 5 min | Sensitive ops |
| `AUTH` | 5/15 min | 15 min | 30 min | Login/register |
| `WEBHOOK` | 1000/min | 1 min | — | Trusted webhooks |
| `PUBLIC_HOURLY` | 1000/hr | 1 hr | — | Public API |
| `PUBLIC_READ` | 200/min | 1 min | — | Public reads |

## API Reference

### `checkRateLimit(identifier, config)`

Synchronous in-memory rate limit check. Returns `RateLimitResult`.

### `checkRateLimitAsync(identifier, config, redis)`

Async rate limit check using a `RedisAdapter`. Falls back to in-memory on failure.

### `getClientIP(headers)`

Extract client IP from proxy headers (Cloudflare, ALB, nginx).

### `buildIdentifier(headers, userId?)`

Build a rate limit key: `user:{id}` if authenticated, `ip:{ip}` otherwise.

### `getSubscriptionRateLimit(config, plan)`

Scale a config's limit by the plan multiplier.

### `blockIdentifier(identifier, durationMs?)`

Manually block a client for a duration (default: 1 hour).

### `unblockIdentifier(identifier)`

Remove a manual block.

### `getStats()`

Returns `{ totalClients, blockedClients }` from the in-memory store.

### `clearStore()`

Clear all rate limit records (useful in tests).

## Subscription Multipliers

| Plan | Multiplier |
|------|-----------|
| Free (default) | 1× |
| Plus / Trader | 2× |
| Pro / Investor | 5× |
| Enterprise / Whale | 10× |

## License

MIT — see [LICENSE](./LICENSE).

---

**Built with ❤️ by [OmniFolio](https://omnifolio.app)**
