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:
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: [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.
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.
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
npm installConfiguration
Getting Your Slack Credentials
Follow these steps to obtain the necessary tokens and IDs from your Slack workspace:
Step 1: Create a Slack App
- Go to Slack API Apps
- Click "Create New App"
- Choose "From scratch"
- Enter an App Name (e.g., "My SDK App")
- Select your workspace
- Click "Create App"
Step 2: Configure Bot Token Scopes
- In your app settings, go to "OAuth & Permissions" in the left sidebar
- Scroll down to "Scopes" section
- Under "Bot Token Scopes", click "Add an OAuth Scope"
- 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
- Go to "OAuth & Permissions"
- Click "Install to Workspace" at the top
- Review the permissions and click "Allow"
- 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:
- Go to "OAuth & Permissions"
- Under "User Token Scopes", add:
search:read- For searching messages
- Reinstall the app if needed
- Copy the "User OAuth Token" (starts with
xoxp-)
Step 5: Find Channel and User IDs
To find a Channel ID:
- Open Slack in your browser or desktop app
- Right-click on a channel name
- Click "View channel details" or "Copy link"
- The Channel ID is in the URL:
https://app.slack.com/client/TXXXXX/C0123456789- The ID starting with
Cis your Channel ID
- The ID starting with
Alternative method:
- Open the channel
- Click the channel name at the top
- Scroll to the bottom of the modal
- The Channel ID is displayed there
To find a User ID:
- Click on a user's profile
- Click the "..." (More) button
- Click "Copy member ID"
- Or find it in the profile URL:
https://app.slack.com/team/U0123456789
- Or find it in the profile URL:
Step 6: Create Environment File
Copy the example file:
bashcp .env.example .envFill in your values:
envSLACK_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
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:
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
# 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.tsAPI 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
npm installConfiguration
Getting Your PostHog Credentials
Follow these steps to obtain the necessary API key from your PostHog account:
Step 1: Get Your Project API Key
- Log in to your PostHog account
- Navigate to your Project Settings
- Click on Project API Key in the left sidebar
- 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)
- In PostHog, click on your profile icon (top right)
- Go to Personal API Keys
- Click Create Personal API Key
- Give it a name and copy the key
- Important: Store this key securely. You won't be able to see it again.
Step 3: Create Environment File
Copy the example environment file:
bashcp env.example .envOpen the
.envfile 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=3000Replace
your-project-api-key-herewith 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
Start the webhook test server:
bashnpm run webhook-serverIn another terminal, start ngrok to expose your local server:
bashngrok http 3000Copy the HTTPS URL from ngrok (e.g.,
https://abc123.ngrok.io)
Step 2: Create Webhook Destination in PostHog
- Go to your PostHog project settings
- Navigate to Data Pipelines โ Destinations
- Click + New โ Destination โ Webhook
- Enter the Webhook URL:
https://your-ngrok-url.ngrok.io/webhook - Configure filters (optional) to select which events to send
- Click Create & Enable
Step 3: Test Webhooks
With the webhook server running, trigger events in PostHog:
- Capture events using the SDK
- Use PostHog's UI to trigger events
- etc.
Watch your terminal for incoming webhook events
Webhook payloads are automatically saved to
tests/fixtures/for testing
Usage
Basic Usage
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:
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:
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
# 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.tsAPI 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:
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:
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:
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:
# Start the webhook server
npm run webhook-server
# In another terminal, expose it with ngrok
ngrok http 3000The 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:
# Run webhook tests (uses fixtures from webhook server)
npm test -- webhooks.test.tsEnvironment Variables
Create a .env file in the project root with the following variables:
# 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=3000License
MIT
File: demo/sdk/hubspot/README.md
HubSpot SDK
A TypeScript SDK for the HubSpot API with Zod validation.
Installation
npm installConfiguration
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
- Log in to your HubSpot account
- Click the Settings icon (โ๏ธ) in the main navigation bar (top right)
- In the left sidebar, navigate to Integrations โ Private Apps
- You'll see a list of existing private apps (if any)
Step 2: Create a New Private App
- Click the "Create a private app" button (top right)
- 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)
- After creating the app, you'll be on the "Scopes" tab
- 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 |
- Tip: You can search for scopes using the search box at the top
- Click "Save" after selecting your scopes
Step 4: Get Your Access Token
- Navigate to the "Auth" tab (in the app settings)
- You'll see a section labeled "Access token"
- Click the "Show token" button (or eye icon) to reveal the token
- Copy the Access token - it will start with
pat-(Private App Token) - 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
Copy the example environment file:
bashcp env.example .envOpen the
.envfile 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=3000Replace
pat-your-actual-access-token-herewith 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)
- In your Private App settings, go to the "Webhooks" tab
- You'll see a "Client secret" - copy this value
- Add it to your
.envfile asHUBSPOT_WEBHOOK_SECRET - This secret is used to verify webhook signatures for security
Step 2: Set Up Local Webhook Server
Start the webhook test server:
bashnpm run webhook-serverIn another terminal, start ngrok to expose your local server:
bashngrok http 3000Copy the HTTPS URL from ngrok (e.g.,
https://abc123.ngrok.io)
Step 3: Create Webhook Subscription in HubSpot
- Go to your Private App settings โ "Webhooks" tab
- Click "Create subscription" button
- 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
- Webhook URL:
- Click "Create"
- Repeat for each event type you want to receive
Step 4: Test Webhooks
With the webhook server running, trigger events in HubSpot:
- Create a new contact
- Update a company
- Create a deal
- etc.
Watch your terminal for incoming webhook events
Webhook payloads are automatically saved to
tests/fixtures/for testing
Available Webhook Event Types
contact.creation- When a contact is createdcontact.propertyChange- When a contact property changescontact.deletion- When a contact is deletedcontact.privacyDeletion- When a contact is privacy deletedcompany.creation- When a company is createdcompany.propertyChange- When a company property changescompany.deletion- When a company is deleteddeal.creation- When a deal is createddeal.propertyChange- When a deal property changesdeal.deletion- When a deal is deletedticket.creation- When a ticket is createdticket.propertyChange- When a ticket property changesticket.deletion- When a ticket is deletedengagement.creation- When an engagement is createdengagement.deletion- When an engagement is deletedconversation.creation- When a conversation is createdconversation.newMessage- When a new message is added to a conversationconversation.propertyChange- When a conversation property changesconversation.deletion- When a conversation is deletedconversation.privacyDeletion- When a conversation is privacy deletedworkflow.enrollment- When a contact is enrolled in a workflow
Usage
Basic Usage
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:
const contact = await HubSpot.Contacts.get({
contactId: '12345',
token: 'pat-your-token-here',
});With Zod Validation
All request arguments have Zod schemas for validation:
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
# 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.tsAPI 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:
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:
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:
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:
# Start the webhook server
npm run webhook-server
# In another terminal, expose it with ngrok
ngrok http 3000The 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:
# Run webhook tests (uses fixtures from webhook server)
npm test -- webhooks.test.tsEnvironment Variables
Create a .env file in the project root with the following variables:
# 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=3000License
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
npm install @corsair/gmail-sdkQuick Start
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
- Go to Google Cloud Console
- Create a new project or select an existing one
- Note your Project ID
Step 2: Enable Gmail API
- Navigate to APIs & Services > Library
- Search for "Gmail API"
- Click Enable
Step 3: Create OAuth 2.0 Credentials
- Go to APIs & Services > Credentials
- Click Create Credentials > OAuth client ID
- Choose Desktop app or Web application
- Add authorized redirect URI:
http://localhost:3000/oauth/callback - Save your
Client IDandClient Secret
Step 4: Get Access Token
Create a script to obtain tokens:
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:
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=meAPI Reference
Messages
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
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
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
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
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
gcloud pubsub topics create gmail-push2. Grant Gmail API Permission
gcloud pubsub topics add-iam-policy-binding gmail-push \
--member=serviceAccount:[email protected] \
--role=roles/pubsub.publisher3. Create Push Subscription
gcloud pubsub subscriptions create gmail-push-sub \
--topic=gmail-push \
--push-endpoint=https://your-domain.com/webhook/gmailUsing the Webhook Handler
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
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
await Gmail.Users.stop('me');Testing
Run Tests
npm testRun Specific Test Suite
npm test -- messages.test.ts
npm test -- labels.test.ts
npm test -- drafts.test.ts
npm test -- threads.test.ts
npm test -- webhooks.test.tsRun Webhook Server
npm run webhook-serverThe 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 messageshttps://www.googleapis.com/auth/gmail.labels- Manage labelshttps://www.googleapis.com/auth/gmail.send- Send emailshttps://www.googleapis.com/auth/gmail.compose- Create and manage draftshttps://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
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
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
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
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
- Gmail API Documentation: 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
npm install2. Configure Environment Variables
Copy the example environment file and configure it with your credentials:
cp .env.example .envEdit .env and add your GitHub personal access token:
GITHUB_TOKEN=your_github_token_hereHow to Get a GitHub Token
- Go to GitHub Settings > Developer settings > Personal access tokens
- Click "Generate new token (classic)"
- Give it a descriptive name (e.g., "API Test Token")
- 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.
- Click "Generate token"
- Copy the token and paste it in your
.envfile
Important: Keep your token secure and never commit it to version control!
3. Run Tests
# 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:verboseProject 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 fileTest 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:
npm test -- meta.testUsersService 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:
npm test -- users.testReposService 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:
npm test -- repos.testWriting New Tests
1. Create a New Test File
Create a new file in the tests/ directory:
// 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 sethandleRateLimit(error): Handle rate limit errors gracefullygetTestUsername(): Get test username from environmentgetTestRepo(): Get test repository name from environmentisTokenConfigured(): 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-jestpreset - Test timeout: 30 seconds (configurable via environment)
- Test pattern:
**/*.test.tsfiles intests/directory - Setup file: Runs
tests/setup.tsbefore all tests - Coverage: Configured to cover
services/andcore/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:
- Tests will log the rate limit error
- Wait for the rate limit to reset (time shown in error)
- Use authenticated requests (set
GITHUB_TOKEN) for higher limits - Run specific test suites instead of all tests
Check rate limit status:
# Using curl with your token
curl -H "Authorization: Bearer YOUR_TOKEN" https://api.github.com/rate_limitTroubleshooting
Tests Fail with "GITHUB_TOKEN not configured"
Some tests require authentication. Make sure you:
- Created a
.envfile (copy from.env.example) - Added your GitHub personal access token
- Token has appropriate scopes for the endpoints you're testing
Tests Timeout
If tests timeout frequently:
- Check your internet connection
- Increase timeout in
.env:TEST_TIMEOUT=60000 - Check GitHub API status: https://www.githubstatus.com/
Rate Limit Errors
If you see rate limit errors:
- Make sure
GITHUB_TOKENis set (authenticated requests have higher limits) - Wait for rate limit to reset (time shown in error message)
- Run fewer tests at once:
npm test -- repos.test
TypeScript Errors
If you see TypeScript compilation errors:
- Check that dependencies are installed:
npm install - Verify TypeScript version:
npm list typescript - 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:
- Edit the generator configuration
- Regenerate the code
- Copy to this test project
Contributing
When adding new tests:
- Follow the existing test structure
- Add descriptive test names
- Include console logging for debugging
- Test both success and error cases
- Handle rate limits properly
- 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 integrationswebhooks.ts- Express server with a single webhook endpoint that handles all integrationsexample.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.
import { createCorsairClient } from "corsair";
const client = createCorsairClient({ baseURL: "/api/corsair" });For React, prefer createCorsairReactClient โ it wraps this one and adds hooks.
Reading
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 /okEvery 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:
const status = await client.connectionStatus.get({ tenantId: "acme" });
// { github: 'connected', slack: 'not_connected', notion: 'missing_credentials' }Writing
await client.tenants.create({ id: "acme" }); // POST /tenantsConnect/OAuth methods are documented on the Connect page.
Options
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:
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:
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
fetchafter your client module is imported. Pass an explicitfetchso the client uses the right one. - Custom behavior โ auth headers, retries, or routing requests to an in-process handler instead of over HTTP.
// 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
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.
"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:
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:
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):
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:
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:
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.