Skip to main content

connector-generation

Guide for creating new external service connectors in DEVS. Use this when asked to add a new OAuth app connector, API connector, or integration with external services like Slack, Trello, Linear, Asana, Dropbox, GitHub, etc.

معلومات المصدر

المستودع
codename-co/devs
آخر نشاط في المصدر
١٦ يناير ٢٠٢٦ في ٢٣:٢٢
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٥٣
التفرعات
٢

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

مستكشف الملفات
4 ملفات

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
connector-generation
description
Guide for creating new external service connectors in DEVS. Use this when asked to add a new OAuth app connector, API connector, or integration with external services like Slack, Trello, Linear, Asana, Dropbox, GitHub, etc.
# Connector Generation for DEVS When creating new connectors for external services in DEVS, follow this comprehensive guide covering OAuth flow, provider implementation, registration, bridge proxy setup, and UI integration. ## Overview DEVS connectors enable synchronizing content from external services into the Knowledge Base. The architecture consists of: 1. **Provider Implementation** - OAuth + API integration in `src/features/connectors/providers/apps/` 2. **Provider Registry** - Lazy loading registration in `provider-registry.ts` 3. **OAuth Gateway** - OAuth config for popup-based auth flow 4. **Bridge Server** - Server-side proxy for OAuth secrets and CORS (in `utils/devs-bridge/`) 5. **UI Configuration** - Icons and display metadata ## Step 1: Define Provider Type Add the new provider to the type union in `src/features/connectors/types.ts`: ```typescript // In types.ts export type AppConnectorProvider = | 'google-drive' | 'gmail' | 'google-calendar' | 'google-meet' | 'google-tasks' | 'notion' | 'dropbox' | 'github' | 'qonto' | 'slack' // <-- Add new provider ``` Also add the config entry in `APP_CONNECTOR_CONFIGS`: ```typescript // In types.ts - APP_CONNECTOR_CONFIGS slack: { id: 'slack', category: 'app', name: 'Slack', icon: 'slack', color: '#4A154B', capabilities: ['read', 'search'], supportedTypes: ['message', 'channel', 'file'], maxFileSize: 10 * 1024 * 1024, rateLimit: { requests: 50, windowSeconds: 60 }, }, ``` ## Step 2: Create Provider Implementation Create a new file `src/features/connectors/providers/apps/{provider}.ts`: ```typescript /** * {Provider} Connector Provider * * Implements OAuth 2.0 authentication and API integration for {Provider}. * Supports listing, reading, searching content, and delta sync. */ import { BRIDGE_URL } from '@/config/bridge' import { BaseAppConnectorProvider } from '../../connector-provider' import type { Connector, ConnectorProviderConfig, OAuthResult, TokenRefreshResult, AccountInfo, ListOptions, ListResult, ContentResult, SearchResult, ChangesResult, ConnectorItem, } from '../../types' // ============================================================================= // Constants // ============================================================================= /** API base URL - use gateway proxy to avoid CORS and keep secrets safe */ const API_BASE = `${BRIDGE_URL}/api/{provider}` /** OAuth endpoints */ const AUTH_URL = 'https://{provider}.com/oauth/authorize' const TOKEN_URL = 'https://{provider}.com/oauth/token' const REVOKE_URL = 'https://{provider}.com/oauth/revoke' const USERINFO_URL = 'https://{provider}.com/api/users/me' // ============================================================================= // Types // ============================================================================= // Define types for API responses (raw JSON from the provider's API) interface RawItem { id: string name: string // ... provider-specific fields } interface ListResponse { items: RawItem[] next_cursor?: string } interface TokenResponse { access_token: string refresh_token?: string expires_in?: number scope: string token_type: string } interface UserInfoResponse { id: string email?: string name?: string avatar?: string } // ============================================================================= // Provider Implementation // ============================================================================= export class {Provider}Provider extends BaseAppConnectorProvider { readonly id = '{provider-id}' as const readonly config: ConnectorProviderConfig = { id: '{provider-id}', category: 'app', name: '{Provider Name}', icon: '{provider-icon}', color: '#HEXCOLOR', capabilities: ['read', 'search'], supportedTypes: ['*'], maxFileSize: 10 * 1024 * 1024, rateLimit: { requests: 100, windowSeconds: 60 }, } /** Get the OAuth client ID from environment */ private get clientId(): string { return import.meta.env.VITE_{PROVIDER}_CLIENT_ID || '' } /** Get the OAuth redirect URI */ private get redirectUri(): string { return `${window.location.origin}/oauth/callback` } // =========================================================================== // OAuth Methods // =========================================================================== /** * Generate the OAuth authorization URL. * * @param state - State parameter for CSRF protection * @param codeChallenge - PKCE code challenge (S256) * @returns The full authorization URL */ getAuthUrl(state: string, codeChallenge: string): string { const params = new URLSearchParams({ client_id: this.clientId, redirect_uri: this.redirectUri, response_type: 'code', scope: 'read:user read:content', // Provider-specific scopes state, // Include PKCE if provider supports it code_challenge: codeChallenge, code_challenge_method: 'S256', }) return `${AUTH_URL}?${params.toString()}` } /** * Exchange an authorization code for access and refresh tokens. * * NOTE: Token exchange should go through BRIDGE_URL to inject client_secret */ async exchangeCode(code: string, codeVerifier: string): Promise<OAuthResult> { const response = await fetch(`${BRIDGE_URL}/api/{provider}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ code, code_verifier: codeVerifier, grant_type: 'authorization_code', redirect_uri: this.redirectUri, // client_id and client_secret injected by bridge server }), }) if (!response.ok) { const errorText = await response.text() throw new Error(`Token exchange failed: ${response.status} ${errorText}`) } const data: TokenResponse = await response.json() return { accessToken: data.access_token, refreshToken: data.refresh_token, expiresIn: data.expires_in, scope: data.scope, tokenType: data.token_type, } } /** * Refresh an expired access token using the refresh token. */ async refreshToken(connector: Connector): Promise<TokenRefreshResult> { const refreshToken = await this.getDecryptedRefreshToken(connector) if (!refreshToken) { throw new Error('No refresh token available') } const response = await fetch(`${BRIDGE_URL}/api/{provider}/oauth/token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ refresh_token: refreshToken, grant_type: 'refresh_token', // client_id and client_secret injected by bridge server }), }) if (!response.ok) { const errorText = await response.text() throw new Error(`Token refresh failed: ${response.status} ${errorText}`) } const data: TokenResponse = await response.json() return { accessToken: data.access_token, expiresIn: data.expires_in, } } /** * Validate that a token is still valid. */ async validateToken(token: string): Promise<boolean> { try { const response = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${token}` }, }) return response.ok } catch { return false } } /** * Revoke all access for a connector. */ async revokeAccess(connector: Connector): Promise<void> { const token = await this.getDecryptedToken(connector) const response = await fetch(REVOKE_URL, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Authorization: `Bearer ${token}`, }, }) if (!response.ok) { const errorText = await response.text() throw new Error(`Token revocation failed: ${response.status} ${errorText}`) } } /** * Get account information for the authenticated user. */ async getAccountInfo(token: string): Promise<AccountInfo> { const response = await fetch(USERINFO_URL, { headers: { Authorization: `Bearer ${token}` }, }) if (!response.ok) { const errorText = await response.text() throw new Error(`Failed to get account info: ${response.status} ${errorText}`) } const data: UserInfoResponse = await response.json() return { id: data.id, email: data.email, name: data.name, picture: data.avatar, } }
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub