comprehensive documentation review + test fixes
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m15s
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m15s
This commit is contained in:
396
docs/subagents/INTEGRATIONS-GUIDE.md
Normal file
396
docs/subagents/INTEGRATIONS-GUIDE.md
Normal file
@@ -0,0 +1,396 @@
|
||||
# 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<StoreLocation[]> {
|
||||
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<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: { maxRetries: number; baseDelay: number },
|
||||
): Promise<T> {
|
||||
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<void> {
|
||||
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
|
||||
Reference in New Issue
Block a user