## File: README.md # Corsair: The Unified Integration Layer for Agents ⭐ the repo · [Website](https://corsair.dev) · [Discord](https://discord.gg/uNgCP3mSzU) · [X](https://x.com/corsairdotdev) Corsair is the unified integration layer for your agents. Connect your Corsair instance to your agent and immediately get access to every integration. Your agent never sees the credentials, and you control exactly what it can do. [https://github.com/user-attachments/assets/a5db555a-7688-447d-9777-33f3dddfb03d](https://github.com/user-attachments/assets/a5db555a-7688-447d-9777-33f3dddfb03d) --- ## Why this exists Agents are now capable of anything. It feels silly to do a routine task manually. But you do it anyways, because giving agents the keys to all your apps feels reckless. One misunderstood instruction and they're sending an email you'd never send. Corsair allows you to safely integrate with any app. Connect the Corsair MCP to any agent and have built-in tool calls, permissions, and scoped auth. --- ## Example: Sending an Email You're away from your computer, so you ask your agent to send an email: ``` Send Sarah the Q1 numbers from the Financials folder in Drive. ``` Using Corsair, your agent calls Google Drive and then Gmail. You have permissions set up so your agent can't send an email without you seeing. Corsair intercepts the Gmail call, sees it's a send action, and creates a permission request: ``` Agent: I've drafted the email. This action requires your approval before it sends. ⚠️ gmail: messages.post To: sarah@corsair.dev Subject: "Q1 Numbers" Hi Sarah, attached is the breakdown we discussed on the call. Best, Claude Review and approve: https://somepubliclink.com/review/a8f2c1 Link expires in 10 minutes. ``` You open the link. Why does it say "Best, Claude"? You deny the permission, scold Claude, and it sends you a new request. --- ## Permission modes Each integration has its own mode. Set GitHub to strict and Slack to cautious based on how much you trust each surface. - **open** — everything runs immediately - **cautious** _(recommended)_ — reads and writes run immediately; destructive actions require approval - **strict** — reads run immediately; writes require approval; destructive actions are blocked - **readonly** — reads only; all writes and destructive actions are blocked You can also override individual endpoints within any mode. For example, set Slack to `open` but require approval before sending any message. --- ## Multi-tenancy Corsair is built for production. Set `multiTenancy: true` and every tenant gets isolated credentials, isolated data storage, and isolated permissions handling. You can scope a request to a tenant id and Corsair ensures there is no cross-contamination. ```typescript import { github } from '@corsair-dev/github'; import { slack } from '@corsair-dev/slack'; import { createCorsair } from 'corsair/core'; const corsair = createCorsair({ multiTenancy: true, plugins: [slack(), github()], }); const client = corsair.withTenant('org-456'); await client.slack.api.messages.post({ channel: '#alerts', text: 'Deploy complete.' }); ``` --- ## Webhooks Every plugin is shipped with typed, signature-verified webhook handlers. All webhooks point to a single endpoint. Set it and forget it. ```typescript import { processWebhook } from 'corsair'; app.post('/webhooks', async (req, res) => { const webhook = await processWebhook(corsair, req.headers, req.body) return res.json(webhook.response) }); ``` --- ## FAQ Where are credentials stored? In an encrypted database using envelope encryption. A KEK you control encrypts per-tenant data keys, which encrypt the actual secrets. If you'd rather manage keys yourself, pass them directly and skip the key manager. Does the agent ever see my API keys? No. The agent sees method names and results. Credentials are resolved internally by Corsair at call time. The agent cannot read, log, or exfiltrate them. What happens if I deny an approval request? The action is discarded. Nothing is sent, created, or modified. Your agent can try again with corrected parameters and will send a new approval request. Can I use Corsair with multiple tenants? Yes. Set `multiTenancy: true` and each tenant gets isolated credentials, data storage, and permission evaluation. Endpoint discovery is available at the root and doesn't require a tenant. Can I use Corsair alongside direct SDK calls? Yes. Corsair is a library. Use it where the permission layer and key management help, and drop down to individual SDKs when you need custom logic. Can the agent go around the permission request? No. Corsair creates a permission request in a database the agent doesn't have access to. Your agent cannot get past the permission request until that database row is set to `approved`. What integrations do you currently support? For the full list of integrations, see our [docs](https://docs.corsair.dev/guides/plugins). We're adding integrations regularly. If there's an integration you need, create a Github issue. --- ## License Licensed under the Apache License, Version 2.0. See [LICENSE](https://github.com/corsairdev/corsair/blob/main/LICENSE) for details. --- ## Star History --- ## File: demo/sdk/slack/README.md # Slack SDK A TypeScript SDK for the Slack API with Zod validation. ## Installation ```bash npm install ``` ## Configuration ### Getting Your Slack Credentials Follow these steps to obtain the necessary tokens and IDs from your Slack workspace: #### Step 1: Create a Slack App 1. Go to [Slack API Apps](https://api.slack.com/apps) 2. Click **"Create New App"** 3. Choose **"From scratch"** 4. Enter an App Name (e.g., "My SDK App") 5. Select your workspace 6. Click **"Create App"** #### Step 2: Configure Bot Token Scopes 1. In your app settings, go to **"OAuth & Permissions"** in the left sidebar 2. Scroll down to **"Scopes"** section 3. Under **"Bot Token Scopes"**, click **"Add an OAuth Scope"** 4. Add the following scopes based on what you need: | Scope | Description | Required For | |-------|-------------|--------------| | `channels:read` | View basic channel info | Listing channels | | `channels:write` | Manage channels | Creating/archiving channels | | `channels:history` | View messages in channels | Getting channel history | | `chat:write` | Send messages | Posting messages | | `groups:read` | View private channels | Listing private channels | | `groups:write` | Manage private channels | Creating private channels | | `im:read` | View direct messages | Listing DMs | | `im:write` | Start direct messages | Opening DMs | | `mpim:read` | View group DMs | Listing group DMs | | `users:read` | View users | Getting user info | | `users:read.email` | View email addresses | Getting user emails | | `userGroups:read` | View user groups | Listing user groups | | `userGroups:write` | Manage user groups | Creating user groups | | `files:read` | View files | Getting file info | | `files:write` | Upload files | Uploading files | | `reactions:read` | View reactions | Getting reactions | | `reactions:write` | Add/remove reactions | Adding reactions | | `stars:read` | View starred items | Listing stars | | `stars:write` | Add/remove stars | Managing stars | #### Step 3: Install App to Workspace 1. Go to **"OAuth & Permissions"** 2. Click **"Install to Workspace"** at the top 3. Review the permissions and click **"Allow"** 4. Copy the **"Bot User OAuth Token"** (starts with `xoxb-`) #### Step 4: Get User Token (Optional) For some APIs like `search.messages`, you need a User Token: 1. Go to **"OAuth & Permissions"** 2. Under **"User Token Scopes"**, add: - `search:read` - For searching messages 3. Reinstall the app if needed 4. Copy the **"User OAuth Token"** (starts with `xoxp-`) #### Step 5: Find Channel and User IDs **To find a Channel ID:** 1. Open Slack in your browser or desktop app 2. Right-click on a channel name 3. Click **"View channel details"** or **"Copy link"** 4. The Channel ID is in the URL: `https://app.slack.com/client/TXXXXX/C0123456789` - The ID starting with `C` is your Channel ID **Alternative method:** 1. Open the channel 2. Click the channel name at the top 3. Scroll to the bottom of the modal 4. The Channel ID is displayed there **To find a User ID:** 1. Click on a user's profile 2. Click the **"..."** (More) button 3. Click **"Copy member ID"** - Or find it in the profile URL: `https://app.slack.com/team/U0123456789` #### Step 6: Create Environment File 1. Copy the example file: ```bash cp .env.example .env ``` 2. Fill in your values: ```env SLACK_BOT_TOKEN=xoxb-your-actual-bot-token SLACK_USER_TOKEN=xoxp-your-actual-user-token TEST_SLACK_CHANNEL=C0123456789 TEST_SLACK_USER=U0123456789 ``` ## Usage ### Basic Usage ```typescript import { Slack, OpenAPI } from '@corsair/slack-sdk'; // Configure the token OpenAPI.TOKEN = process.env.SLACK_BOT_TOKEN; // List channels const channels = await Slack.Channels.list({ limit: 10 }); console.log(channels); // Send a message const message = await Slack.Messages.send({ channel: 'C0123456789', text: 'Hello from the SDK!', }); console.log(message); // Get user info const user = await Slack.Users.get({ user: 'U0123456789' }); console.log(user); ``` ### Available APIs - **Slack.Channels** - Channel management (archive, create, get, list, invite, etc.) - **Slack.Users** - User information (get, list, getProfile, getPresence, updateProfile) - **Slack.Usergroups** - User group management (create, disable, enable, list, update) - **Slack.Files** - File operations (get, list, upload) - **Slack.Messages** - Messaging (send, update, delete, search, getPermalink) - **Slack.Reactions** - Reaction management (add, get, remove) - **Slack.Stars** - Star management (add, remove, list) ### With Zod Validation All request arguments have Zod schemas for validation: ```typescript import { ChatPostMessageArgsSchema } from '@corsair/slack-sdk'; const args = { channel: 'C0123456789', text: 'Hello!', }; // Validate before sending const result = ChatPostMessageArgsSchema.safeParse(args); if (result.success) { await Slack.Messages.send(result.data); } else { console.error('Validation failed:', result.error); } ``` ## Running Tests ```bash # Run all tests npm test # Run tests with coverage npm run test:coverage # Run tests in watch mode npm run test:watch # Run only model validation tests (no token required) npm test -- models.test.ts ``` ## API Reference ### Channels | Method | Description | |--------|-------------| | `Slack.Channels.archive(args)` | Archive a channel | | `Slack.Channels.close(args)` | Close a direct message | | `Slack.Channels.create(args)` | Create a channel | | `Slack.Channels.get(args)` | Get channel info | | `Slack.Channels.list(args)` | List channels | | `Slack.Channels.getHistory(args)` | Get channel message history | | `Slack.Channels.invite(args)` | Invite users to a channel | | `Slack.Channels.join(args)` | Join a channel | | `Slack.Channels.kick(args)` | Remove a user from a channel | | `Slack.Channels.leave(args)` | Leave a channel | | `Slack.Channels.getMembers(args)` | Get channel members | | `Slack.Channels.open(args)` | Open a direct message | | `Slack.Channels.rename(args)` | Rename a channel | | `Slack.Channels.getReplies(args)` | Get thread replies | | `Slack.Channels.setPurpose(args)` | Set channel purpose | | `Slack.Channels.setTopic(args)` | Set channel topic | | `Slack.Channels.unarchive(args)` | Unarchive a channel | ### Users | Method | Description | |--------|-------------| | `Slack.Users.get(args)` | Get user info | | `Slack.Users.list(args)` | List users | | `Slack.Users.getProfile(args)` | Get user profile | | `Slack.Users.getPresence(args)` | Get user presence | | `Slack.Users.updateProfile(args)` | Update user profile | ### Messages | Method | Description | |--------|-------------| | `Slack.Messages.send(args)` | Send a message | | `Slack.Messages.update(args)` | Update a message | | `Slack.Messages.delete(args)` | Delete a message | | `Slack.Messages.search(args)` | Search messages | | `Slack.Messages.getPermalink(args)` | Get message permalink | ### Reactions | Method | Description | |--------|-------------| | `Slack.Reactions.add(args)` | Add a reaction | | `Slack.Reactions.get(args)` | Get reactions | | `Slack.Reactions.remove(args)` | Remove a reaction | ### Stars | Method | Description | |--------|-------------| | `Slack.Stars.add(args)` | Star an item | | `Slack.Stars.remove(args)` | Unstar an item | | `Slack.Stars.list(args)` | List starred items | ### Files | Method | Description | |--------|-------------| | `Slack.Files.get(args)` | Get file info | | `Slack.Files.list(args)` | List files | | `Slack.Files.upload(args)` | Upload a file | ### Usergroups | Method | Description | |--------|-------------| | `Slack.Usergroups.create(args)` | Create a user group | | `Slack.Usergroups.disable(args)` | Disable a user group | | `Slack.Usergroups.enable(args)` | Enable a user group | | `Slack.Usergroups.list(args)` | List user groups | | `Slack.Usergroups.update(args)` | Update a user group | ## License MIT --- ## File: demo/sdk/posthog/README.md # PostHog SDK A TypeScript SDK for the PostHog API with Zod validation. ## Installation ```bash npm install ``` ## Configuration ### Getting Your PostHog Credentials Follow these steps to obtain the necessary API key from your PostHog account: #### Step 1: Get Your Project API Key 1. Log in to your [PostHog account](https://app.posthog.com) 2. Navigate to your **Project Settings** 3. Click on **Project API Key** in the left sidebar 4. Copy your **Project API Key** - this is the key you'll use for capturing events #### Step 2: Get Your Personal API Key (Optional, for API access) 1. In PostHog, click on your profile icon (top right) 2. Go to **Personal API Keys** 3. Click **Create Personal API Key** 4. Give it a name and copy the key 5. **Important**: Store this key securely. You won't be able to see it again. #### Step 3: Create Environment File 1. Copy the example environment file: ```bash cp env.example .env ``` 2. Open the `.env` file and fill in your values: ```env # PostHog API Configuration POSTHOG_API_KEY=your-project-api-key-here POSTHOG_API_HOST=https://app.posthog.com # Optional: Personal API Key for API access POSTHOG_PERSONAL_API_KEY=your-personal-api-key-here # Webhook Configuration (optional, for webhook testing) POSTHOG_WEBHOOK_SECRET=your-webhook-secret-here # Test Configuration TEST_TIMEOUT=30000 # Webhook Server Configuration PORT=3000 ``` 3. Replace `your-project-api-key-here` with the Project API Key you copied in Step 1 ### Setting Up Webhooks To receive webhook events from PostHog, follow these steps: #### Step 1: Set Up Local Webhook Server 1. Start the webhook test server: ```bash npm run webhook-server ``` 2. In another terminal, start ngrok to expose your local server: ```bash ngrok http 3000 ``` 3. Copy the HTTPS URL from ngrok (e.g., `https://abc123.ngrok.io`) #### Step 2: Create Webhook Destination in PostHog 1. Go to your PostHog project settings 2. Navigate to **Data Pipelines** → **Destinations** 3. Click **+ New** → **Destination** → **Webhook** 4. Enter the Webhook URL: `https://your-ngrok-url.ngrok.io/webhook` 5. Configure filters (optional) to select which events to send 6. Click **Create & Enable** #### Step 3: Test Webhooks 1. With the webhook server running, trigger events in PostHog: - Capture events using the SDK - Use PostHog's UI to trigger events - etc. 2. Watch your terminal for incoming webhook events 3. Webhook payloads are automatically saved to `tests/fixtures/` for testing ## Usage ### Basic Usage ```typescript import { PostHog, OpenAPI } from '@corsair/posthog-sdk'; // Configure the API key OpenAPI.TOKEN = process.env.POSTHOG_API_KEY; // Create an event const event = await PostHog.Events.create({ distinct_id: 'user-123', event: 'button_clicked', properties: { button_name: 'Sign Up', page: '/home', }, }); console.log(event); // Create an identity const identity = await PostHog.Identity.create({ distinct_id: 'user-123', properties: { email: 'user@example.com', name: 'John Doe', plan: 'premium', }, }); console.log(identity); // Track a page view const pageView = await PostHog.Track.trackPage({ distinct_id: 'user-123', url: 'https://example.com/products', properties: { title: 'Products Page', }, }); console.log(pageView); // Track a screen view const screenView = await PostHog.Track.trackScreen({ distinct_id: 'user-123', screen_name: 'HomeScreen', properties: { app_version: '1.0.0', platform: 'ios', }, }); console.log(screenView); // Create an alias const alias = await PostHog.Alias.create({ distinct_id: 'user-123', alias: 'old-user-id', }); console.log(alias); ``` ### Available APIs - **PostHog.Alias** - Alias management (create) - **PostHog.Events** - Event tracking (create) - **PostHog.Identity** - User identification (create) - **PostHog.Track** - Page and screen tracking (trackPage, trackScreen) ### With Per-Request Token You can also pass a token per request: ```typescript const event = await PostHog.Events.create({ distinct_id: 'user-123', event: 'button_clicked', token: 'your-api-key-here', }); ``` ### With Zod Validation All request arguments have Zod schemas for validation: ```typescript import { CreateEventArgsSchema } from '@corsair/posthog-sdk'; const args = { distinct_id: 'user-123', event: 'button_clicked', properties: { button_name: 'Sign Up', }, }; // Validate before sending const result = CreateEventArgsSchema.safeParse(args); if (result.success) { await PostHog.Events.create(result.data); } else { console.error('Validation failed:', result.error); } ``` ## Running Tests ```bash # Run all tests npm test # Run tests with coverage npm run test:coverage # Run tests in watch mode npm run test:watch # Run only model validation tests (no token required) npm test -- models.test.ts ``` ## API Reference ### Alias | Method | Description | |--------|-------------| | `PostHog.Alias.create(args)` | Create an alias to link two distinct IDs | ### Events | Method | Description | |--------|-------------| | `PostHog.Events.create(args)` | Create a custom event | ### Identity | Method | Description | |--------|-------------| | `PostHog.Identity.create(args)` | Create or update user identity properties | ### Track | Method | Description | |--------|-------------| | `PostHog.Track.trackPage(args)` | Track a page view | | `PostHog.Track.trackScreen(args)` | Track a screen view (mobile apps) | ## Error Handling The SDK throws `ApiError` for API errors: ```typescript import { ApiError } from '@corsair/posthog-sdk'; try { await PostHog.Events.create({ distinct_id: 'user-123', event: 'test_event', }); } catch (error) { if (error instanceof ApiError) { console.error('API Error:', error.status, error.message); console.error('Response body:', error.body); } } ``` ## Rate Limiting PostHog has rate limits. The SDK will throw an error with status 429 when rate limited. You can handle this: ```typescript try { await PostHog.Events.create({ distinct_id: 'user-123', event: 'test_event', }); } catch (error) { if (error instanceof ApiError && error.status === 429) { // Handle rate limit const retryAfter = error.headers?.['retry-after']; console.log(`Rate limited. Retry after ${retryAfter} seconds`); } } ``` ## Webhook Handling The SDK includes a webhook handler for processing PostHog webhook events: ```typescript import { createWebhookHandler } from '@corsair/posthog-sdk'; const handler = createWebhookHandler({ secret: process.env.POSTHOG_WEBHOOK_SECRET, // Optional, for signature verification }); // Handle event captured handler.on('event.captured', async (event) => { console.log('Event captured:', event.event); console.log('User:', event.distinct_id); console.log('Properties:', event.properties); // Your logic here }); // Process webhook payload const result = await handler.handleWebhook(headers, payload); if (result.success) { console.log('Webhook processed:', result.eventType); } ``` ### Running the Webhook Test Server The SDK includes a test server for receiving and testing webhooks: ```bash # Start the webhook server npm run webhook-server # In another terminal, expose it with ngrok ngrok http 3000 ``` The server will: - Receive webhook events from PostHog - Log event details to the console - Save webhook payloads to `tests/fixtures/` for testing - Display a helpful setup page at `http://localhost:3000` ### Testing Webhooks After capturing webhook events, you can test them: ```bash # Run webhook tests (uses fixtures from webhook server) npm test -- webhooks.test.ts ``` ## Environment Variables Create a `.env` file in the project root with the following variables: ```env # Required: PostHog Project API Key POSTHOG_API_KEY=your-project-api-key-here # Optional: Override API base URL (default: https://app.posthog.com) POSTHOG_API_HOST=https://app.posthog.com # Optional: Personal API Key for API access POSTHOG_PERSONAL_API_KEY=your-personal-api-key-here # Optional: Webhook secret for signature verification POSTHOG_WEBHOOK_SECRET=your-webhook-secret-here # Optional: Test timeout in milliseconds (default: 30000) TEST_TIMEOUT=30000 # Optional: Webhook server port (default: 3000) PORT=3000 ``` ## License MIT --- ## File: demo/sdk/hubspot/README.md # HubSpot SDK A TypeScript SDK for the HubSpot API with Zod validation. ## Installation ```bash npm install ``` ## Configuration ### Getting Your HubSpot Credentials Follow these steps to obtain the necessary access token from your HubSpot account (updated for 2024/2025): #### Step 1: Navigate to Private Apps 1. Log in to your [HubSpot account](https://app.hubspot.com) 2. Click the **Settings** icon (⚙️) in the main navigation bar (top right) 3. In the left sidebar, navigate to **Integrations** → **Private Apps** 4. You'll see a list of existing private apps (if any) #### Step 2: Create a New Private App 1. Click the **"Create a private app"** button (top right) 2. In the **"Basic Info"** tab: - Enter an **App name** (e.g., "My SDK App" or "Workflow Integration") - Optionally add a **Description** - Click **"Create app"** at the bottom #### Step 3: Configure Scopes (Permissions) 1. After creating the app, you'll be on the **"Scopes"** tab 2. Select the scopes you need based on what you want to do. Here are the recommended scopes: | Scope | Description | Required For | |-------|-------------|--------------| | `crm.objects.contacts.read` | Read contacts | Getting contact info, listing contacts | | `crm.objects.contacts.write` | Write contacts | Creating/updating/deleting contacts | | `crm.objects.companies.read` | Read companies | Getting company info, listing companies | | `crm.objects.companies.write` | Write companies | Creating/updating/deleting companies | | `crm.objects.deals.read` | Read deals | Getting deal info, listing deals | | `crm.objects.deals.write` | Write deals | Creating/updating/deleting deals | | `crm.objects.tickets.read` | Read tickets | Getting ticket info, listing tickets | | `crm.objects.tickets.write` | Write tickets | Creating/updating/deleting tickets | | `engagements.read` | Read engagements | Getting engagement info | | `engagements.write` | Write engagements | Creating/deleting engagements | | `contacts.read` | Read contact lists | Viewing contact lists | | `contacts.write` | Write contact lists | Adding/removing contacts from lists | 3. **Tip**: You can search for scopes using the search box at the top 4. Click **"Save"** after selecting your scopes #### Step 4: Get Your Access Token 1. Navigate to the **"Auth"** tab (in the app settings) 2. You'll see a section labeled **"Access token"** 3. Click the **"Show token"** button (or eye icon) to reveal the token 4. Copy the **Access token** - it will start with `pat-` (Private App Token) 5. **Important**: Store this token securely. You won't be able to see it again after closing this page. If you lose it, you'll need to create a new private app. #### Step 5: Create Environment File 1. Copy the example environment file: ```bash cp env.example .env ``` 2. Open the `.env` file and fill in your values: ```env # HubSpot API Configuration HUBSPOT_ACCESS_TOKEN=pat-your-actual-access-token-here HUBSPOT_BASE_URL=https://api.hubapi.com # Webhook Configuration (optional, for webhook testing) HUBSPOT_WEBHOOK_SECRET=your-webhook-secret-here # Test Configuration TEST_TIMEOUT=30000 TEST_LIST_ID=your-test-list-id-here # Webhook Server Configuration PORT=3000 ``` 3. Replace `pat-your-actual-access-token-here` with the token you copied in Step 4 ### Setting Up Webhooks To receive webhook events from HubSpot, follow these steps: #### Step 1: Get Your Webhook Secret (Optional but Recommended) 1. In your Private App settings, go to the **"Webhooks"** tab 2. You'll see a **"Client secret"** - copy this value 3. Add it to your `.env` file as `HUBSPOT_WEBHOOK_SECRET` 4. This secret is used to verify webhook signatures for security #### Step 2: Set Up Local Webhook Server 1. Start the webhook test server: ```bash npm run webhook-server ``` 2. In another terminal, start ngrok to expose your local server: ```bash ngrok http 3000 ``` 3. Copy the HTTPS URL from ngrok (e.g., `https://abc123.ngrok.io`) #### Step 3: Create Webhook Subscription in HubSpot 1. Go to your Private App settings → **"Webhooks"** tab 2. Click **"Create subscription"** button 3. Fill in the subscription details: - **Webhook URL**: `https://your-ngrok-url.ngrok.io/webhook` - **Event type**: Select from the dropdown (e.g., `contact.creation`, `company.creation`, etc.) - **Property name**: (Optional) Leave blank for all properties, or specify a property - **Active**: Check this box to enable the subscription 4. Click **"Create"** 5. Repeat for each event type you want to receive #### Step 4: Test Webhooks 1. With the webhook server running, trigger events in HubSpot: - Create a new contact - Update a company - Create a deal - etc. 2. Watch your terminal for incoming webhook events 3. Webhook payloads are automatically saved to `tests/fixtures/` for testing #### Available Webhook Event Types - `contact.creation` - When a contact is created - `contact.propertyChange` - When a contact property changes - `contact.deletion` - When a contact is deleted - `contact.privacyDeletion` - When a contact is privacy deleted - `company.creation` - When a company is created - `company.propertyChange` - When a company property changes - `company.deletion` - When a company is deleted - `deal.creation` - When a deal is created - `deal.propertyChange` - When a deal property changes - `deal.deletion` - When a deal is deleted - `ticket.creation` - When a ticket is created - `ticket.propertyChange` - When a ticket property changes - `ticket.deletion` - When a ticket is deleted - `engagement.creation` - When an engagement is created - `engagement.deletion` - When an engagement is deleted - `conversation.creation` - When a conversation is created - `conversation.newMessage` - When a new message is added to a conversation - `conversation.propertyChange` - When a conversation property changes - `conversation.deletion` - When a conversation is deleted - `conversation.privacyDeletion` - When a conversation is privacy deleted - `workflow.enrollment` - When a contact is enrolled in a workflow ## Usage ### Basic Usage ```typescript import { HubSpot, OpenAPI } from '@corsair/hubspot-sdk'; // Configure the token OpenAPI.TOKEN = process.env.HUBSPOT_ACCESS_TOKEN; // Get a contact const contact = await HubSpot.Contacts.get({ contactId: '12345', }); console.log(contact); // Create a contact const newContact = await HubSpot.Contacts.createOrUpdate({ properties: { email: 'newcontact@example.com', firstname: 'New', lastname: 'Contact', }, }); console.log(newContact); // Get many companies const companies = await HubSpot.Companies.getMany({ limit: 10 }); console.log(companies); ``` ### Available APIs - **HubSpot.Contacts** - Contact management (get, getMany, createOrUpdate, delete, getRecentlyCreated, getRecentlyUpdated, search) - **HubSpot.Companies** - Company management (get, getMany, create, update, delete, getRecentlyCreated, getRecentlyUpdated, searchByDomain) - **HubSpot.Deals** - Deal management (get, getMany, create, update, delete, getRecentlyCreated, getRecentlyUpdated, search) - **HubSpot.Tickets** - Ticket management (get, getMany, create, update, delete) - **HubSpot.Engagements** - Engagement management (get, getMany, create, delete) - **HubSpot.ContactLists** - Contact list operations (addContact, removeContact) ### With Per-Request Token You can also pass a token per request: ```typescript const contact = await HubSpot.Contacts.get({ contactId: '12345', token: 'pat-your-token-here', }); ``` ### With Zod Validation All request arguments have Zod schemas for validation: ```typescript import { GetContactArgsSchema } from '@corsair/hubspot-sdk'; const args = { contactId: '12345', }; // Validate before sending const result = GetContactArgsSchema.safeParse(args); if (result.success) { await HubSpot.Contacts.get(result.data); } else { console.error('Validation failed:', result.error); } ``` ## Running Tests ```bash # Run all tests npm test # Run tests with coverage npm run test:coverage # Run tests in watch mode npm run test:watch # Run only model validation tests (no token required) npm test -- models.test.ts ``` ## API Reference ### Contacts | Method | Description | |--------|-------------| | `HubSpot.Contacts.get(args)` | Get a contact by ID | | `HubSpot.Contacts.getMany(args)` | Get many contacts | | `HubSpot.Contacts.createOrUpdate(args)` | Create or update a contact | | `HubSpot.Contacts.delete(args)` | Delete a contact | | `HubSpot.Contacts.getRecentlyCreated(args)` | Get recently created contacts | | `HubSpot.Contacts.getRecentlyUpdated(args)` | Get recently updated contacts | | `HubSpot.Contacts.search(args)` | Search contacts | ### Companies | Method | Description | |--------|-------------| | `HubSpot.Companies.get(args)` | Get a company by ID | | `HubSpot.Companies.getMany(args)` | Get many companies | | `HubSpot.Companies.create(args)` | Create a company | | `HubSpot.Companies.update(args)` | Update a company | | `HubSpot.Companies.delete(args)` | Delete a company | | `HubSpot.Companies.getRecentlyCreated(args)` | Get recently created companies | | `HubSpot.Companies.getRecentlyUpdated(args)` | Get recently updated companies | | `HubSpot.Companies.searchByDomain(args)` | Search company by domain | ### Deals | Method | Description | |--------|-------------| | `HubSpot.Deals.get(args)` | Get a deal by ID | | `HubSpot.Deals.getMany(args)` | Get many deals | | `HubSpot.Deals.create(args)` | Create a deal | | `HubSpot.Deals.update(args)` | Update a deal | | `HubSpot.Deals.delete(args)` | Delete a deal | | `HubSpot.Deals.getRecentlyCreated(args)` | Get recently created deals | | `HubSpot.Deals.getRecentlyUpdated(args)` | Get recently updated deals | | `HubSpot.Deals.search(args)` | Search deals | ### Tickets | Method | Description | |--------|-------------| | `HubSpot.Tickets.get(args)` | Get a ticket by ID | | `HubSpot.Tickets.getMany(args)` | Get many tickets | | `HubSpot.Tickets.create(args)` | Create a ticket | | `HubSpot.Tickets.update(args)` | Update a ticket | | `HubSpot.Tickets.delete(args)` | Delete a ticket | ### Engagements | Method | Description | |--------|-------------| | `HubSpot.Engagements.get(args)` | Get an engagement by ID | | `HubSpot.Engagements.getMany(args)` | Get many engagements | | `HubSpot.Engagements.create(args)` | Create an engagement | | `HubSpot.Engagements.delete(args)` | Delete an engagement | ### Contact Lists | Method | Description | |--------|-------------| | `HubSpot.ContactLists.addContact(args)` | Add a contact to a list | | `HubSpot.ContactLists.removeContact(args)` | Remove a contact from a list | ## Error Handling The SDK throws `ApiError` for API errors: ```typescript import { ApiError } from '@corsair/hubspot-sdk'; try { await HubSpot.Contacts.get({ contactId: '12345' }); } catch (error) { if (error instanceof ApiError) { console.error('API Error:', error.status, error.message); console.error('Response body:', error.body); } } ``` ## Rate Limiting HubSpot has rate limits. The SDK will throw an error with status 429 when rate limited. You can handle this: ```typescript try { await HubSpot.Contacts.getMany(); } catch (error) { if (error instanceof ApiError && error.status === 429) { // Handle rate limit const retryAfter = error.headers?.['retry-after']; console.log(`Rate limited. Retry after ${retryAfter} seconds`); } } ``` ## Webhook Handling The SDK includes a webhook handler for processing HubSpot webhook events: ```typescript import { createWebhookHandler } from '@corsair/hubspot-sdk'; const handler = createWebhookHandler({ secret: process.env.HUBSPOT_WEBHOOK_SECRET, // Optional, for signature verification }); // Handle contact creation handler.on('contact.creation', async (event) => { console.log('Contact created:', event.objectId); // Your logic here }); // Handle contact updates handler.on('contact.propertyChange', async (event) => { console.log('Contact updated:', event.objectId); console.log('Property:', event.propertyName); console.log('New value:', event.propertyValue); }); // Process webhook payload const result = await handler.handleWebhook(headers, payload); if (result.success) { console.log('Webhook processed:', result.eventType); } ``` ### Running the Webhook Test Server The SDK includes a test server for receiving and testing webhooks: ```bash # Start the webhook server npm run webhook-server # In another terminal, expose it with ngrok ngrok http 3000 ``` The server will: - Receive webhook events from HubSpot - Log event details to the console - Save webhook payloads to `tests/fixtures/` for testing - Display a helpful setup page at `http://localhost:3000` ### Testing Webhooks After capturing webhook events, you can test them: ```bash # Run webhook tests (uses fixtures from webhook server) npm test -- webhooks.test.ts ``` ## Environment Variables Create a `.env` file in the project root with the following variables: ```env # Required: HubSpot API Access Token HUBSPOT_ACCESS_TOKEN=pat-your-access-token-here # Optional: Override API base URL (default: https://api.hubapi.com) HUBSPOT_BASE_URL=https://api.hubapi.com # Optional: Webhook secret for signature verification HUBSPOT_WEBHOOK_SECRET=your-webhook-secret-here # Optional: Test timeout in milliseconds (default: 30000) TEST_TIMEOUT=30000 # Optional: Test list ID for contact list tests TEST_LIST_ID=your-test-list-id-here # Optional: Webhook server port (default: 3000) PORT=3000 ``` ## License MIT --- ## File: demo/sdk/gmail/README.md # Gmail SDK A TypeScript SDK for the Gmail API with full type safety, real API integration tests, and webhook support via Gmail Watch API. ## Features - 🔒 **OAuth2 Authentication** - Secure authentication with access and refresh tokens - 📧 **Complete API Coverage** - Messages, Labels, Drafts, Threads, and History - 🔔 **Webhook Support** - Gmail Watch API integration with push notifications - ✅ **Type Safe** - Full TypeScript support with comprehensive type definitions - 🧪 **Real API Tests** - Integration tests using actual Gmail API (no mocks) - 🎯 **User-Friendly API** - Clean, intuitive facade over Gmail REST API ## Installation ```bash npm install @corsair/gmail-sdk ``` ## Quick Start ```typescript import { Gmail, OpenAPI } from '@corsair/gmail-sdk'; OpenAPI.TOKEN = 'your-access-token'; const messages = await Gmail.Messages.list('me', 'is:inbox', 10); console.log(`Found ${messages.messages?.length} messages`); const labels = await Gmail.Labels.list('me'); console.log(`Found ${labels.labels?.length} labels`); ``` ## Authentication Setup ### Step 1: Create Google Cloud Project 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a new project or select an existing one 3. Note your Project ID ### Step 2: Enable Gmail API 1. Navigate to **APIs & Services** > **Library** 2. Search for "Gmail API" 3. Click **Enable** ### Step 3: Create OAuth 2.0 Credentials 1. Go to **APIs & Services** > **Credentials** 2. Click **Create Credentials** > **OAuth client ID** 3. Choose **Desktop app** or **Web application** 4. Add authorized redirect URI: `http://localhost:3000/oauth/callback` 5. Save your `Client ID` and `Client Secret` ### Step 4: Get Access Token Create a script to obtain tokens: ```javascript const { google } = require('googleapis'); const readline = require('readline'); const oauth2Client = new google.auth.OAuth2( 'YOUR_CLIENT_ID', 'YOUR_CLIENT_SECRET', 'http://localhost:3000/oauth/callback' ); const scopes = [ 'https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/gmail.labels', 'https://www.googleapis.com/auth/gmail.send', ]; const authUrl = oauth2Client.generateAuthUrl({ access_type: 'offline', scope: scopes, }); console.log('Authorize this app by visiting:', authUrl); const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question('Enter the code from that page here: ', async (code) => { const { tokens } = await oauth2Client.getToken(code); console.log('Refresh Token:', tokens.refresh_token); console.log('Access Token:', tokens.access_token); rl.close(); }); ``` ### Step 5: Configure Environment Variables Create a `.env` file: ```bash GMAIL_CLIENT_ID=your-client-id.apps.googleusercontent.com GMAIL_CLIENT_SECRET=your-client-secret GMAIL_REFRESH_TOKEN=your-refresh-token GMAIL_ACCESS_TOKEN=your-access-token GMAIL_USER_ID=me ``` ## API Reference ### Messages ```typescript const messages = await Gmail.Messages.list('me', 'is:inbox', 10); const message = await Gmail.Messages.get('me', messageId, 'full'); const raw = Buffer.from(emailContent).toString('base64') .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const sent = await Gmail.Messages.send('me', { raw }); await Gmail.Messages.delete('me', messageId); await Gmail.Messages.modify('me', messageId, { addLabelIds: ['STARRED'], removeLabelIds: ['UNREAD'], }); await Gmail.Messages.trash('me', messageId); await Gmail.Messages.untrash('me', messageId); ``` ### Labels ```typescript const labels = await Gmail.Labels.list('me'); const label = await Gmail.Labels.get('me', 'INBOX'); const newLabel = await Gmail.Labels.create('me', { name: 'My Custom Label', labelListVisibility: 'labelShow', messageListVisibility: 'show', }); await Gmail.Labels.update('me', labelId, { id: labelId, name: 'Updated Label Name', }); await Gmail.Labels.delete('me', labelId); ``` ### Drafts ```typescript const drafts = await Gmail.Drafts.list('me', 10); const draft = await Gmail.Drafts.get('me', draftId, 'full'); const newDraft = await Gmail.Drafts.create('me', { message: { raw: encodedEmail }, }); await Gmail.Drafts.update('me', draftId, { id: draftId, message: { raw: updatedEmail }, }); const sentMessage = await Gmail.Drafts.send('me', { id: draftId, }); await Gmail.Drafts.delete('me', draftId); ``` ### Threads ```typescript const threads = await Gmail.Threads.list('me', 'is:inbox', 10); const thread = await Gmail.Threads.get('me', threadId, 'full'); await Gmail.Threads.modify('me', threadId, { addLabelIds: ['IMPORTANT'], }); await Gmail.Threads.trash('me', threadId); await Gmail.Threads.untrash('me', threadId); await Gmail.Threads.delete('me', threadId); ``` ### History ```typescript const profile = await Gmail.Users.getProfile('me'); const currentHistoryId = profile.historyId; const history = await Gmail.History.list('me', startHistoryId, 100); if (history.history) { for (const item of history.history) { if (item.messagesAdded) { console.log('New messages:', item.messagesAdded.length); } if (item.messagesDeleted) { console.log('Deleted messages:', item.messagesDeleted.length); } if (item.labelsAdded) { console.log('Labels added:', item.labelsAdded.length); } } } ``` ## Webhook Support ### Setting Up Gmail Push Notifications #### 1. Create Cloud Pub/Sub Topic ```bash gcloud pubsub topics create gmail-push ``` #### 2. Grant Gmail API Permission ```bash gcloud pubsub topics add-iam-policy-binding gmail-push \ --member=serviceAccount:gmail-api-push@system.gserviceaccount.com \ --role=roles/pubsub.publisher ``` #### 3. Create Push Subscription ```bash gcloud pubsub subscriptions create gmail-push-sub \ --topic=gmail-push \ --push-endpoint=https://your-domain.com/webhook/gmail ``` ### Using the Webhook Handler ```typescript import { createWebhookHandler } from '@corsair/gmail-sdk'; const handler = createWebhookHandler({ userId: 'me', autoFetchHistory: true, }); handler.on('history', async (event) => { console.log('History event:', event.historyId); }); handler.on('messageReceived', async (event) => { console.log('New message:', event.message.id); console.log('Snippet:', event.message.snippet); }); handler.on('messageDeleted', async (event) => { console.log('Message deleted:', event.message.id); }); handler.on('messageLabelChanged', async (event) => { console.log('Labels changed for:', event.message.id); console.log('Added:', event.labelsAdded); console.log('Removed:', event.labelsRemoved); }); app.post('/webhook/gmail', async (req, res) => { const result = await handler.handleRawNotification(req.body); res.status(result.success ? 200 : 400).json(result); }); ``` ### Starting Watch ```typescript const watchResponse = await Gmail.Users.watch('me', { topicName: 'projects/your-project/topics/gmail-push', labelIds: ['INBOX'], }); console.log('Watch started, expires:', watchResponse.expiration); handler.setLastHistoryId(watchResponse.historyId!); ``` ### Stopping Watch ```typescript await Gmail.Users.stop('me'); ``` ## Testing ### Run Tests ```bash npm test ``` ### Run Specific Test Suite ```bash npm test -- messages.test.ts npm test -- labels.test.ts npm test -- drafts.test.ts npm test -- threads.test.ts npm test -- webhooks.test.ts ``` ### Run Webhook Server ```bash npm run webhook-server ``` The webhook server will start on port 3000 (configurable via `WEBHOOK_PORT` env var) and listen for Gmail push notifications. ## Required OAuth Scopes For full SDK functionality, request these scopes: - `https://www.googleapis.com/auth/gmail.modify` - Read, modify, and delete messages - `https://www.googleapis.com/auth/gmail.labels` - Manage labels - `https://www.googleapis.com/auth/gmail.send` - Send emails - `https://www.googleapis.com/auth/gmail.compose` - Create and manage drafts - `https://www.googleapis.com/auth/gmail.readonly` - Read-only access (alternative) ## Rate Limits Gmail API has the following quotas: - **250 quota units per user per second** - **1 billion quota units per day** Most operations cost 5-25 quota units. Monitor your usage in the Google Cloud Console. ## Error Handling ```typescript try { const message = await Gmail.Messages.get('me', messageId); } catch (error) { if (error.status === 404) { console.log('Message not found'); } else if (error.status === 429) { console.log('Rate limit exceeded'); } else if (error.status === 401) { console.log('Authentication failed - refresh token'); } else { console.error('Error:', error.message); } } ``` ## Examples ### Send an Email ```typescript function createEmail(to: string, subject: string, body: string): string { const email = [ `To: ${to}`, `Subject: ${subject}`, 'Content-Type: text/plain; charset=utf-8', '', body, ].join('\n'); return Buffer.from(email) .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); } const raw = createEmail( 'recipient@example.com', 'Hello from Gmail SDK', 'This is a test email sent via the Gmail API!' ); const message = await Gmail.Messages.send('me', { raw }); console.log('Message sent:', message.id); ``` ### Search Messages ```typescript const unreadMessages = await Gmail.Messages.list('me', 'is:unread', 20); const fromSender = await Gmail.Messages.list('me', 'from:sender@example.com'); const withAttachment = await Gmail.Messages.list('me', 'has:attachment'); const dateRange = await Gmail.Messages.list('me', 'after:2024/01/01 before:2024/12/31'); ``` ### Batch Operations ```typescript const messageIds = ['msg1', 'msg2', 'msg3']; await Gmail.Messages.batchModify('me', { ids: messageIds, addLabelIds: ['STARRED'], removeLabelIds: ['UNREAD'], }); ``` ## License MIT ## Contributing Contributions are welcome! Please open an issue or submit a pull request. ## Support For issues and questions: - GitHub Issues: [github.com/your-repo/issues](https://github.com/your-repo/issues) - Gmail API Documentation: [developers.google.com/gmail/api](https://developers.google.com/gmail/api) --- ## File: demo/sdk/github/README.md # GitHub API Test Server Integration test suite for the GitHub API TypeScript client generated from OpenAPI specifications. ## Overview This project contains integration tests that make real API calls to GitHub's REST API. The tests verify that the generated TypeScript client correctly interacts with the GitHub API endpoints. ## Prerequisites - **Node.js**: Version 18.x or higher - **npm**: Version 9.x or higher - **GitHub Personal Access Token**: Required for authenticated API requests ## Getting Started ### 1. Install Dependencies ```bash npm install ``` ### 2. Configure Environment Variables Copy the example environment file and configure it with your credentials: ```bash cp .env.example .env ``` Edit `.env` and add your GitHub personal access token: ```env GITHUB_TOKEN=your_github_token_here ``` #### How to Get a GitHub Token 1. Go to [GitHub Settings > Developer settings > Personal access tokens](https://github.com/settings/tokens) 2. Click "Generate new token (classic)" 3. Give it a descriptive name (e.g., "API Test Token") 4. Select scopes based on what you want to test: - For read-only public data: No scopes needed - For accessing your private data: Select `repo`, `user`, etc. 5. Click "Generate token" 6. Copy the token and paste it in your `.env` file **Important**: Keep your token secure and never commit it to version control! ### 3. Run Tests ```bash # Run all tests npm test # Run tests in watch mode (re-runs on file changes) npm run test:watch # Run tests with coverage report npm run test:coverage # Run tests with verbose output npm run test:verbose ``` ## Project Structure ``` test-server/ ├── core/ # API client core functionality │ ├── ApiError.ts │ ├── CancelablePromise.ts │ ├── OpenAPI.ts │ └── request.ts ├── models/ # TypeScript type definitions (1000+ files) ├── services/ # Service classes for API endpoints │ ├── ActionsService.ts │ ├── UsersService.ts │ ├── ReposService.ts │ └── ... (40+ services) ├── tests/ # Test files │ ├── setup.ts # Test configuration and utilities │ ├── meta.test.ts # Tests for Meta/API info endpoints │ ├── users.test.ts # Tests for Users API │ └── repos.test.ts # Tests for Repositories API ├── .env.example # Example environment variables ├── .gitignore # Git ignore rules ├── jest.config.js # Jest configuration ├── package.json # Project dependencies and scripts ├── tsconfig.json # TypeScript configuration └── README.md # This file ``` ## Test Suites ### MetaService Tests (`tests/meta.test.ts`) Tests GitHub API metadata endpoints that don't require authentication: - **API Meta Information**: Get GitHub API configuration and limits - **API Root**: Get available API endpoint URLs - **Octocat**: Get ASCII art of GitHub's mascot - **Zen**: Get random GitHub zen quotes **Run specific suite:** ```bash npm test -- meta.test ``` ### UsersService Tests (`tests/users.test.ts`) Tests GitHub Users API endpoints: - **Get Authenticated User**: Fetch current user's profile (requires token) - **Get User by Username**: Fetch any public user's profile - **List Users**: Paginate through GitHub users - **Error Handling**: Test handling of non-existent users **Run specific suite:** ```bash npm test -- users.test ``` ### ReposService Tests (`tests/repos.test.ts`) Tests GitHub Repositories API endpoints: - **Get Repository**: Fetch public repository details - **List User Repositories**: Get repositories for a user - **List Authenticated User Repos**: Get your repositories (requires token) - **List Commits**: Fetch commit history - **List Branches**: Get repository branches **Run specific suite:** ```bash npm test -- repos.test ``` ## Writing New Tests ### 1. Create a New Test File Create a new file in the `tests/` directory: ```typescript // tests/gists.test.ts import { GistsService } from '../services/GistsService'; import { requireToken, handleRateLimit } from './setup'; describe('GistsService - GitHub Gists API', () => { describe('list', () => { it('should list public gists', async () => { try { const gists = await GistsService.gistsList({ perPage: 5, }); expect(Array.isArray(gists)).toBe(true); expect(gists.length).toBeGreaterThan(0); console.log('Fetched gists:', gists.length); } catch (error) { await handleRateLimit(error); } }); }); }); ``` ### 2. Use Helper Functions The test setup provides several helper functions: - **`requireToken()`**: Skip tests that require authentication if no token is set - **`handleRateLimit(error)`**: Handle rate limit errors gracefully - **`getTestUsername()`**: Get test username from environment - **`getTestRepo()`**: Get test repository name from environment - **`isTokenConfigured()`**: Check if GitHub token is configured ### 3. Test Structure Best Practices - Use descriptive test names - Group related tests with `describe()` blocks - Log useful information with `console.log()` for debugging - Always handle rate limit errors - Test both success and error cases - Verify response structure and data types ## Configuration ### Environment Variables All environment variables are optional except for `GITHUB_TOKEN` (required for authenticated requests): | Variable | Description | Default | |----------|-------------|---------| | `GITHUB_TOKEN` | Personal access token for authentication | - | | `GITHUB_BASE_URL` | GitHub API base URL | `https://api.github.com` | | `TEST_TIMEOUT` | Test timeout in milliseconds | `30000` | | `TEST_GITHUB_USERNAME` | Username for test cases | `octocat` | | `TEST_GITHUB_REPO` | Repository name for test cases | `Hello-World` | ### Jest Configuration The Jest configuration in `jest.config.js` includes: - **TypeScript support**: Using `ts-jest` preset - **Test timeout**: 30 seconds (configurable via environment) - **Test pattern**: `**/*.test.ts` files in `tests/` directory - **Setup file**: Runs `tests/setup.ts` before all tests - **Coverage**: Configured to cover `services/` and `core/` directories ### TypeScript Configuration The TypeScript configuration in `tsconfig.json` includes: - **Target**: ES2020 for modern Node.js - **Strict mode**: Enabled for type safety - **Module system**: CommonJS for Node.js compatibility - **Source maps**: Enabled for debugging ## API Rate Limits GitHub API has rate limits that you should be aware of: - **Unauthenticated requests**: 60 requests per hour - **Authenticated requests**: 5,000 requests per hour - **Search API**: 30 requests per minute If you hit rate limits: 1. Tests will log the rate limit error 2. Wait for the rate limit to reset (time shown in error) 3. Use authenticated requests (set `GITHUB_TOKEN`) for higher limits 4. Run specific test suites instead of all tests Check rate limit status: ```bash # Using curl with your token curl -H "Authorization: Bearer YOUR_TOKEN" https://api.github.com/rate_limit ``` ## Troubleshooting ### Tests Fail with "GITHUB_TOKEN not configured" Some tests require authentication. Make sure you: 1. Created a `.env` file (copy from `.env.example`) 2. Added your GitHub personal access token 3. Token has appropriate scopes for the endpoints you're testing ### Tests Timeout If tests timeout frequently: 1. Check your internet connection 2. Increase timeout in `.env`: `TEST_TIMEOUT=60000` 3. Check GitHub API status: https://www.githubstatus.com/ ### Rate Limit Errors If you see rate limit errors: 1. Make sure `GITHUB_TOKEN` is set (authenticated requests have higher limits) 2. Wait for rate limit to reset (time shown in error message) 3. Run fewer tests at once: `npm test -- repos.test` ### TypeScript Errors If you see TypeScript compilation errors: 1. Check that dependencies are installed: `npm install` 2. Verify TypeScript version: `npm list typescript` 3. Run type check: `npm run type-check` ## Generated Code The API client code in `core/`, `models/`, and `services/` directories is generated from GitHub's OpenAPI specification using `openapi-typescript-codegen`. **Do not manually edit these files** - they will be overwritten if the code is regenerated. If you need to modify the client behavior: 1. Edit the generator configuration 2. Regenerate the code 3. Copy to this test project ## Contributing When adding new tests: 1. Follow the existing test structure 2. Add descriptive test names 3. Include console logging for debugging 4. Test both success and error cases 5. Handle rate limits properly 6. Update this README if adding new test suites ## Resources - [GitHub REST API Documentation](https://docs.github.com/en/rest) - [Jest Documentation](https://jestjs.io/docs/getting-started) - [TypeScript Documentation](https://www.typescriptlang.org/docs/) - [GitHub API Rate Limiting](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting) ## License MIT --- ## File: demo/minimal/README.md # Corsair Minimal Demo A minimal demo showing how easy it is to work with Corsair - great types, simple APIs, and automatic webhook routing. The sandbox editor on corsair.dev points to this project. ## Files - **`corsair.ts`** - Setup file showing how to initialize Corsair with multiple integrations - **`webhooks.ts`** - Express server with a single webhook endpoint that handles all integrations - **`example.ts`** - Example showing API usage with full TypeScript support ## Key Features Demonstrated ✅ **Easy Setup** - Configure once, use everywhere ✅ **Great Types** - Full TypeScript autocomplete for all APIs ✅ **Simple Webhooks** - One endpoint handles all integrations automatically ✅ **Multi-Tenancy** - Built-in tenant isolation ✅ **Automatic Sync** - API responses automatically stored in database If you want to clone a demo and use it with your database, clone `demo/testing`! --- ## File: docs/adapters/client.mdx --- title: Vanilla Client --- `createCorsairClient({ baseURL })` returns a typed client that mirrors every route on the [handler](/management/handler). Use it from a Node script, a CLI, a worker, or a non-React frontend. ```ts client.ts import { createCorsairClient } from "corsair"; const client = createCorsairClient({ baseURL: "/api/corsair" }); ``` For React, prefer [`createCorsairReactClient`](/adapters/react) — it wraps this one and adds hooks. ## Reading ```ts reads.ts await client.tenants.list(); // GET /tenants await client.tenants.get("acme"); // GET /tenants/acme await client.plugins.list(); // GET /plugins await client.plugins.get("github"); // GET /plugins/github await client.connectionStatus.get({ // GET /connection-status tenantId: "acme", }); await client.permissions.get({ id: "perm_123" }); // GET /permissions/perm_123 await client.permissions.get({ token: "tok_abc" }); // POST /permissions/lookup-by-token await client.ok(); // GET /ok ``` Every method is typed against the route's response. Hovering `client.tenants.list()` in your editor shows `Promise`, and so on. `connectionStatus.get` returns a `Record` keyed by plugin id: ```ts const status = await client.connectionStatus.get({ tenantId: "acme" }); // { github: 'connected', slack: 'not_connected', notion: 'missing_credentials' } ``` ## Writing ```ts writes.ts await client.tenants.create({ id: "acme" }); // POST /tenants ``` Connect/OAuth methods are documented on the [Connect page](/management/connect). ## Options ```ts createCorsairClient({ baseURL: "/api/corsair", // required fetch: customFetch, // optional — override globalThis.fetch }); ``` `baseURL` is the origin + base path the handler is mounted at, e.g. `https://app.example.com/api/corsair`. A trailing slash is tolerated. `fetch` is optional — see [Custom fetch](#custom-fetch) below. Need auth headers, custom retry, or interceptors? Pass a wrapped `fetch`: ```ts const client = createCorsairClient({ baseURL: "/api/corsair", fetch: (input, init) => globalThis.fetch(input, { ...init, headers: { ...init?.headers, Authorization: `Bearer ${token()}` }, }), }); ``` ## Error handling Failed requests throw `CorsairClientError`: ```ts import { CorsairClientError } from "corsair"; try { await client.tenants.get("does-not-exist"); } catch (err) { if (err instanceof CorsairClientError) { err.status; // number — HTTP status err.code; // string — e.g. "not_found" err.message; // string — human message from the server err.extra; // Record — any additional fields the server returned } } ``` Network failures (DNS, abort, no response) throw a plain `Error` — `CorsairClientError` is only used when the server responded with a non-2xx body. ## Custom fetch When you call `createCorsairClient`, the client picks its `fetch` function once — either the one you pass in, or `globalThis.fetch` at that moment. Every later call (`client.tenants.list()`, etc.) uses that same function; it does not re-read `globalThis.fetch` on each request. In a normal browser or Node 18+ app, this makes no practical difference. `fetch` is already available when you create the client, and it stays the same. It only matters in two cases: - **Tests** — your test runner or jsdom may install or replace `fetch` after your client module is imported. Pass an explicit `fetch` so the client uses the right one. - **Custom behavior** — auth headers, retries, or routing requests to an in-process handler instead of over HTTP. ```ts // Test: wire the client directly to the handler, no TCP socket const handler = managementHandler(corsair); const client = createCorsairClient({ baseURL: "http://test.local/api/corsair", fetch: (input, init) => handler(new Request(String(input), init)), }); ``` If you create the client at module scope and later replace `globalThis.fetch`, the client will not pick up the change. Create the client after your environment is ready, or pass `fetch` explicitly. --- ## File: docs/adapters/handlers.mdx --- title: Frameworks sidebarTitle: Frameworks --- import CreateCorsairHub from '/snippets/create-corsair-hub.mdx'; import MountHandlerTabs from '/snippets/mount-handler-tabs.mdx'; Corsair mounts **one** route. It serves Hub delivery — OAuth callbacks, connect pages, self-registration — and the [management API](/management/handler) in the same place. Every adapter wraps a single primitive, `managementHandler(corsair)`, which returns a `(request: Request) => Promise`. 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](/hub/delivery-urls). Stays grey? The route isn't reachable at `/api/corsair`. Check the mount path and — on Express and Hono — that `basePath` and the wildcard (`/api/corsair/*`) match the path you mounted. ## What's next 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`](/adapters/client). One factory call per app — use the returned hooks anywhere in your component tree. ```tsx corsair-client.ts "use client"; import { createCorsairReactClient } from "corsair/client/react"; export const { useTenants, useTenant, useCreateTenant, usePlugins, usePlugin, useConnectionStatus, usePermission, useCreateConnectLink, useOAuthCallback, client, // escape hatch — the underlying vanilla client } = createCorsairReactClient({ baseURL: "/api/corsair" }); ``` React 18+ is a peer dependency. If you aren't on React, use the [vanilla client](/adapters/client). ## Read hooks Read hooks follow the same shape: ```tsx tenants-list.tsx const { data, loading, error, refetch } = useTenants(); ``` | Field | Type | Notes | | ---------- | ----------------------------- | --------------------------------------- | | `data` | the typed response, or `null` | populated on success | | `loading` | `boolean` | `true` while a request is in flight | | `error` | `Error \| null` | typed error if the call failed | | `refetch` | `() => Promise` | 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 ; 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 (
{ e.preventDefault(); const id = new FormData(e.currentTarget).get("id") as string; await mutate({ id }); }}> Create {error && {error.message}} {data && Created {data.id}}
); } ``` Available mutation hooks: `useCreateTenant`, `useCreateConnectLink`, `useOAuthCallback`. ## Connection status `useConnectionStatus` is the hook your dashboard probably opens with. The response is a `Record` 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]) => (
  • {plugin}: {status === "connected" ? "✓" : "Connect →"}
  • ))} ); } ``` For wiring the actual connect-and-authorize flow, see the [Connect page](/management/connect). ## Escape hatch If a hook doesn't fit (e.g. you need imperative access inside an event handler), reach for `client`: ```tsx escape.tsx const handleClick = async () => { const tenant = await client.tenants.create({ id: "acme" }); console.log(tenant); }; ``` It is exactly the [vanilla client](/adapters/client), sharing the same `baseURL`. ## What this is not These hooks are intentionally minimal: no cache, no deduplication, no request reuse. If you want React Query, SWR, or RTK semantics, build them on top of `client` — the hooks here exist to give you typed loading/error/data state without forcing a data-layer choice.