## File: README.md

# Corsair: The Unified Integration Layer for Agents

⭐ the repo · [Website](https://corsair.dev) · [Discord](https://discord.gg/uNgCP3mSzU) · [X](https://x.com/corsairdotdev) 


Corsair is the unified integration layer for your agents. Connect your Corsair instance to your agent and immediately get access to every integration. Your agent never sees the credentials, and you control exactly what it can do.

[https://github.com/user-attachments/assets/a5db555a-7688-447d-9777-33f3dddfb03d](https://github.com/user-attachments/assets/a5db555a-7688-447d-9777-33f3dddfb03d)

---
## Why this exists

Agents are now capable of anything. It feels silly to do a routine task manually. But you do it anyways, because giving agents the keys to all your apps feels reckless. One misunderstood instruction and they're sending an email you'd never send. 

Corsair allows you to safely integrate with any app. Connect the Corsair MCP to any agent and have built-in tool calls, permissions, and scoped auth.

---
## Example: Sending an Email

You're away from your computer, so you ask your agent to send an email:

```
Send Sarah the Q1 numbers from the Financials folder in Drive.
```

Using Corsair, your agent calls Google Drive and then Gmail. You have permissions set up so your agent can't send an email without you seeing. Corsair intercepts the Gmail call, sees it's a send action, and creates a permission request:

```
Agent: I've drafted the email. This action requires your approval before it sends.

  ⚠️ gmail: messages.post
     To: sarah@corsair.dev
     Subject: "Q1 Numbers"
     
     Hi Sarah, attached is the breakdown we discussed on the call.

     <file>
     
     Best,
     Claude

  Review and approve: https://somepubliclink.com/review/a8f2c1
  Link expires in 10 minutes.
```

You open the link. Why does it say "Best, Claude"? You deny the permission, scold Claude, and it sends you a new request.

---
## Permission modes

Each integration has its own mode. Set GitHub to strict and Slack to cautious based on how much you trust each surface.

- **open** — everything runs immediately
- **cautious** _(recommended)_ — reads and writes run immediately; destructive actions require approval
- **strict** — reads run immediately; writes require approval; destructive actions are blocked
- **readonly** — reads only; all writes and destructive actions are blocked

You can also override individual endpoints within any mode. For example, set Slack to `open` but require approval before sending any message.

---
## Multi-tenancy

Corsair is built for production. Set `multiTenancy: true` and every tenant gets isolated credentials, isolated data storage, and isolated permissions handling. You can scope a request to a tenant id and Corsair ensures there is no cross-contamination.

```typescript
import { github } from '@corsair-dev/github';
import { slack } from '@corsair-dev/slack';
import { createCorsair } from 'corsair/core';

const corsair = createCorsair({
  multiTenancy: true,
  plugins: [slack(), github()],
});

const client = corsair.withTenant('org-456');
await client.slack.api.messages.post({ channel: '#alerts', text: 'Deploy complete.' });
```

---
## Webhooks

Every plugin is shipped with typed, signature-verified webhook handlers. All webhooks point to a single endpoint. Set it and forget it.

```typescript
import { processWebhook } from 'corsair';

app.post('/webhooks', async (req, res) => {
  const webhook = await processWebhook(corsair, req.headers, req.body)
  
  return res.json(webhook.response)
});
```

---
## FAQ

 Where are credentials stored?

In an encrypted database using envelope encryption. A KEK you control encrypts per-tenant data keys, which encrypt the actual secrets. If you'd rather manage keys yourself, pass them directly and skip the key manager.

  Does the agent ever see my API keys?

No. The agent sees method names and results. Credentials are resolved internally by Corsair at call time. The agent cannot read, log, or exfiltrate them.

  What happens if I deny an approval request?

The action is discarded. Nothing is sent, created, or modified. Your agent can try again with corrected parameters and will send a new approval request.

  Can I use Corsair with multiple tenants?

Yes. Set `multiTenancy: true` and each tenant gets isolated credentials, data storage, and permission evaluation. Endpoint discovery is available at the root and doesn't require a tenant.

  Can I use Corsair alongside direct SDK calls?

Yes. Corsair is a library. Use it where the permission layer and key management help, and drop down to individual SDKs when you need custom logic.




Can the agent go around the permission request?
No. Corsair creates a permission request in a database the agent doesn't have access to. Your agent cannot get past the permission request until that database row is set to `approved`.



What integrations do you currently support?
For the full list of integrations, see our [docs](https://docs.corsair.dev/guides/plugins). We're adding integrations regularly. If there's an integration you need, create a Github issue.


---
## License

Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/corsairdev/corsair/blob/main/LICENSE) for details.

---
## Star History

<a href="https://www.star-history.com/?repos=corsairdev%2Fcorsair&type=date&legend=top-left">
 
   
   
   
 
</a>

---

## File: demo/sdk/slack/README.md

# Slack SDK

A TypeScript SDK for the Slack API with Zod validation.

## Installation

```bash
npm install
```

## Configuration

### Getting Your Slack Credentials

Follow these steps to obtain the necessary tokens and IDs from your Slack workspace:

#### Step 1: Create a Slack App

1. Go to [Slack API Apps](https://api.slack.com/apps)
2. Click **"Create New App"**
3. Choose **"From scratch"**
4. Enter an App Name (e.g., "My SDK App")
5. Select your workspace
6. Click **"Create App"**

#### Step 2: Configure Bot Token Scopes

1. In your app settings, go to **"OAuth & Permissions"** in the left sidebar
2. Scroll down to **"Scopes"** section
3. Under **"Bot Token Scopes"**, click **"Add an OAuth Scope"**
4. Add the following scopes based on what you need:

| Scope | Description | Required For |
|-------|-------------|--------------|
| `channels:read` | View basic channel info | Listing channels |
| `channels:write` | Manage channels | Creating/archiving channels |
| `channels:history` | View messages in channels | Getting channel history |
| `chat:write` | Send messages | Posting messages |
| `groups:read` | View private channels | Listing private channels |
| `groups:write` | Manage private channels | Creating private channels |
| `im:read` | View direct messages | Listing DMs |
| `im:write` | Start direct messages | Opening DMs |
| `mpim:read` | View group DMs | Listing group DMs |
| `users:read` | View users | Getting user info |
| `users:read.email` | View email addresses | Getting user emails |
| `userGroups:read` | View user groups | Listing user groups |
| `userGroups:write` | Manage user groups | Creating user groups |
| `files:read` | View files | Getting file info |
| `files:write` | Upload files | Uploading files |
| `reactions:read` | View reactions | Getting reactions |
| `reactions:write` | Add/remove reactions | Adding reactions |
| `stars:read` | View starred items | Listing stars |
| `stars:write` | Add/remove stars | Managing stars |

#### Step 3: Install App to Workspace

1. Go to **"OAuth & Permissions"**
2. Click **"Install to Workspace"** at the top
3. Review the permissions and click **"Allow"**
4. Copy the **"Bot User OAuth Token"** (starts with `xoxb-`)

#### Step 4: Get User Token (Optional)

For some APIs like `search.messages`, you need a User Token:

1. Go to **"OAuth & Permissions"**
2. Under **"User Token Scopes"**, add:
   - `search:read` - For searching messages
3. Reinstall the app if needed
4. Copy the **"User OAuth Token"** (starts with `xoxp-`)

#### Step 5: Find Channel and User IDs

**To find a Channel ID:**
1. Open Slack in your browser or desktop app
2. Right-click on a channel name
3. Click **"View channel details"** or **"Copy link"**
4. The Channel ID is in the URL: `https://app.slack.com/client/TXXXXX/C0123456789`
   - The ID starting with `C` is your Channel ID

**Alternative method:**
1. Open the channel
2. Click the channel name at the top
3. Scroll to the bottom of the modal
4. The Channel ID is displayed there

**To find a User ID:**
1. Click on a user's profile
2. Click the **"..."** (More) button
3. Click **"Copy member ID"**
   - Or find it in the profile URL: `https://app.slack.com/team/U0123456789`

#### Step 6: Create Environment File

1. Copy the example file:
   ```bash
   cp .env.example .env
   ```

2. Fill in your values:
   ```env
   SLACK_BOT_TOKEN=xoxb-your-actual-bot-token
   SLACK_USER_TOKEN=xoxp-your-actual-user-token
   TEST_SLACK_CHANNEL=C0123456789
   TEST_SLACK_USER=U0123456789
   ```

## Usage

### Basic Usage

```typescript
import { Slack, OpenAPI } from '@corsair/slack-sdk';

// Configure the token
OpenAPI.TOKEN = process.env.SLACK_BOT_TOKEN;

// List channels
const channels = await Slack.Channels.list({ limit: 10 });
console.log(channels);

// Send a message
const message = await Slack.Messages.send({
  channel: 'C0123456789',
  text: 'Hello from the SDK!',
});
console.log(message);

// Get user info
const user = await Slack.Users.get({ user: 'U0123456789' });
console.log(user);
```

### Available APIs

- **Slack.Channels** - Channel management (archive, create, get, list, invite, etc.)
- **Slack.Users** - User information (get, list, getProfile, getPresence, updateProfile)
- **Slack.Usergroups** - User group management (create, disable, enable, list, update)
- **Slack.Files** - File operations (get, list, upload)
- **Slack.Messages** - Messaging (send, update, delete, search, getPermalink)
- **Slack.Reactions** - Reaction management (add, get, remove)
- **Slack.Stars** - Star management (add, remove, list)

### With Zod Validation

All request arguments have Zod schemas for validation:

```typescript
import { ChatPostMessageArgsSchema } from '@corsair/slack-sdk';

const args = {
  channel: 'C0123456789',
  text: 'Hello!',
};

// Validate before sending
const result = ChatPostMessageArgsSchema.safeParse(args);
if (result.success) {
  await Slack.Messages.send(result.data);
} else {
  console.error('Validation failed:', result.error);
}
```

## Running Tests

```bash
# Run all tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

# Run only model validation tests (no token required)
npm test -- models.test.ts
```

## API Reference

### Channels

| Method | Description |
|--------|-------------|
| `Slack.Channels.archive(args)` | Archive a channel |
| `Slack.Channels.close(args)` | Close a direct message |
| `Slack.Channels.create(args)` | Create a channel |
| `Slack.Channels.get(args)` | Get channel info |
| `Slack.Channels.list(args)` | List channels |
| `Slack.Channels.getHistory(args)` | Get channel message history |
| `Slack.Channels.invite(args)` | Invite users to a channel |
| `Slack.Channels.join(args)` | Join a channel |
| `Slack.Channels.kick(args)` | Remove a user from a channel |
| `Slack.Channels.leave(args)` | Leave a channel |
| `Slack.Channels.getMembers(args)` | Get channel members |
| `Slack.Channels.open(args)` | Open a direct message |
| `Slack.Channels.rename(args)` | Rename a channel |
| `Slack.Channels.getReplies(args)` | Get thread replies |
| `Slack.Channels.setPurpose(args)` | Set channel purpose |
| `Slack.Channels.setTopic(args)` | Set channel topic |
| `Slack.Channels.unarchive(args)` | Unarchive a channel |

### Users

| Method | Description |
|--------|-------------|
| `Slack.Users.get(args)` | Get user info |
| `Slack.Users.list(args)` | List users |
| `Slack.Users.getProfile(args)` | Get user profile |
| `Slack.Users.getPresence(args)` | Get user presence |
| `Slack.Users.updateProfile(args)` | Update user profile |

### Messages

| Method | Description |
|--------|-------------|
| `Slack.Messages.send(args)` | Send a message |
| `Slack.Messages.update(args)` | Update a message |
| `Slack.Messages.delete(args)` | Delete a message |
| `Slack.Messages.search(args)` | Search messages |
| `Slack.Messages.getPermalink(args)` | Get message permalink |

### Reactions

| Method | Description |
|--------|-------------|
| `Slack.Reactions.add(args)` | Add a reaction |
| `Slack.Reactions.get(args)` | Get reactions |
| `Slack.Reactions.remove(args)` | Remove a reaction |

### Stars

| Method | Description |
|--------|-------------|
| `Slack.Stars.add(args)` | Star an item |
| `Slack.Stars.remove(args)` | Unstar an item |
| `Slack.Stars.list(args)` | List starred items |

### Files

| Method | Description |
|--------|-------------|
| `Slack.Files.get(args)` | Get file info |
| `Slack.Files.list(args)` | List files |
| `Slack.Files.upload(args)` | Upload a file |

### Usergroups

| Method | Description |
|--------|-------------|
| `Slack.Usergroups.create(args)` | Create a user group |
| `Slack.Usergroups.disable(args)` | Disable a user group |
| `Slack.Usergroups.enable(args)` | Enable a user group |
| `Slack.Usergroups.list(args)` | List user groups |
| `Slack.Usergroups.update(args)` | Update a user group |

## License

MIT

---

## File: demo/sdk/posthog/README.md

# PostHog SDK

A TypeScript SDK for the PostHog API with Zod validation.

## Installation

```bash
npm install
```

## Configuration

### Getting Your PostHog Credentials

Follow these steps to obtain the necessary API key from your PostHog account:

#### Step 1: Get Your Project API Key

1. Log in to your [PostHog account](https://app.posthog.com)
2. Navigate to your **Project Settings**
3. Click on **Project API Key** in the left sidebar
4. Copy your **Project API Key** - this is the key you'll use for capturing events

#### Step 2: Get Your Personal API Key (Optional, for API access)

1. In PostHog, click on your profile icon (top right)
2. Go to **Personal API Keys**
3. Click **Create Personal API Key**
4. Give it a name and copy the key
5. **Important**: Store this key securely. You won't be able to see it again.

#### Step 3: Create Environment File

1. Copy the example environment file:
   ```bash
   cp env.example .env
   ```

2. Open the `.env` file and fill in your values:
   ```env
   # PostHog API Configuration
   POSTHOG_API_KEY=your-project-api-key-here
   POSTHOG_API_HOST=https://app.posthog.com

   # Optional: Personal API Key for API access
   POSTHOG_PERSONAL_API_KEY=your-personal-api-key-here

   # Webhook Configuration (optional, for webhook testing)
   POSTHOG_WEBHOOK_SECRET=your-webhook-secret-here

   # Test Configuration
   TEST_TIMEOUT=30000

   # Webhook Server Configuration
   PORT=3000
   ```

3. Replace `your-project-api-key-here` with the Project API Key you copied in Step 1

### Setting Up Webhooks

To receive webhook events from PostHog, follow these steps:

#### Step 1: Set Up Local Webhook Server

1. Start the webhook test server:
   ```bash
   npm run webhook-server
   ```

2. In another terminal, start ngrok to expose your local server:
   ```bash
   ngrok http 3000
   ```

3. Copy the HTTPS URL from ngrok (e.g., `https://abc123.ngrok.io`)

#### Step 2: Create Webhook Destination in PostHog

1. Go to your PostHog project settings
2. Navigate to **Data Pipelines** → **Destinations**
3. Click **+ New** → **Destination** → **Webhook**
4. Enter the Webhook URL: `https://your-ngrok-url.ngrok.io/webhook`
5. Configure filters (optional) to select which events to send
6. Click **Create & Enable**

#### Step 3: Test Webhooks

1. With the webhook server running, trigger events in PostHog:
   - Capture events using the SDK
   - Use PostHog's UI to trigger events
   - etc.

2. Watch your terminal for incoming webhook events
3. Webhook payloads are automatically saved to `tests/fixtures/` for testing

## Usage

### Basic Usage

```typescript
import { PostHog, OpenAPI } from '@corsair/posthog-sdk';

// Configure the API key
OpenAPI.TOKEN = process.env.POSTHOG_API_KEY;

// Create an event
const event = await PostHog.Events.create({
  distinct_id: 'user-123',
  event: 'button_clicked',
  properties: {
    button_name: 'Sign Up',
    page: '/home',
  },
});
console.log(event);

// Create an identity
const identity = await PostHog.Identity.create({
  distinct_id: 'user-123',
  properties: {
    email: 'user@example.com',
    name: 'John Doe',
    plan: 'premium',
  },
});
console.log(identity);

// Track a page view
const pageView = await PostHog.Track.trackPage({
  distinct_id: 'user-123',
  url: 'https://example.com/products',
  properties: {
    title: 'Products Page',
  },
});
console.log(pageView);

// Track a screen view
const screenView = await PostHog.Track.trackScreen({
  distinct_id: 'user-123',
  screen_name: 'HomeScreen',
  properties: {
    app_version: '1.0.0',
    platform: 'ios',
  },
});
console.log(screenView);

// Create an alias
const alias = await PostHog.Alias.create({
  distinct_id: 'user-123',
  alias: 'old-user-id',
});
console.log(alias);
```

### Available APIs

- **PostHog.Alias** - Alias management (create)
- **PostHog.Events** - Event tracking (create)
- **PostHog.Identity** - User identification (create)
- **PostHog.Track** - Page and screen tracking (trackPage, trackScreen)

### With Per-Request Token

You can also pass a token per request:

```typescript
const event = await PostHog.Events.create({
  distinct_id: 'user-123',
  event: 'button_clicked',
  token: 'your-api-key-here',
});
```

### With Zod Validation

All request arguments have Zod schemas for validation:

```typescript
import { CreateEventArgsSchema } from '@corsair/posthog-sdk';

const args = {
  distinct_id: 'user-123',
  event: 'button_clicked',
  properties: {
    button_name: 'Sign Up',
  },
};

// Validate before sending
const result = CreateEventArgsSchema.safeParse(args);
if (result.success) {
  await PostHog.Events.create(result.data);
} else {
  console.error('Validation failed:', result.error);
}
```

## Running Tests

```bash
# Run all tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

# Run only model validation tests (no token required)
npm test -- models.test.ts
```

## API Reference

### Alias

| Method | Description |
|--------|-------------|
| `PostHog.Alias.create(args)` | Create an alias to link two distinct IDs |

### Events

| Method | Description |
|--------|-------------|
| `PostHog.Events.create(args)` | Create a custom event |

### Identity

| Method | Description |
|--------|-------------|
| `PostHog.Identity.create(args)` | Create or update user identity properties |

### Track

| Method | Description |
|--------|-------------|
| `PostHog.Track.trackPage(args)` | Track a page view |
| `PostHog.Track.trackScreen(args)` | Track a screen view (mobile apps) |

## Error Handling

The SDK throws `ApiError` for API errors:

```typescript
import { ApiError } from '@corsair/posthog-sdk';

try {
  await PostHog.Events.create({
    distinct_id: 'user-123',
    event: 'test_event',
  });
} catch (error) {
  if (error instanceof ApiError) {
    console.error('API Error:', error.status, error.message);
    console.error('Response body:', error.body);
  }
}
```

## Rate Limiting

PostHog has rate limits. The SDK will throw an error with status 429 when rate limited. You can handle this:

```typescript
try {
  await PostHog.Events.create({
    distinct_id: 'user-123',
    event: 'test_event',
  });
} catch (error) {
  if (error instanceof ApiError && error.status === 429) {
    // Handle rate limit
    const retryAfter = error.headers?.['retry-after'];
    console.log(`Rate limited. Retry after ${retryAfter} seconds`);
  }
}
```

## Webhook Handling

The SDK includes a webhook handler for processing PostHog webhook events:

```typescript
import { createWebhookHandler } from '@corsair/posthog-sdk';

const handler = createWebhookHandler({
  secret: process.env.POSTHOG_WEBHOOK_SECRET, // Optional, for signature verification
});

// Handle event captured
handler.on('event.captured', async (event) => {
  console.log('Event captured:', event.event);
  console.log('User:', event.distinct_id);
  console.log('Properties:', event.properties);
  // Your logic here
});

// Process webhook payload
const result = await handler.handleWebhook(headers, payload);
if (result.success) {
  console.log('Webhook processed:', result.eventType);
}
```

### Running the Webhook Test Server

The SDK includes a test server for receiving and testing webhooks:

```bash
# Start the webhook server
npm run webhook-server

# In another terminal, expose it with ngrok
ngrok http 3000
```

The server will:
- Receive webhook events from PostHog
- Log event details to the console
- Save webhook payloads to `tests/fixtures/` for testing
- Display a helpful setup page at `http://localhost:3000`

### Testing Webhooks

After capturing webhook events, you can test them:

```bash
# Run webhook tests (uses fixtures from webhook server)
npm test -- webhooks.test.ts
```

## Environment Variables

Create a `.env` file in the project root with the following variables:

```env
# Required: PostHog Project API Key
POSTHOG_API_KEY=your-project-api-key-here

# Optional: Override API base URL (default: https://app.posthog.com)
POSTHOG_API_HOST=https://app.posthog.com

# Optional: Personal API Key for API access
POSTHOG_PERSONAL_API_KEY=your-personal-api-key-here

# Optional: Webhook secret for signature verification
POSTHOG_WEBHOOK_SECRET=your-webhook-secret-here

# Optional: Test timeout in milliseconds (default: 30000)
TEST_TIMEOUT=30000

# Optional: Webhook server port (default: 3000)
PORT=3000
```

## License

MIT

---

## File: demo/sdk/hubspot/README.md

# HubSpot SDK

A TypeScript SDK for the HubSpot API with Zod validation.

## Installation

```bash
npm install
```

## Configuration

### Getting Your HubSpot Credentials

Follow these steps to obtain the necessary access token from your HubSpot account (updated for 2024/2025):

#### Step 1: Navigate to Private Apps

1. Log in to your [HubSpot account](https://app.hubspot.com)
2. Click the **Settings** icon (⚙️) in the main navigation bar (top right)
3. In the left sidebar, navigate to **Integrations** → **Private Apps**
4. You'll see a list of existing private apps (if any)

#### Step 2: Create a New Private App

1. Click the **"Create a private app"** button (top right)
2. In the **"Basic Info"** tab:
   - Enter an **App name** (e.g., "My SDK App" or "Workflow Integration")
   - Optionally add a **Description**
   - Click **"Create app"** at the bottom

#### Step 3: Configure Scopes (Permissions)

1. After creating the app, you'll be on the **"Scopes"** tab
2. Select the scopes you need based on what you want to do. Here are the recommended scopes:

| Scope | Description | Required For |
|-------|-------------|--------------|
| `crm.objects.contacts.read` | Read contacts | Getting contact info, listing contacts |
| `crm.objects.contacts.write` | Write contacts | Creating/updating/deleting contacts |
| `crm.objects.companies.read` | Read companies | Getting company info, listing companies |
| `crm.objects.companies.write` | Write companies | Creating/updating/deleting companies |
| `crm.objects.deals.read` | Read deals | Getting deal info, listing deals |
| `crm.objects.deals.write` | Write deals | Creating/updating/deleting deals |
| `crm.objects.tickets.read` | Read tickets | Getting ticket info, listing tickets |
| `crm.objects.tickets.write` | Write tickets | Creating/updating/deleting tickets |
| `engagements.read` | Read engagements | Getting engagement info |
| `engagements.write` | Write engagements | Creating/deleting engagements |
| `contacts.read` | Read contact lists | Viewing contact lists |
| `contacts.write` | Write contact lists | Adding/removing contacts from lists |

3. **Tip**: You can search for scopes using the search box at the top
4. Click **"Save"** after selecting your scopes

#### Step 4: Get Your Access Token

1. Navigate to the **"Auth"** tab (in the app settings)
2. You'll see a section labeled **"Access token"**
3. Click the **"Show token"** button (or eye icon) to reveal the token
4. Copy the **Access token** - it will start with `pat-` (Private App Token)
5. **Important**: Store this token securely. You won't be able to see it again after closing this page. If you lose it, you'll need to create a new private app.

#### Step 5: Create Environment File

1. Copy the example environment file:
   ```bash
   cp env.example .env
   ```

2. Open the `.env` file and fill in your values:
   ```env
   # HubSpot API Configuration
   HUBSPOT_ACCESS_TOKEN=pat-your-actual-access-token-here
   HUBSPOT_BASE_URL=https://api.hubapi.com

   # Webhook Configuration (optional, for webhook testing)
   HUBSPOT_WEBHOOK_SECRET=your-webhook-secret-here

   # Test Configuration
   TEST_TIMEOUT=30000
   TEST_LIST_ID=your-test-list-id-here

   # Webhook Server Configuration
   PORT=3000
   ```

3. Replace `pat-your-actual-access-token-here` with the token you copied in Step 4

### Setting Up Webhooks

To receive webhook events from HubSpot, follow these steps:

#### Step 1: Get Your Webhook Secret (Optional but Recommended)

1. In your Private App settings, go to the **"Webhooks"** tab
2. You'll see a **"Client secret"** - copy this value
3. Add it to your `.env` file as `HUBSPOT_WEBHOOK_SECRET`
4. This secret is used to verify webhook signatures for security

#### Step 2: Set Up Local Webhook Server

1. Start the webhook test server:
   ```bash
   npm run webhook-server
   ```

2. In another terminal, start ngrok to expose your local server:
   ```bash
   ngrok http 3000
   ```

3. Copy the HTTPS URL from ngrok (e.g., `https://abc123.ngrok.io`)

#### Step 3: Create Webhook Subscription in HubSpot

1. Go to your Private App settings → **"Webhooks"** tab
2. Click **"Create subscription"** button
3. Fill in the subscription details:
   - **Webhook URL**: `https://your-ngrok-url.ngrok.io/webhook`
   - **Event type**: Select from the dropdown (e.g., `contact.creation`, `company.creation`, etc.)
   - **Property name**: (Optional) Leave blank for all properties, or specify a property
   - **Active**: Check this box to enable the subscription
4. Click **"Create"**
5. Repeat for each event type you want to receive

#### Step 4: Test Webhooks

1. With the webhook server running, trigger events in HubSpot:
   - Create a new contact
   - Update a company
   - Create a deal
   - etc.

2. Watch your terminal for incoming webhook events
3. Webhook payloads are automatically saved to `tests/fixtures/` for testing

#### Available Webhook Event Types

- `contact.creation` - When a contact is created
- `contact.propertyChange` - When a contact property changes
- `contact.deletion` - When a contact is deleted
- `contact.privacyDeletion` - When a contact is privacy deleted
- `company.creation` - When a company is created
- `company.propertyChange` - When a company property changes
- `company.deletion` - When a company is deleted
- `deal.creation` - When a deal is created
- `deal.propertyChange` - When a deal property changes
- `deal.deletion` - When a deal is deleted
- `ticket.creation` - When a ticket is created
- `ticket.propertyChange` - When a ticket property changes
- `ticket.deletion` - When a ticket is deleted
- `engagement.creation` - When an engagement is created
- `engagement.deletion` - When an engagement is deleted
- `conversation.creation` - When a conversation is created
- `conversation.newMessage` - When a new message is added to a conversation
- `conversation.propertyChange` - When a conversation property changes
- `conversation.deletion` - When a conversation is deleted
- `conversation.privacyDeletion` - When a conversation is privacy deleted
- `workflow.enrollment` - When a contact is enrolled in a workflow

## Usage

### Basic Usage

```typescript
import { HubSpot, OpenAPI } from '@corsair/hubspot-sdk';

// Configure the token
OpenAPI.TOKEN = process.env.HUBSPOT_ACCESS_TOKEN;

// Get a contact
const contact = await HubSpot.Contacts.get({
  contactId: '12345',
});
console.log(contact);

// Create a contact
const newContact = await HubSpot.Contacts.createOrUpdate({
  properties: {
    email: 'newcontact@example.com',
    firstname: 'New',
    lastname: 'Contact',
  },
});
console.log(newContact);

// Get many companies
const companies = await HubSpot.Companies.getMany({ limit: 10 });
console.log(companies);
```

### Available APIs

- **HubSpot.Contacts** - Contact management (get, getMany, createOrUpdate, delete, getRecentlyCreated, getRecentlyUpdated, search)
- **HubSpot.Companies** - Company management (get, getMany, create, update, delete, getRecentlyCreated, getRecentlyUpdated, searchByDomain)
- **HubSpot.Deals** - Deal management (get, getMany, create, update, delete, getRecentlyCreated, getRecentlyUpdated, search)
- **HubSpot.Tickets** - Ticket management (get, getMany, create, update, delete)
- **HubSpot.Engagements** - Engagement management (get, getMany, create, delete)
- **HubSpot.ContactLists** - Contact list operations (addContact, removeContact)

### With Per-Request Token

You can also pass a token per request:

```typescript
const contact = await HubSpot.Contacts.get({
  contactId: '12345',
  token: 'pat-your-token-here',
});
```

### With Zod Validation

All request arguments have Zod schemas for validation:

```typescript
import { GetContactArgsSchema } from '@corsair/hubspot-sdk';

const args = {
  contactId: '12345',
};

// Validate before sending
const result = GetContactArgsSchema.safeParse(args);
if (result.success) {
  await HubSpot.Contacts.get(result.data);
} else {
  console.error('Validation failed:', result.error);
}
```

## Running Tests

```bash
# Run all tests
npm test

# Run tests with coverage
npm run test:coverage

# Run tests in watch mode
npm run test:watch

# Run only model validation tests (no token required)
npm test -- models.test.ts
```

## API Reference

### Contacts

| Method | Description |
|--------|-------------|
| `HubSpot.Contacts.get(args)` | Get a contact by ID |
| `HubSpot.Contacts.getMany(args)` | Get many contacts |
| `HubSpot.Contacts.createOrUpdate(args)` | Create or update a contact |
| `HubSpot.Contacts.delete(args)` | Delete a contact |
| `HubSpot.Contacts.getRecentlyCreated(args)` | Get recently created contacts |
| `HubSpot.Contacts.getRecentlyUpdated(args)` | Get recently updated contacts |
| `HubSpot.Contacts.search(args)` | Search contacts |

### Companies

| Method | Description |
|--------|-------------|
| `HubSpot.Companies.get(args)` | Get a company by ID |
| `HubSpot.Companies.getMany(args)` | Get many companies |
| `HubSpot.Companies.create(args)` | Create a company |
| `HubSpot.Companies.update(args)` | Update a company |
| `HubSpot.Companies.delete(args)` | Delete a company |
| `HubSpot.Companies.getRecentlyCreated(args)` | Get recently created companies |
| `HubSpot.Companies.getRecentlyUpdated(args)` | Get recently updated companies |
| `HubSpot.Companies.searchByDomain(args)` | Search company by domain |

### Deals

| Method | Description |
|--------|-------------|
| `HubSpot.Deals.get(args)` | Get a deal by ID |
| `HubSpot.Deals.getMany(args)` | Get many deals |
| `HubSpot.Deals.create(args)` | Create a deal |
| `HubSpot.Deals.update(args)` | Update a deal |
| `HubSpot.Deals.delete(args)` | Delete a deal |
| `HubSpot.Deals.getRecentlyCreated(args)` | Get recently created deals |
| `HubSpot.Deals.getRecentlyUpdated(args)` | Get recently updated deals |
| `HubSpot.Deals.search(args)` | Search deals |

### Tickets

| Method | Description |
|--------|-------------|
| `HubSpot.Tickets.get(args)` | Get a ticket by ID |
| `HubSpot.Tickets.getMany(args)` | Get many tickets |
| `HubSpot.Tickets.create(args)` | Create a ticket |
| `HubSpot.Tickets.update(args)` | Update a ticket |
| `HubSpot.Tickets.delete(args)` | Delete a ticket |

### Engagements

| Method | Description |
|--------|-------------|
| `HubSpot.Engagements.get(args)` | Get an engagement by ID |
| `HubSpot.Engagements.getMany(args)` | Get many engagements |
| `HubSpot.Engagements.create(args)` | Create an engagement |
| `HubSpot.Engagements.delete(args)` | Delete an engagement |

### Contact Lists

| Method | Description |
|--------|-------------|
| `HubSpot.ContactLists.addContact(args)` | Add a contact to a list |
| `HubSpot.ContactLists.removeContact(args)` | Remove a contact from a list |

## Error Handling

The SDK throws `ApiError` for API errors:

```typescript
import { ApiError } from '@corsair/hubspot-sdk';

try {
  await HubSpot.Contacts.get({ contactId: '12345' });
} catch (error) {
  if (error instanceof ApiError) {
    console.error('API Error:', error.status, error.message);
    console.error('Response body:', error.body);
  }
}
```

## Rate Limiting

HubSpot has rate limits. The SDK will throw an error with status 429 when rate limited. You can handle this:

```typescript
try {
  await HubSpot.Contacts.getMany();
} catch (error) {
  if (error instanceof ApiError && error.status === 429) {
    // Handle rate limit
    const retryAfter = error.headers?.['retry-after'];
    console.log(`Rate limited. Retry after ${retryAfter} seconds`);
  }
}
```

## Webhook Handling

The SDK includes a webhook handler for processing HubSpot webhook events:

```typescript
import { createWebhookHandler } from '@corsair/hubspot-sdk';

const handler = createWebhookHandler({
  secret: process.env.HUBSPOT_WEBHOOK_SECRET, // Optional, for signature verification
});

// Handle contact creation
handler.on('contact.creation', async (event) => {
  console.log('Contact created:', event.objectId);
  // Your logic here
});

// Handle contact updates
handler.on('contact.propertyChange', async (event) => {
  console.log('Contact updated:', event.objectId);
  console.log('Property:', event.propertyName);
  console.log('New value:', event.propertyValue);
});

// Process webhook payload
const result = await handler.handleWebhook(headers, payload);
if (result.success) {
  console.log('Webhook processed:', result.eventType);
}
```

### Running the Webhook Test Server

The SDK includes a test server for receiving and testing webhooks:

```bash
# Start the webhook server
npm run webhook-server

# In another terminal, expose it with ngrok
ngrok http 3000
```

The server will:
- Receive webhook events from HubSpot
- Log event details to the console
- Save webhook payloads to `tests/fixtures/` for testing
- Display a helpful setup page at `http://localhost:3000`

### Testing Webhooks

After capturing webhook events, you can test them:

```bash
# Run webhook tests (uses fixtures from webhook server)
npm test -- webhooks.test.ts
```

## Environment Variables

Create a `.env` file in the project root with the following variables:

```env
# Required: HubSpot API Access Token
HUBSPOT_ACCESS_TOKEN=pat-your-access-token-here

# Optional: Override API base URL (default: https://api.hubapi.com)
HUBSPOT_BASE_URL=https://api.hubapi.com

# Optional: Webhook secret for signature verification
HUBSPOT_WEBHOOK_SECRET=your-webhook-secret-here

# Optional: Test timeout in milliseconds (default: 30000)
TEST_TIMEOUT=30000

# Optional: Test list ID for contact list tests
TEST_LIST_ID=your-test-list-id-here

# Optional: Webhook server port (default: 3000)
PORT=3000
```

## License

MIT

---

## File: demo/sdk/gmail/README.md

# Gmail SDK

A TypeScript SDK for the Gmail API with full type safety, real API integration tests, and webhook support via Gmail Watch API.

## Features

- 🔒 **OAuth2 Authentication** - Secure authentication with access and refresh tokens
- 📧 **Complete API Coverage** - Messages, Labels, Drafts, Threads, and History
- 🔔 **Webhook Support** - Gmail Watch API integration with push notifications
- ✅ **Type Safe** - Full TypeScript support with comprehensive type definitions
- 🧪 **Real API Tests** - Integration tests using actual Gmail API (no mocks)
- 🎯 **User-Friendly API** - Clean, intuitive facade over Gmail REST API

## Installation

```bash
npm install @corsair/gmail-sdk
```

## Quick Start

```typescript
import { Gmail, OpenAPI } from '@corsair/gmail-sdk';

OpenAPI.TOKEN = 'your-access-token';

const messages = await Gmail.Messages.list('me', 'is:inbox', 10);
console.log(`Found ${messages.messages?.length} messages`);

const labels = await Gmail.Labels.list('me');
console.log(`Found ${labels.labels?.length} labels`);
```

## Authentication Setup

### Step 1: Create Google Cloud Project

1. Go to [Google Cloud Console](https://console.cloud.google.com/)
2. Create a new project or select an existing one
3. Note your Project ID

### Step 2: Enable Gmail API

1. Navigate to **APIs & Services** > **Library**
2. Search for "Gmail API"
3. Click **Enable**

### Step 3: Create OAuth 2.0 Credentials

1. Go to **APIs & Services** > **Credentials**
2. Click **Create Credentials** > **OAuth client ID**
3. Choose **Desktop app** or **Web application**
4. Add authorized redirect URI: `http://localhost:3000/oauth/callback`
5. Save your `Client ID` and `Client Secret`

### Step 4: Get Access Token

Create a script to obtain tokens:

```javascript
const { google } = require('googleapis');
const readline = require('readline');

const oauth2Client = new google.auth.OAuth2(
  'YOUR_CLIENT_ID',
  'YOUR_CLIENT_SECRET',
  'http://localhost:3000/oauth/callback'
);

const scopes = [
  'https://www.googleapis.com/auth/gmail.modify',
  'https://www.googleapis.com/auth/gmail.labels',
  'https://www.googleapis.com/auth/gmail.send',
];

const authUrl = oauth2Client.generateAuthUrl({
  access_type: 'offline',
  scope: scopes,
});

console.log('Authorize this app by visiting:', authUrl);

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

rl.question('Enter the code from that page here: ', async (code) => {
  const { tokens } = await oauth2Client.getToken(code);
  console.log('Refresh Token:', tokens.refresh_token);
  console.log('Access Token:', tokens.access_token);
  rl.close();
});
```

### Step 5: Configure Environment Variables

Create a `.env` file:

```bash
GMAIL_CLIENT_ID=your-client-id.apps.googleusercontent.com
GMAIL_CLIENT_SECRET=your-client-secret
GMAIL_REFRESH_TOKEN=your-refresh-token
GMAIL_ACCESS_TOKEN=your-access-token
GMAIL_USER_ID=me
```

## API Reference

### Messages

```typescript
const messages = await Gmail.Messages.list('me', 'is:inbox', 10);

const message = await Gmail.Messages.get('me', messageId, 'full');

const raw = Buffer.from(emailContent).toString('base64')
  .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const sent = await Gmail.Messages.send('me', { raw });

await Gmail.Messages.delete('me', messageId);

await Gmail.Messages.modify('me', messageId, {
  addLabelIds: ['STARRED'],
  removeLabelIds: ['UNREAD'],
});

await Gmail.Messages.trash('me', messageId);
await Gmail.Messages.untrash('me', messageId);
```

### Labels

```typescript
const labels = await Gmail.Labels.list('me');

const label = await Gmail.Labels.get('me', 'INBOX');

const newLabel = await Gmail.Labels.create('me', {
  name: 'My Custom Label',
  labelListVisibility: 'labelShow',
  messageListVisibility: 'show',
});

await Gmail.Labels.update('me', labelId, {
  id: labelId,
  name: 'Updated Label Name',
});

await Gmail.Labels.delete('me', labelId);
```

### Drafts

```typescript
const drafts = await Gmail.Drafts.list('me', 10);

const draft = await Gmail.Drafts.get('me', draftId, 'full');

const newDraft = await Gmail.Drafts.create('me', {
  message: { raw: encodedEmail },
});

await Gmail.Drafts.update('me', draftId, {
  id: draftId,
  message: { raw: updatedEmail },
});

const sentMessage = await Gmail.Drafts.send('me', {
  id: draftId,
});

await Gmail.Drafts.delete('me', draftId);
```

### Threads

```typescript
const threads = await Gmail.Threads.list('me', 'is:inbox', 10);

const thread = await Gmail.Threads.get('me', threadId, 'full');

await Gmail.Threads.modify('me', threadId, {
  addLabelIds: ['IMPORTANT'],
});

await Gmail.Threads.trash('me', threadId);
await Gmail.Threads.untrash('me', threadId);

await Gmail.Threads.delete('me', threadId);
```

### History

```typescript
const profile = await Gmail.Users.getProfile('me');
const currentHistoryId = profile.historyId;

const history = await Gmail.History.list('me', startHistoryId, 100);

if (history.history) {
  for (const item of history.history) {
    if (item.messagesAdded) {
      console.log('New messages:', item.messagesAdded.length);
    }
    if (item.messagesDeleted) {
      console.log('Deleted messages:', item.messagesDeleted.length);
    }
    if (item.labelsAdded) {
      console.log('Labels added:', item.labelsAdded.length);
    }
  }
}
```

## Webhook Support

### Setting Up Gmail Push Notifications

#### 1. Create Cloud Pub/Sub Topic

```bash
gcloud pubsub topics create gmail-push
```

#### 2. Grant Gmail API Permission

```bash
gcloud pubsub topics add-iam-policy-binding gmail-push \
  --member=serviceAccount:gmail-api-push@system.gserviceaccount.com \
  --role=roles/pubsub.publisher
```

#### 3. Create Push Subscription

```bash
gcloud pubsub subscriptions create gmail-push-sub \
  --topic=gmail-push \
  --push-endpoint=https://your-domain.com/webhook/gmail
```

### Using the Webhook Handler

```typescript
import { createWebhookHandler } from '@corsair/gmail-sdk';

const handler = createWebhookHandler({
  userId: 'me',
  autoFetchHistory: true,
});

handler.on('history', async (event) => {
  console.log('History event:', event.historyId);
});

handler.on('messageReceived', async (event) => {
  console.log('New message:', event.message.id);
  console.log('Snippet:', event.message.snippet);
});

handler.on('messageDeleted', async (event) => {
  console.log('Message deleted:', event.message.id);
});

handler.on('messageLabelChanged', async (event) => {
  console.log('Labels changed for:', event.message.id);
  console.log('Added:', event.labelsAdded);
  console.log('Removed:', event.labelsRemoved);
});

app.post('/webhook/gmail', async (req, res) => {
  const result = await handler.handleRawNotification(req.body);
  res.status(result.success ? 200 : 400).json(result);
});
```

### Starting Watch

```typescript
const watchResponse = await Gmail.Users.watch('me', {
  topicName: 'projects/your-project/topics/gmail-push',
  labelIds: ['INBOX'],
});

console.log('Watch started, expires:', watchResponse.expiration);

handler.setLastHistoryId(watchResponse.historyId!);
```

### Stopping Watch

```typescript
await Gmail.Users.stop('me');
```

## Testing

### Run Tests

```bash
npm test
```

### Run Specific Test Suite

```bash
npm test -- messages.test.ts
npm test -- labels.test.ts
npm test -- drafts.test.ts
npm test -- threads.test.ts
npm test -- webhooks.test.ts
```

### Run Webhook Server

```bash
npm run webhook-server
```

The webhook server will start on port 3000 (configurable via `WEBHOOK_PORT` env var) and listen for Gmail push notifications.

## Required OAuth Scopes

For full SDK functionality, request these scopes:

- `https://www.googleapis.com/auth/gmail.modify` - Read, modify, and delete messages
- `https://www.googleapis.com/auth/gmail.labels` - Manage labels
- `https://www.googleapis.com/auth/gmail.send` - Send emails
- `https://www.googleapis.com/auth/gmail.compose` - Create and manage drafts
- `https://www.googleapis.com/auth/gmail.readonly` - Read-only access (alternative)

## Rate Limits

Gmail API has the following quotas:

- **250 quota units per user per second**
- **1 billion quota units per day**

Most operations cost 5-25 quota units. Monitor your usage in the Google Cloud Console.

## Error Handling

```typescript
try {
  const message = await Gmail.Messages.get('me', messageId);
} catch (error) {
  if (error.status === 404) {
    console.log('Message not found');
  } else if (error.status === 429) {
    console.log('Rate limit exceeded');
  } else if (error.status === 401) {
    console.log('Authentication failed - refresh token');
  } else {
    console.error('Error:', error.message);
  }
}
```

## Examples

### Send an Email

```typescript
function createEmail(to: string, subject: string, body: string): string {
  const email = [
    `To: ${to}`,
    `Subject: ${subject}`,
    'Content-Type: text/plain; charset=utf-8',
    '',
    body,
  ].join('\n');

  return Buffer.from(email)
    .toString('base64')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=+$/, '');
}

const raw = createEmail(
  'recipient@example.com',
  'Hello from Gmail SDK',
  'This is a test email sent via the Gmail API!'
);

const message = await Gmail.Messages.send('me', { raw });
console.log('Message sent:', message.id);
```

### Search Messages

```typescript
const unreadMessages = await Gmail.Messages.list('me', 'is:unread', 20);

const fromSender = await Gmail.Messages.list('me', 'from:sender@example.com');

const withAttachment = await Gmail.Messages.list('me', 'has:attachment');

const dateRange = await Gmail.Messages.list('me', 'after:2024/01/01 before:2024/12/31');
```

### Batch Operations

```typescript
const messageIds = ['msg1', 'msg2', 'msg3'];

await Gmail.Messages.batchModify('me', {
  ids: messageIds,
  addLabelIds: ['STARRED'],
  removeLabelIds: ['UNREAD'],
});
```

## License

MIT

## Contributing

Contributions are welcome! Please open an issue or submit a pull request.

## Support

For issues and questions:
- GitHub Issues: [github.com/your-repo/issues](https://github.com/your-repo/issues)
- Gmail API Documentation: [developers.google.com/gmail/api](https://developers.google.com/gmail/api)

---

## File: demo/sdk/github/README.md

# GitHub API Test Server

Integration test suite for the GitHub API TypeScript client generated from OpenAPI specifications.

## Overview

This project contains integration tests that make real API calls to GitHub's REST API. The tests verify that the generated TypeScript client correctly interacts with the GitHub API endpoints.

## Prerequisites

- **Node.js**: Version 18.x or higher
- **npm**: Version 9.x or higher
- **GitHub Personal Access Token**: Required for authenticated API requests

## Getting Started

### 1. Install Dependencies

```bash
npm install
```

### 2. Configure Environment Variables

Copy the example environment file and configure it with your credentials:

```bash
cp .env.example .env
```

Edit `.env` and add your GitHub personal access token:

```env
GITHUB_TOKEN=your_github_token_here
```

#### How to Get a GitHub Token

1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens)
2. Click "Generate new token (classic)"
3. Give it a descriptive name (e.g., "API Test Token")
4. Select scopes based on what you want to test:
   - For read-only public data: No scopes needed
   - For accessing your private data: Select `repo`, `user`, etc.
5. Click "Generate token"
6. Copy the token and paste it in your `.env` file

**Important**: Keep your token secure and never commit it to version control!

### 3. Run Tests

```bash
# Run all tests
npm test

# Run tests in watch mode (re-runs on file changes)
npm run test:watch

# Run tests with coverage report
npm run test:coverage

# Run tests with verbose output
npm run test:verbose
```

## Project Structure

```
test-server/
├── core/              # API client core functionality
│   ├── ApiError.ts
│   ├── CancelablePromise.ts
│   ├── OpenAPI.ts
│   └── request.ts
├── models/            # TypeScript type definitions (1000+ files)
├── services/          # Service classes for API endpoints
│   ├── ActionsService.ts
│   ├── UsersService.ts
│   ├── ReposService.ts
│   └── ... (40+ services)
├── tests/             # Test files
│   ├── setup.ts       # Test configuration and utilities
│   ├── meta.test.ts   # Tests for Meta/API info endpoints
│   ├── users.test.ts  # Tests for Users API
│   └── repos.test.ts  # Tests for Repositories API
├── .env.example       # Example environment variables
├── .gitignore         # Git ignore rules
├── jest.config.js     # Jest configuration
├── package.json       # Project dependencies and scripts
├── tsconfig.json      # TypeScript configuration
└── README.md          # This file
```

## Test Suites

### MetaService Tests (`tests/meta.test.ts`)

Tests GitHub API metadata endpoints that don't require authentication:

- **API Meta Information**: Get GitHub API configuration and limits
- **API Root**: Get available API endpoint URLs
- **Octocat**: Get ASCII art of GitHub's mascot
- **Zen**: Get random GitHub zen quotes

**Run specific suite:**
```bash
npm test -- meta.test
```

### UsersService Tests (`tests/users.test.ts`)

Tests GitHub Users API endpoints:

- **Get Authenticated User**: Fetch current user's profile (requires token)
- **Get User by Username**: Fetch any public user's profile
- **List Users**: Paginate through GitHub users
- **Error Handling**: Test handling of non-existent users

**Run specific suite:**
```bash
npm test -- users.test
```

### ReposService Tests (`tests/repos.test.ts`)

Tests GitHub Repositories API endpoints:

- **Get Repository**: Fetch public repository details
- **List User Repositories**: Get repositories for a user
- **List Authenticated User Repos**: Get your repositories (requires token)
- **List Commits**: Fetch commit history
- **List Branches**: Get repository branches

**Run specific suite:**
```bash
npm test -- repos.test
```

## Writing New Tests

### 1. Create a New Test File

Create a new file in the `tests/` directory:

```typescript
// tests/gists.test.ts
import { GistsService } from '../services/GistsService';
import { requireToken, handleRateLimit } from './setup';

describe('GistsService - GitHub Gists API', () => {
  describe('list', () => {
    it('should list public gists', async () => {
      try {
        const gists = await GistsService.gistsList({
          perPage: 5,
        });
        
        expect(Array.isArray(gists)).toBe(true);
        expect(gists.length).toBeGreaterThan(0);
        
        console.log('Fetched gists:', gists.length);
      } catch (error) {
        await handleRateLimit(error);
      }
    });
  });
});
```

### 2. Use Helper Functions

The test setup provides several helper functions:

- **`requireToken()`**: Skip tests that require authentication if no token is set
- **`handleRateLimit(error)`**: Handle rate limit errors gracefully
- **`getTestUsername()`**: Get test username from environment
- **`getTestRepo()`**: Get test repository name from environment
- **`isTokenConfigured()`**: Check if GitHub token is configured

### 3. Test Structure Best Practices

- Use descriptive test names
- Group related tests with `describe()` blocks
- Log useful information with `console.log()` for debugging
- Always handle rate limit errors
- Test both success and error cases
- Verify response structure and data types

## Configuration

### Environment Variables

All environment variables are optional except for `GITHUB_TOKEN` (required for authenticated requests):

| Variable | Description | Default |
|----------|-------------|---------|
| `GITHUB_TOKEN` | Personal access token for authentication | - |
| `GITHUB_BASE_URL` | GitHub API base URL | `https://api.github.com` |
| `TEST_TIMEOUT` | Test timeout in milliseconds | `30000` |
| `TEST_GITHUB_USERNAME` | Username for test cases | `octocat` |
| `TEST_GITHUB_REPO` | Repository name for test cases | `Hello-World` |

### Jest Configuration

The Jest configuration in `jest.config.js` includes:

- **TypeScript support**: Using `ts-jest` preset
- **Test timeout**: 30 seconds (configurable via environment)
- **Test pattern**: `**/*.test.ts` files in `tests/` directory
- **Setup file**: Runs `tests/setup.ts` before all tests
- **Coverage**: Configured to cover `services/` and `core/` directories

### TypeScript Configuration

The TypeScript configuration in `tsconfig.json` includes:

- **Target**: ES2020 for modern Node.js
- **Strict mode**: Enabled for type safety
- **Module system**: CommonJS for Node.js compatibility
- **Source maps**: Enabled for debugging

## API Rate Limits

GitHub API has rate limits that you should be aware of:

- **Unauthenticated requests**: 60 requests per hour
- **Authenticated requests**: 5,000 requests per hour
- **Search API**: 30 requests per minute

If you hit rate limits:

1. Tests will log the rate limit error
2. Wait for the rate limit to reset (time shown in error)
3. Use authenticated requests (set `GITHUB_TOKEN`) for higher limits
4. Run specific test suites instead of all tests

Check rate limit status:

```bash
# Using curl with your token
curl -H "Authorization: Bearer YOUR_TOKEN" https://api.github.com/rate_limit
```

## Troubleshooting

### Tests Fail with "GITHUB_TOKEN not configured"

Some tests require authentication. Make sure you:

1. Created a `.env` file (copy from `.env.example`)
2. Added your GitHub personal access token
3. Token has appropriate scopes for the endpoints you're testing

### Tests Timeout

If tests timeout frequently:

1. Check your internet connection
2. Increase timeout in `.env`: `TEST_TIMEOUT=60000`
3. Check GitHub API status: https://www.githubstatus.com/

### Rate Limit Errors

If you see rate limit errors:

1. Make sure `GITHUB_TOKEN` is set (authenticated requests have higher limits)
2. Wait for rate limit to reset (time shown in error message)
3. Run fewer tests at once: `npm test -- repos.test`

### TypeScript Errors

If you see TypeScript compilation errors:

1. Check that dependencies are installed: `npm install`
2. Verify TypeScript version: `npm list typescript`
3. Run type check: `npm run type-check`

## Generated Code

The API client code in `core/`, `models/`, and `services/` directories is generated from GitHub's OpenAPI specification using `openapi-typescript-codegen`. 

**Do not manually edit these files** - they will be overwritten if the code is regenerated.

If you need to modify the client behavior:

1. Edit the generator configuration
2. Regenerate the code
3. Copy to this test project

## Contributing

When adding new tests:

1. Follow the existing test structure
2. Add descriptive test names
3. Include console logging for debugging
4. Test both success and error cases
5. Handle rate limits properly
6. Update this README if adding new test suites

## Resources

- [GitHub REST API Documentation](https://docs.github.com/en/rest)
- [Jest Documentation](https://jestjs.io/docs/getting-started)
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
- [GitHub API Rate Limiting](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting)

## License

MIT

---

## File: demo/minimal/README.md

# Corsair Minimal Demo

A minimal demo showing how easy it is to work with Corsair - great types, simple APIs, and automatic webhook routing.

The sandbox editor on corsair.dev points to this project.

## Files

- **`corsair.ts`** - Setup file showing how to initialize Corsair with multiple integrations
- **`webhooks.ts`** - Express server with a single webhook endpoint that handles all integrations
- **`example.ts`** - Example showing API usage with full TypeScript support

## Key Features Demonstrated

✅ **Easy Setup** - Configure once, use everywhere  
✅ **Great Types** - Full TypeScript autocomplete for all APIs  
✅ **Simple Webhooks** - One endpoint handles all integrations automatically  
✅ **Multi-Tenancy** - Built-in tenant isolation  
✅ **Automatic Sync** - API responses automatically stored in database

If you want to clone a demo and use it with your database, clone `demo/testing`!

---

## File: docs/adapters/client.mdx

---
title: Vanilla Client
---

`createCorsairClient({ baseURL })` returns a typed client that mirrors every route on the [handler](/management/handler). Use it from a Node script, a CLI, a worker, or a non-React frontend.

```ts client.ts
import { createCorsairClient } from "corsair";

const client = createCorsairClient({ baseURL: "/api/corsair" });
```

For React, prefer [`createCorsairReactClient`](/adapters/react) — it wraps this one and adds hooks.

## Reading

```ts reads.ts
await client.tenants.list();                      // GET /tenants
await client.tenants.get("acme");                 // GET /tenants/acme

await client.plugins.list();                      // GET /plugins
await client.plugins.get("github");               // GET /plugins/github

await client.connectionStatus.get({               // GET /connection-status
  tenantId: "acme",
});

await client.permissions.get({ id: "perm_123" });   // GET /permissions/perm_123
await client.permissions.get({ token: "tok_abc" }); // POST /permissions/lookup-by-token

await client.ok();                                // GET /ok
```

Every method is typed against the route's response. Hovering `client.tenants.list()` in your editor shows `Promise<Tenant[]>`, and so on.

`connectionStatus.get` returns a `Record<string, 'connected' | 'missing_credentials' | 'not_connected'>` keyed by plugin id:

```ts
const status = await client.connectionStatus.get({ tenantId: "acme" });
// { github: 'connected', slack: 'not_connected', notion: 'missing_credentials' }
```

## Writing

```ts writes.ts
await client.tenants.create({ id: "acme" });      // POST /tenants
```

Connect/OAuth methods are documented on the [Connect page](/management/connect).

## Options

```ts
createCorsairClient({
  baseURL: "/api/corsair",     // required
  fetch: customFetch,          // optional — override globalThis.fetch
});
```

`baseURL` is the origin + base path the handler is mounted at, e.g. `https://app.example.com/api/corsair`. A trailing slash is tolerated.

`fetch` is optional — see [Custom fetch](#custom-fetch) below.

Need auth headers, custom retry, or interceptors? Pass a wrapped `fetch`:

```ts
const client = createCorsairClient({
  baseURL: "/api/corsair",
  fetch: (input, init) =>
    globalThis.fetch(input, {
      ...init,
      headers: { ...init?.headers, Authorization: `Bearer ${token()}` },
    }),
});
```

## Error handling

Failed requests throw `CorsairClientError`:

```ts
import { CorsairClientError } from "corsair";

try {
  await client.tenants.get("does-not-exist");
} catch (err) {
  if (err instanceof CorsairClientError) {
    err.status;   // number — HTTP status
    err.code;     // string — e.g. "not_found"
    err.message;  // string — human message from the server
    err.extra;    // Record<string, unknown> — any additional fields the server returned
  }
}
```

Network failures (DNS, abort, no response) throw a plain `Error` — `CorsairClientError` is only used when the server responded with a non-2xx body.

## Custom fetch

When you call `createCorsairClient`, the client picks its `fetch` function once — either the one you pass in, or `globalThis.fetch` at that moment. Every later call (`client.tenants.list()`, etc.) uses that same function; it does not re-read `globalThis.fetch` on each request.

In a normal browser or Node 18+ app, this makes no practical difference. `fetch` is already available when you create the client, and it stays the same.

It only matters in two cases:

- **Tests** — your test runner or jsdom may install or replace `fetch` after your client module is imported. Pass an explicit `fetch` so the client uses the right one.
- **Custom behavior** — auth headers, retries, or routing requests to an in-process handler instead of over HTTP.

```ts
// Test: wire the client directly to the handler, no TCP socket
const handler = managementHandler(corsair);
const client = createCorsairClient({
  baseURL: "http://test.local/api/corsair",
  fetch: (input, init) => handler(new Request(String(input), init)),
});
```

If you create the client at module scope and later replace `globalThis.fetch`, the client will not pick up the change. Create the client after your environment is ready, or pass `fetch` explicitly.

---

## File: docs/adapters/handlers.mdx

---
title: Frameworks
sidebarTitle: Frameworks
---

import CreateCorsairHub from '/snippets/create-corsair-hub.mdx';
import MountHandlerTabs from '/snippets/mount-handler-tabs.mdx';

Corsair mounts **one** route. It serves Hub delivery — OAuth callbacks, connect pages, self-registration — and the [management API](/management/handler) in the same place. Every adapter wraps a single primitive, `managementHandler(corsair)`, which returns a `(request: Request) => Promise<Response>`. Pick the tab for your server; the rest of your app is identical.

## Configure Corsair

<CreateCorsairHub />

## Mount the route

<MountHandlerTabs />

## Run and go green

Start your dev server and make one request to `/api/corsair`. On that first request your app self-registers its delivery URL with Hub and the dashboard header dot turns green. See [Delivery URLs](/hub/delivery-urls).

Stays grey? The route isn't reachable at `/api/corsair`. Check the mount path and — on Express and Hono — that `basePath` and the wildcard (`/api/corsair/*`) match the path you mounted.

## What's next

<CardGroup cols={2}>
  <Card title="Hub setup" href="/hub/setup">Create a project, copy keys, reach the green check.</Card>
  <Card title="Add a plugin" href="/guides/plugins">Browse the catalog — GitHub, Slack, Linear, and hundreds more.</Card>
  <Card title="Vanilla client" href="/adapters/client">Call the management API from any JS runtime.</Card>
  <Card title="React hooks" href="/adapters/react">Typed `useTenants`, `useConnectionStatus`, and friends.</Card>
</CardGroup>

---

## File: docs/adapters/react.mdx

---
title: React Hooks
---

`createCorsairReactClient({ baseURL })` returns a bag of typed React hooks built on top of [`createCorsairClient`](/adapters/client). One factory call per app — use the returned hooks anywhere in your component tree.

```tsx corsair-client.ts
"use client";
import { createCorsairReactClient } from "corsair/client/react";

export const {
  useTenants, useTenant, useCreateTenant,
  usePlugins, usePlugin,
  useConnectionStatus,
  usePermission,
  useCreateConnectLink, useOAuthCallback,
  client, // escape hatch — the underlying vanilla client
} = createCorsairReactClient({ baseURL: "/api/corsair" });
```

React 18+ is a peer dependency. If you aren't on React, use the [vanilla client](/adapters/client).

## Read hooks

Read hooks follow the same shape:

```tsx tenants-list.tsx
const { data, loading, error, refetch } = useTenants();
```

| Field      | Type                          | Notes                                   |
| ---------- | ----------------------------- | --------------------------------------- |
| `data`     | the typed response, or `null` | populated on success                    |
| `loading`  | `boolean`                     | `true` while a request is in flight     |
| `error`    | `Error \| null`               | typed error if the call failed          |
| `refetch`  | `() => Promise<void>`         | manual re-trigger                       |

Read hooks re-fetch automatically when their argument changes:

```tsx tenant-detail.tsx
function TenantDetail({ id }: { id: string }) {
  const { data, loading } = useTenant(id);
  // changing `id` triggers a fresh fetch automatically
  if (loading) return <Spinner />;
  return {JSON.stringify(data, null, 2)};
}
```

Available read hooks: `useTenants`, `useTenant(id)`, `usePlugins`, `usePlugin(id)`, `useConnectionStatus({ tenantId })`, `usePermission({ id })` or `usePermission({ token })`.

## Mutation hooks

Mutations stay idle until you call `mutate(input)`:

```tsx create-tenant.tsx
function CreateTenant() {
  const { mutate, loading, error, data } = useCreateTenant();

  return (
    <form onSubmit={async (e) => {
      e.preventDefault();
      const id = new FormData(e.currentTarget).get("id") as string;
      await mutate({ id });
    }}>
      
      Create
      {error && {error.message}}
      {data && Created {data.id}}
    </form>
  );
}
```

Available mutation hooks: `useCreateTenant`, `useCreateConnectLink`, `useOAuthCallback`.

## Connection status

`useConnectionStatus` is the hook your dashboard probably opens with. The response is a `Record<string, 'connected' | 'missing_credentials' | 'not_connected'>` keyed by plugin id:

```tsx connections.tsx
function Connections({ tenantId }: { tenantId: string }) {
  const { data } = useConnectionStatus({ tenantId });
  if (!data) return null;
  return (
    
      {Object.entries(data).map(([plugin, status]) => (
        <li key={plugin}>
          {plugin}: {status === "connected" ? "✓" : "Connect →"}
        </li>
      ))}
    
  );
}
```

For wiring the actual connect-and-authorize flow, see the [Connect page](/management/connect).

## Escape hatch

If a hook doesn't fit (e.g. you need imperative access inside an event handler), reach for `client`:

```tsx escape.tsx
const handleClick = async () => {
  const tenant = await client.tenants.create({ id: "acme" });
  console.log(tenant);
};
```

It is exactly the [vanilla client](/adapters/client), sharing the same `baseURL`.

## What this is not

These hooks are intentionally minimal: no cache, no deduplication, no request reuse. If you want React Query, SWR, or RTK semantics, build them on top of `client` — the hooks here exist to give you typed loading/error/data state without forcing a data-layer choice.