# Middleware

URL: https://cogitator.app/docs/channels/middleware

> Intercept and filter messages with pairing, rate limiting, and custom middleware.

## Overview

Gateway middleware intercepts messages before they reach your agent. Use it for security, rate limiting, logging, and custom processing.

```typescript
interface GatewayMiddleware {
  name: string;
  handle(msg: ChannelMessage, ctx: MiddlewareContext, next: () => Promise<void>): Promise<void>;
}
```

Middleware follows an Express-style chain: call `next()` to pass the message forward, or skip it to block the message. The whole chain completes before the agent runs, so code after `await next()` executes before the LLM call (use [lifecycle hooks](/docs/channels/gateway#lifecycle-hooks) such as `agent:after_run` to react to answers).

## Built-in Middleware

### Pairing

Prevents unknown users from talking to your bot. New users must be approved by the owner.

```typescript
import { pairing } from '@cogitator-ai/channels';

const gateway = new Gateway({
  // ...
  middleware: [
    pairing({
      ownerIds: { telegram: '123456789' },
      codeLength: 6,
      expiresIn: 300, // seconds
    }),
  ],
});
```

**Flow:**

1. Unknown user sends a message
2. Bot responds: ``Hi! Ask the bot owner to approve you with: `/pair A7X92K` ``
3. Owner types `/pair A7X92K` (or `/pair@your_bot A7X92K` in groups)
4. User is now approved and can chat normally

Only owners can approve codes — approved users cannot pair others. Codes use an unambiguous alphabet (no `0/O/1/I`) and expired codes are pruned automatically. Approvals live in memory and are lost on restart; for persistent approvals use `dmPolicy` below. `PairingMiddleware` applies to every message, including group chats.

### DM Policy

Controls who may talk to the bot in direct messages and groups, with approvals persisted to disk.

```typescript
import { dmPolicy } from '@cogitator-ai/channels';

dmPolicy({
  mode: 'pairing', // 'open' | 'allowlist' | 'pairing' | 'disabled'
  ownerIds: { telegram: '123456789' },
  allowlist: ['telegram:42', '777'], // channel-scoped or bare user ids
  storePath: './data/dm-allowlist.json', // default: ~/.cogitator/dm-allowlist.json
  pairingCodeLength: 6, // default
  pairingExpiresMs: 300_000, // default: 5 minutes
  groupPolicy: 'allowlist', // 'open' | 'allowlist' | 'disabled'
  groupAllowlist: ['-1001234567890'],
  onPairingRequest: (userId, code) => notifyOwner(userId, code),
  onStoreError: (err) => log.error(err),
});
```

| Mode        | Unknown user in a DM                                                      |
| ----------- | ------------------------------------------------------------------------- |
| `open`      | Passes                                                                    |
| `allowlist` | Gets `Not authorized.`                                                    |
| `pairing`   | Gets a `/pair CODE` message; an owner approves it with `/pair CODE` in DM |
| `disabled`  | Silently ignored                                                          |

Owners always pass. Group messages (`groupId` set) are judged only by `groupPolicy` — `allowlist` lets in the chats in `groupAllowlist` plus owners. Approved users are saved to `storePath` (written atomically, loaded on start) and listed by `getApprovedUsers()`; write failures go to `onStoreError` (default: `console.error`). A leading `~` in `storePath` (`~/...`) expands to the home directory. The class form is `new DmPolicyMiddleware(config)`.

### Auto-Extract

Extracts entities and relations from every user message into a knowledge graph in the background.

```typescript
import { autoExtract } from '@cogitator-ai/channels';

autoExtract({
  extractor, // { extract(text) => { entities, relations } }
  graphAdapter, // GraphAdapter from @cogitator-ai/memory
  agentId: 'jarvis',
  coreFacts, // optional { set(key, value) }
  coreFactPatterns: { name: /my name is (\w+)/i },
  onError: (err) => log.warn(err),
});
```

Unknown entity/relation types are stored as `custom` (the original type is kept in `properties.originalType` / the edge `label`). Existing edges are not duplicated — their confidence is raised instead.

### Rate Limiting

Per-user (`channelType:userId`) sliding one-minute window. The limit notice is sent once per window, not for every blocked message; idle buckets are pruned. Blocked messages never reach the agent.

```typescript
import { rateLimit } from '@cogitator-ai/channels';

const gateway = new Gateway({
  // ...
  middleware: [
    rateLimit({
      maxPerMinute: 10,
      message: 'Too many messages. Please wait.',
    }),
  ],
});
```

### Owner Commands

Intercepts slash commands from the bot owner. See the [Owner Commands](/docs/channels/owner-commands) page.

## Custom Middleware

```typescript
import type { GatewayMiddleware, ChannelMessage, MiddlewareContext } from '@cogitator-ai/channels';

const loggingMiddleware: GatewayMiddleware = {
  name: 'logging',
  async handle(msg: ChannelMessage, ctx: MiddlewareContext, next: () => Promise<void>) {
    console.log(`[${msg.channelType}] ${msg.userId}: ${msg.text}`);
    await next(); // pass to next middleware or agent
  },
};
```

### Blocking Messages

Skip `next()` to block a message:

```typescript
const profanityFilter: GatewayMiddleware = {
  name: 'profanity-filter',
  async handle(msg, ctx, next) {
    if (containsProfanity(msg.text)) {
      await ctx.channel.sendText(msg.channelId, 'Please keep it civil.');
      return; // message blocked
    }
    await next();
  },
};
```

### Middleware Context

```typescript
interface MiddlewareContext {
  threadId: string;
  user: ChannelUser;
  channel: Channel;
  set(key: string, value: unknown): void;
  get<T>(key: string): T | undefined;
}
```

Use `set`/`get` to pass data between middleware:

```typescript
// in auth middleware
ctx.set('userRole', 'admin');

// in later middleware
const role = ctx.get<string>('userRole');
```
