corsair Documentation

Your Agent's Integration Layer

5,482 stars TypeScript TypeScript 1 file ยท ~16,360 tokens
RAW Doc

File: README.md

Corsair: The Unified Integration Layer for Agents

โญ the repo ยท Website ยท Discord ยท X

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


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:

text
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:

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

  โš ๏ธ gmail: messages.post
     To: [email protected]
     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. 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 for details.


Star History


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
  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
  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: '[email protected]',
    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
  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
  1. Tip: You can search for scopes using the search box at the top
  2. 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:

  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: '[email protected]',
    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
  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:[email protected] \
  --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(
  '[email protected]',
  '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:[email protected]');

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:


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
  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

text
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

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. 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 โ€” 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.

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 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 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

Mount the route

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.

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

Create a project, copy keys, reach the green check. Browse the catalog โ€” GitHub, Slack, Linear, and hundreds more. Call the management API from any JS runtime. Typed `useTenants`, `useConnectionStatus`, and friends.

File: docs/adapters/react.mdx


title: React Hooks

createCorsairReactClient({ baseURL }) returns a bag of typed React hooks built on top of createCorsairClient. 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.

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.

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, 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.