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.

Informations de source

Dépôt
codename-co/devs
Dernière activité de la source
16 janvier 2026 à 23:22
Langue détectée de SKILL.md
anglais
Étoiles
53
Forks
2

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
4 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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, } }
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub