# Integrations Subagent Guide The **integrations-specialist** subagent handles third-party services, webhooks, and external API integrations in the Flyer Crawler project. ## Quick Reference | Aspect | Details | | --------------- | --------------------------------------------------------------------------- | | **Primary Use** | External APIs, webhooks, OAuth, third-party services | | **Key Files** | `src/services/external/`, `src/routes/webhooks.routes.ts` | | **Key ADRs** | ADR-041 (AI Integration), ADR-016 (API Security), ADR-048 (Auth) | | **MCP Tools** | `mcp__gitea-projectium__*`, `mcp__bugsink__*` | | **Security** | API key storage, webhook signatures, OAuth state param | | **Delegate To** | `coder` (implementation), `security-engineer` (review), `ai-usage` (Gemini) | ## When to Use Use the **integrations-specialist** subagent when you need to: - Integrate with external APIs (OAuth, REST, GraphQL) - Implement webhook handlers - Configure third-party services - Debug external service connectivity - Handle API authentication flows - Manage external service rate limits ## What integrations-specialist Knows The integrations-specialist subagent understands: - OAuth 2.0 flows (authorization code, client credentials) - REST API integration patterns - Webhook security (signature verification) - External service error handling - Rate limiting and retry strategies - API key management ## Current Integrations | Service | Purpose | Integration Type | Key Files | | ------------- | ---------------------- | ---------------- | ---------------------------------- | | Google Gemini | AI flyer extraction | REST API | `src/services/aiService.server.ts` | | Bugsink | Error tracking | REST API | MCP: `mcp__bugsink__*` | | Gitea | Repository and CI/CD | REST API | MCP: `mcp__gitea-projectium__*` | | Redis | Caching and job queues | Native client | `src/services/redis.server.ts` | | PostgreSQL | Primary database | Native client | `src/services/db/pool.db.ts` | ## Example Requests ### Adding External API Integration ``` "Use integrations-specialist to integrate with the Store API to automatically fetch store location data. Include proper error handling, rate limiting, and caching." ``` ### OAuth Implementation ``` "Use integrations-specialist to implement Google OAuth for user authentication. Include token refresh handling and session management." ``` ### Webhook Handler ``` "Use integrations-specialist to create a webhook handler for receiving store inventory updates. Include signature verification and idempotency handling." ``` ### Debugging External Service Issues ``` "Use integrations-specialist to debug why the Gemini API calls are intermittently failing with timeout errors. Check connection pooling, retry logic, and error handling." ``` ## Integration Patterns ### REST API Client Pattern ```typescript // src/services/external/storeApi.server.ts import { env } from '@/config/env'; import { log } from '@/services/logger.server'; interface StoreApiConfig { baseUrl: string; apiKey: string; timeout: number; } class StoreApiClient { private config: StoreApiConfig; constructor(config: StoreApiConfig) { this.config = config; } async getStoreLocations(storeId: string): Promise { const url = `${this.config.baseUrl}/stores/${storeId}/locations`; try { const response = await fetch(url, { headers: { Authorization: `Bearer ${this.config.apiKey}`, 'Content-Type': 'application/json', }, signal: AbortSignal.timeout(this.config.timeout), }); if (!response.ok) { throw new ExternalApiError(`Store API error: ${response.status}`, response.status); } return response.json(); } catch (error) { log.error({ error, storeId }, 'Failed to fetch store locations'); throw error; } } } export const storeApiClient = new StoreApiClient({ baseUrl: env.STORE_API_BASE_URL, apiKey: env.STORE_API_KEY, timeout: 10000, }); ``` ### Webhook Handler Pattern ```typescript // src/routes/webhooks.routes.ts import { Router } from 'express'; import crypto from 'crypto'; import { env } from '@/config/env'; const router = Router(); function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean { const expected = crypto.createHmac('sha256', secret).update(payload).digest('hex'); return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(`sha256=${expected}`)); } router.post('/store-updates', async (req, res, next) => { try { const signature = req.headers['x-webhook-signature'] as string; const payload = JSON.stringify(req.body); if (!verifyWebhookSignature(payload, signature, env.WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process webhook with idempotency check const eventId = req.headers['x-event-id'] as string; const alreadyProcessed = await checkIdempotencyKey(eventId); if (alreadyProcessed) { return res.status(200).json({ status: 'already_processed' }); } await processStoreUpdate(req.body); await markEventProcessed(eventId); res.status(200).json({ status: 'processed' }); } catch (error) { next(error); } }); ``` ### OAuth Flow Pattern ```typescript // src/services/oauth/googleOAuth.server.ts import { OAuth2Client } from 'google-auth-library'; import { env } from '@/config/env'; const oauth2Client = new OAuth2Client( env.GOOGLE_CLIENT_ID, env.GOOGLE_CLIENT_SECRET, env.GOOGLE_REDIRECT_URI, ); export function getAuthorizationUrl(): string { return oauth2Client.generateAuthUrl({ access_type: 'offline', scope: ['email', 'profile'], prompt: 'consent', }); } export async function exchangeCodeForTokens(code: string) { const { tokens } = await oauth2Client.getToken(code); return tokens; } export async function refreshAccessToken(refreshToken: string) { oauth2Client.setCredentials({ refresh_token: refreshToken }); const { credentials } = await oauth2Client.refreshAccessToken(); return credentials; } ``` ## Error Handling for External Services ### Custom Error Classes ```typescript // src/services/external/errors.ts export class ExternalApiError extends Error { constructor( message: string, public statusCode: number, public retryable: boolean = false, ) { super(message); this.name = 'ExternalApiError'; } } export class RateLimitError extends ExternalApiError { constructor( message: string, public retryAfter: number, ) { super(message, 429, true); this.name = 'RateLimitError'; } } ``` ### Retry with Exponential Backoff ```typescript async function fetchWithRetry( fn: () => Promise, options: { maxRetries: number; baseDelay: number }, ): Promise { let lastError: Error; for (let attempt = 0; attempt <= options.maxRetries; attempt++) { try { return await fn(); } catch (error) { lastError = error as Error; if (error instanceof ExternalApiError && !error.retryable) { throw error; } if (attempt < options.maxRetries) { const delay = options.baseDelay * Math.pow(2, attempt); await new Promise((resolve) => setTimeout(resolve, delay)); } } } throw lastError!; } ``` ## Rate Limiting Strategies ### Token Bucket Pattern ```typescript class RateLimiter { private tokens: number; private lastRefill: number; private readonly maxTokens: number; private readonly refillRate: number; // tokens per second constructor(maxTokens: number, refillRate: number) { this.maxTokens = maxTokens; this.tokens = maxTokens; this.refillRate = refillRate; this.lastRefill = Date.now(); } async acquire(): Promise { this.refill(); if (this.tokens < 1) { const waitTime = ((1 - this.tokens) / this.refillRate) * 1000; await new Promise((resolve) => setTimeout(resolve, waitTime)); this.refill(); } this.tokens -= 1; } private refill(): void { const now = Date.now(); const elapsed = (now - this.lastRefill) / 1000; this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate); this.lastRefill = now; } } ``` ## Testing Integrations ### Mocking External Services ```typescript // src/tests/mocks/storeApi.mock.ts import { vi } from 'vitest'; export const mockStoreApiClient = { getStoreLocations: vi.fn(), }; vi.mock('@/services/external/storeApi.server', () => ({ storeApiClient: mockStoreApiClient, })); ``` ### Integration Test with Real Service ```typescript // src/tests/integration/storeApi.integration.test.ts describe('Store API Integration', () => { it.skipIf(!env.STORE_API_KEY)('fetches real store locations', async () => { const locations = await storeApiClient.getStoreLocations('test-store'); expect(locations).toBeInstanceOf(Array); }); }); ``` ## MCP Tools for Integrations ### Gitea Integration ``` // List repositories mcp__gitea-projectium__list_my_repos() // Create issue mcp__gitea-projectium__create_issue({ owner: "projectium", repo: "flyer-crawler", title: "Issue title", body: "Issue description" }) ``` ### Bugsink Integration ``` // List projects mcp__bugsink__list_projects() // Get issue details mcp__bugsink__get_issue({ issue_id: "..." }) // Get stacktrace mcp__bugsink__get_stacktrace({ event_id: "..." }) ``` ## Security Considerations ### API Key Storage - Never commit API keys to version control - Use environment variables via `src/config/env.ts` - Rotate keys periodically - Use separate keys for dev/test/prod ### Webhook Security - Always verify webhook signatures - Use HTTPS for webhook endpoints - Implement idempotency - Log webhook events for audit ### OAuth Security - Use state parameter to prevent CSRF - Store tokens securely (encrypted at rest) - Implement token refresh before expiration - Validate token scopes ## Related Documentation - [OVERVIEW.md](./OVERVIEW.md) - Subagent system overview - [SECURITY-DEBUG-GUIDE.md](./SECURITY-DEBUG-GUIDE.md) - Security patterns - [AI-USAGE-GUIDE.md](./AI-USAGE-GUIDE.md) - Gemini API integration - [../adr/0041-ai-gemini-integration-architecture.md](../adr/0041-ai-gemini-integration-architecture.md) - AI integration ADR - [../adr/0016-api-security-hardening.md](../adr/0016-api-security-hardening.md) - API security - [../adr/0048-authentication-strategy.md](../adr/0048-authentication-strategy.md) - Authentication