Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions .github/workflows/canary-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
name: Canary Release

on:
push:
branches:
- dev
paths-ignore:
- "**.md"
- "docs/**"
- "examples/**"
- ".github/**"
- "!.github/workflows/canary-release.yml"

permissions:
contents: write # Required to create releases and tags

jobs:
generate-version:
runs-on: ubuntu-latest
outputs:
canary-version: ${{ steps.version.outputs.canary-version }}
base-version: ${{ steps.version.outputs.base-version }}
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Get base version
id: base
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

- name: Query npm for existing canary versions
id: npm-versions
run: |
BASE_VERSION="${{ steps.base.outputs.version }}"
echo "Base version: $BASE_VERSION"

# Query npm for all versions and find max canary increment
MAX_INCREMENT=$(node -e "
const { execSync } = require('child_process');
const baseVersion = process.argv[1];
let versions = [];
try {
const output = execSync('npm view integrate-sdk versions --json', { encoding: 'utf-8' });
versions = JSON.parse(output);
} catch (e) {
versions = [];
}
const pattern = new RegExp('^' + baseVersion.replace(/\./g, '\\.') + '-dev\\.(\\d+)$');
const matches = versions
.filter(v => pattern.test(v))
.map(v => {
const match = v.match(pattern);
return match ? parseInt(match[1], 10) : 0;
});
const maxIncrement = matches.length > 0 ? Math.max(...matches) : -1;
console.log(maxIncrement);
" "$BASE_VERSION")

echo "max-increment=$MAX_INCREMENT" >> $GITHUB_OUTPUT

- name: Generate canary version
id: version
run: |
BASE_VERSION="${{ steps.base.outputs.version }}"
MAX_INCREMENT="${{ steps.npm-versions.outputs.max-increment }}"

# Increment the number
NEXT_INCREMENT=$((MAX_INCREMENT + 1))
CANARY_VERSION="${BASE_VERSION}-dev.${NEXT_INCREMENT}"

echo "base-version=$BASE_VERSION" >> $GITHUB_OUTPUT
echo "canary-version=$CANARY_VERSION" >> $GITHUB_OUTPUT
echo "Generated canary version: $CANARY_VERSION"

publish:
needs: generate-version
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Run tests
run: bun test

- name: Type check
run: bun run type-check

- name: Build
run: bun run build

- name: Update package.json version
run: |
CANARY_VERSION="${{ needs.generate-version.outputs.canary-version }}"
# Use node to update package.json version
node -e "
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
pkg.version = '$CANARY_VERSION';
fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
echo "Updated package.json version to $CANARY_VERSION"

- name: Setup Node.js (for npm publish)
uses: actions/setup-node@v4
with:
node-version: "20"
registry-url: "https://registry.npmjs.org"

- name: Publish to npm
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create Git Tag
run: |
CANARY_VERSION="${{ needs.generate-version.outputs.canary-version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a v$CANARY_VERSION -m "Canary release v$CANARY_VERSION"
git push origin v$CANARY_VERSION

- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
tag_name: v${{ needs.generate-version.outputs.canary-version }}
name: Canary Release v${{ needs.generate-version.outputs.canary-version }}
body: |
## Canary Release v${{ needs.generate-version.outputs.canary-version }}

This is a canary release from the `dev` branch.

### Installation
```bash
bun add integrate-sdk@${{ needs.generate-version.outputs.canary-version }}
```
draft: false
prerelease: true

- name: Check for Discord webhook
id: check-discord
run: |
if [ -n "${{ secrets.DISCORD_WEBHOOK_URL }}" ]; then
echo "has_webhook=true" >> $GITHUB_OUTPUT
else
echo "has_webhook=false" >> $GITHUB_OUTPUT
fi

- name: Github Releases To Discord
if: steps.check-discord.outputs.has_webhook == 'true'
uses: SethCohen/github-releases-to-discord@v1.13.1
with:
webhook_url: ${{ secrets.DISCORD_WEBHOOK_URL }}
color: "1316451"
content: "Canary release published:"
footer_title: "integrate.dev"
footer_timestamp: true

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "integrate-sdk",
"version": "0.8.27",
"version": "0.8.28",
"description": "Type-safe 3rd party integration SDK for the Integrate MCP server",
"type": "module",
"main": "./dist/index.js",
Expand Down
37 changes: 24 additions & 13 deletions src/adapters/base-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,41 +60,49 @@ export interface OAuthHandlerConfig {
* Called automatically after successful OAuth callback
*
* @param provider - Provider name (e.g., 'github')
* @param tokenData - OAuth tokens (accessToken, refreshToken, etc.)
* @param tokenData - OAuth tokens (accessToken, refreshToken, etc.), or null to delete
* @param email - Optional email to store specific account token
* @param context - User context (userId, organizationId, etc.)
*
* @example
* ```typescript
* setProviderToken: async (provider, tokens, context) => {
* setProviderToken: async (provider, tokens, email, context) => {
* await db.tokens.upsert({
* where: { provider_userId: { provider, userId: context.userId } },
* create: { provider, userId: context.userId, ...tokens },
* where: { provider_email_userId: { provider, email, userId: context.userId } },
* create: { provider, email, userId: context.userId, ...tokens },
* update: tokens,
* });
* }
* ```
*/
setProviderToken?: (provider: string, tokenData: ProviderTokenData, context?: MCPContext) => Promise<void> | void;
setProviderToken?: (provider: string, tokenData: ProviderTokenData | null, email?: string, context?: MCPContext) => Promise<void> | void;
/**
* Optional callback to delete provider tokens from database
* Called automatically when disconnecting providers
* Called automatically when disconnecting providers or accounts
*
* @param provider - Provider name (e.g., 'github')
* @param email - Optional email to delete specific account token. If not provided, deletes all tokens for the provider
* @param context - User context (userId, organizationId, etc.)
*
* @example
* ```typescript
* removeProviderToken: async (provider, context) => {
* removeProviderToken: async (provider, email, context) => {
* const userId = context?.userId;
* if (!userId) return;
*
* await db.tokens.delete({
* where: { provider_userId: { provider, userId } }
* });
* if (email) {
* await db.tokens.deleteMany({
* where: { provider, email, userId }
* });
* } else {
* await db.tokens.deleteMany({
* where: { provider, userId }
* });
* }
* }
* ```
*/
removeProviderToken?: (provider: string, context?: MCPContext) => Promise<void> | void;
removeProviderToken?: (provider: string, email?: string, context?: MCPContext) => Promise<void> | void;
}

/**
Expand Down Expand Up @@ -476,7 +484,9 @@ export class OAuthHandler {
scopes: result.scopes, // Include scopes in token data
};

await this.config.setProviderToken(callbackRequest.provider, tokenData, context);
// Email is not available at server-side callback time (fetched client-side)
// Pass undefined for email - customer's callback can fetch it if needed
await this.config.setProviderToken(callbackRequest.provider, tokenData, undefined, context);
} catch (error) {
// Token storage failed - log but don't fail the OAuth flow
}
Expand Down Expand Up @@ -563,9 +573,10 @@ export class OAuthHandler {
}

// Call removeProviderToken callback with context
// Note: Email is not available in disconnect requests - pass undefined to delete all tokens for provider
if (context) {
try {
await this.config.removeProviderToken(request.provider, context);
await this.config.removeProviderToken(request.provider, undefined, context);
} catch (error) {
// Log error but don't fail the request - MCP server revocation will still happen
console.error(`Failed to delete token for ${request.provider} from database via removeProviderToken:`, error);
Expand Down
Loading