External Services Integration
This guide covers how to integrate your Qirvo plugins with external APIs and third-party services securely and efficiently. Learn about authentication, rate limiting, error handling, and best practices.
Table of Contents
- Service Integration Overview
- Authentication Methods
- Popular Service Integrations
- Rate Limiting & Quotas
- Error Handling
- Security Best Practices
Service Integration Overview
Configuration Schema for External Services
Define external service configurations in your manifest:
{
"manifest_version": 1,
"name": "Multi-Service Plugin",
"permissions": ["network-access"],
"configSchema": {
"type": "object",
"properties": {
"services": {
"type": "object",
"title": "External Services",
"properties": {
"weather": {
"type": "object",
"title": "Weather Service",
"properties": {
"provider": {
"type": "string",
"enum": ["openweathermap", "weatherapi", "accuweather"],
"title": "Weather Provider"
},
"apiKey": {
"type": "string",
"title": "API Key",
"format": "password"
},
"units": {
"type": "string",
"enum": ["metric", "imperial", "kelvin"],
"default": "metric"
}
},
"required": ["provider", "apiKey"]
}
}
}
}
}
}
Service Manager Implementation
import { BasePlugin, PluginRuntimeContext } from '@qirvo/plugin-sdk';
interface ServiceConfig {
provider: string;
apiKey?: string;
credentials?: any;
baseUrl?: string;
timeout?: number;
retries?: number;
}
export default class ExternalServicePlugin extends BasePlugin {
private serviceManager: ServiceManager;
async onEnable(context: PluginRuntimeContext): Promise<void> {
this.serviceManager = new ServiceManager(context, this);
// Initialize configured services
const services = context.config.services || {};
for (const [serviceName, config] of Object.entries(services)) {
await this.serviceManager.registerService(serviceName, config as ServiceConfig);
}
}
async onConfigChange(context: PluginRuntimeContext): Promise<void> {
// Reinitialize services when configuration changes
await this.serviceManager.updateServices(context.config.services || {});
}
}
class ServiceManager {
private services = new Map<string, ExternalService>();
constructor(
private context: PluginRuntimeContext,
private plugin: BasePlugin
) {}
async registerService(name: string, config: ServiceConfig): Promise<void> {
const service = this.createService(name, config);
this.services.set(name, service);
// Test service connection
try {
await service.healthCheck();
this.plugin.log('info', `Service ${name} registered successfully`);
} catch (error) {
this.plugin.log('error', `Failed to register service ${name}:`, error);
}
}
async getService(name: string): Promise<ExternalService> {
const service = this.services.get(name);
if (!service) {
throw new Error(`Service ${name} not registered`);
}
return service;
}
private createService(name: string, config: ServiceConfig): ExternalService {
switch (name) {
case 'weather':
return new WeatherService(config, this.context, this.plugin);
case 'github':
return new GitHubService(config, this.context, this.plugin);
default:
return new GenericService(name, config, this.context, this.plugin);
}
}
}
Authentication Methods
API Key Authentication
class ApiKeyService extends ExternalService {
protected async makeRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ServiceResponse<T>> {
const url = `${this.config.baseUrl}${endpoint}`;
const headers = {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': `Qirvo-Plugin/${this.plugin.context.plugin.version}`,
...options.headers
};
const response = await this.context.api.http.request(url, {
...options,
headers
});
return this.processResponse<T>(response);
}
async healthCheck(): Promise<boolean> {
try {
await this.makeRequest('/health');
return true;
} catch (error) {
this.plugin.log('error', 'Health check failed:', error);
return false;
}
}
}
OAuth 2.0 Authentication
class OAuthService extends ExternalService {
private accessToken?: string;
private refreshToken?: string;
private tokenExpiry?: Date;
async authenticate(): Promise<void> {
if (this.isTokenValid()) {
return;
}
if (this.refreshToken) {
await this.refreshAccessToken();
} else {
await this.performOAuthFlow();
}
}
private async refreshAccessToken(): Promise<void> {
try {
const response = await this.context.api.http.post('/oauth/token', {
grant_type: 'refresh_token',
refresh_token: this.refreshToken,
client_id: this.config.credentials.clientId,
client_secret: this.config.credentials.clientSecret
});
const tokenData = await response.json();
this.accessToken = tokenData.access_token;
this.refreshToken = tokenData.refresh_token || this.refreshToken;
this.tokenExpiry = new Date(Date.now() + tokenData.expires_in * 1000);
// Store tokens securely
await this.storeTokens();
this.plugin.log('info', 'Access token refreshed successfully');
} catch (error) {
this.plugin.log('error', 'Token refresh failed:', error);
throw new Error('Authentication failed');
}
}
private isTokenValid(): boolean {
return this.accessToken &&
this.tokenExpiry &&
this.tokenExpiry > new Date();
}
protected async makeRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ServiceResponse<T>> {
await this.authenticate();
const headers = {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
...options.headers
};
const response = await this.context.api.http.request(
`${this.config.baseUrl}${endpoint}`,
{ ...options, headers }
);
return this.processResponse<T>(response);
}
}
Popular Service Integrations
Weather Services
class WeatherService extends ApiKeyService {
constructor(config: ServiceConfig, context: PluginRuntimeContext, plugin: BasePlugin) {
const baseUrls = {
openweathermap: 'https://api.openweathermap.org/data/2.5',
weatherapi: 'https://api.weatherapi.com/v1',
accuweather: 'https://dataservice.accuweather.com'
};
super({
...config,
baseUrl: baseUrls[config.provider] || config.baseUrl
}, context, plugin);
}
async getCurrentWeather(location: string): Promise<WeatherData> {
const cacheKey = `weather_${location}`;
// Check cache first (weather data valid for 10 minutes)
const cached = await this.getCached<WeatherData>(cacheKey, 10 * 60 * 1000);
if (cached) {
return cached;
}
let endpoint: string;
let params: Record<string, string>;
switch (this.config.provider) {
case 'openweathermap':
endpoint = '/weather';
params = {
q: location,
appid: this.config.apiKey,
units: this.config.units || 'metric'
};
break;
case 'weatherapi':
endpoint = '/current.json';
params = {
key: this.config.apiKey,
q: location,
aqi: 'no'
};
break;
default:
throw new Error(`Unsupported weather provider: ${this.config.provider}`);
}
const url = `${endpoint}?${new URLSearchParams(params)}`;
const response = await this.makeRequest<any>(url);
const weatherData = this.normalizeWeatherData(response.data);
// Cache the result
await this.setCached(cacheKey, weatherData, 10 * 60 * 1000);
return weatherData;
}
private normalizeWeatherData(rawData: any): WeatherData {
// Normalize different provider formats to a common structure
switch (this.config.provider) {
case 'openweathermap':
return {
temperature: rawData.main.temp,
description: rawData.weather[0].description,
humidity: rawData.main.humidity,
windSpeed: rawData.wind.speed,
location: rawData.name,
timestamp: new Date()
};
case 'weatherapi':
return {
temperature: rawData.current.temp_c,
description: rawData.current.condition.text,
humidity: rawData.current.humidity,
windSpeed: rawData.current.wind_kph,
location: rawData.location.name,
timestamp: new Date()
};
default:
return rawData;
}
}
}
interface WeatherData {
temperature: number;
description: string;
humidity: number;
windSpeed: number;
location: string;
timestamp: Date;
}
GitHub Integration
class GitHubService extends ApiKeyService {
constructor(config: ServiceConfig, context: PluginRuntimeContext, plugin: BasePlugin) {
super({
...config,
baseUrl: 'https://api.github.com'
}, context, plugin);
}
async getRepositories(username: string): Promise<Repository[]> {
const response = await this.makeRequest<Repository[]>(`/users/${username}/repos`);
return response.data;
}
async getRepository(owner: string, repo: string): Promise<Repository> {
const response = await this.makeRequest<Repository>(`/repos/${owner}/${repo}`);
return response.data;
}
async createIssue(owner: string, repo: string, issue: CreateIssueData): Promise<Issue> {
const response = await this.makeRequest<Issue>(`/repos/${owner}/${repo}/issues`, {
method: 'POST',
body: JSON.stringify(issue)
});
return response.data;
}
protected async makeRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ServiceResponse<T>> {
const headers = {
'Authorization': `token ${this.config.apiKey}`,
'Accept': 'application/vnd.github.v3+json',
'User-Agent': `Qirvo-Plugin/${this.plugin.context.plugin.version}`,
...options.headers
};
const response = await this.context.api.http.request(
`${this.config.baseUrl}${endpoint}`,
{ ...options, headers }
);
// Handle GitHub rate limiting
if (response.status === 403) {
const resetTime = response.headers.get('X-RateLimit-Reset');
if (resetTime) {
const resetDate = new Date(parseInt(resetTime) * 1000);
throw new Error(`Rate limit exceeded. Resets at ${resetDate.toISOString()}`);
}
}
return this.processResponse<T>(response);
}
}
Rate Limiting & Quotas
Rate Limiter Implementation
class RateLimiter {
private requests: Map<string, number[]> = new Map();
constructor(
private maxRequests: number,
private windowMs: number
) {}
async checkLimit(key: string): Promise<boolean> {
const now = Date.now();
const requests = this.requests.get(key) || [];
// Remove old requests outside the window
const validRequests = requests.filter(time => now - time < this.windowMs);
if (validRequests.length >= this.maxRequests) {
return false;
}
validRequests.push(now);
this.requests.set(key, validRequests);
return true;
}
async waitForSlot(key: string): Promise<void> {
while (!(await this.checkLimit(key))) {
const requests = this.requests.get(key) || [];
const oldestRequest = Math.min(...requests);
const waitTime = this.windowMs - (Date.now() - oldestRequest);
if (waitTime > 0) {
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
}
// Usage in service
class RateLimitedService extends ExternalService {
private rateLimiter: RateLimiter;
constructor(config: ServiceConfig, context: PluginRuntimeContext, plugin: BasePlugin) {
super(config, context, plugin);
// Configure rate limiting based on service
const limits = {
github: { requests: 5000, window: 60 * 60 * 1000 }, // 5000/hour
twitter: { requests: 300, window: 15 * 60 * 1000 }, // 300/15min
default: { requests: 100, window: 60 * 1000 } // 100/min
};
const limit = limits[config.provider] || limits.default;
this.rateLimiter = new RateLimiter(limit.requests, limit.window);
}
protected async makeRequest<T>(
endpoint: string,
options: RequestInit = {}
): Promise<ServiceResponse<T>> {
const rateLimitKey = `${this.config.provider}_${this.config.apiKey?.slice(-4)}`;
// Wait for rate limit slot
await this.rateLimiter.waitForSlot(rateLimitKey);
return await super.makeRequest<T>(endpoint, options);
}
}
Error Handling
Service Error Handler
class ServiceError extends Error {
constructor(
message: string,
public type: string,
public service: string,
public operation: string,
public retryable: boolean = false,
public originalError?: any
) {
super(message);
this.name = 'ServiceError';
}
}
class ServiceErrorHandler {
static async handleError(error: any, service: string, operation: string): Promise<never> {
let errorMessage = 'Unknown service error';
let errorType = 'service_error';
let retryable = false;
if (error.response) {
const status = error.response.status;
switch (status) {
case 400:
errorMessage = 'Bad request - check your parameters';
errorType = 'bad_request';
break;
case 401:
errorMessage = 'Authentication failed - check your API key';
errorType = 'auth_error';
break;
case 403:
errorMessage = 'Access forbidden - insufficient permissions';
errorType = 'permission_error';
break;
case 429:
errorMessage = 'Rate limit exceeded';
errorType = 'rate_limit';
retryable = true;
break;
case 500:
case 502:
case 503:
case 504:
errorMessage = 'Service temporarily unavailable';
errorType = 'service_unavailable';
retryable = true;
break;
}
}
const serviceError = new ServiceError(errorMessage, errorType, service, operation, retryable);
serviceError.originalError = error;
throw serviceError;
}
}
Security Best Practices
Secure Configuration Storage
class SecureServiceConfig {
constructor(private plugin: BasePlugin) {}
async storeCredentials(service: string, credentials: any): Promise<void> {
// Encrypt sensitive data before storage
const encrypted = await this.encrypt(JSON.stringify(credentials));
await this.plugin.setStorage(`credentials_${service}`, encrypted);
}
async getCredentials(service: string): Promise<any> {
const encrypted = await this.plugin.getStorage(`credentials_${service}`);
if (!encrypted) return null;
const decrypted = await this.decrypt(encrypted);
return JSON.parse(decrypted);
}
private async encrypt(data: string): Promise<string> {
// In a real implementation, use proper encryption
return btoa(data);
}
private async decrypt(data: string): Promise<string> {
return atob(data);
}
}
Input Validation
class InputValidator {
static validateUrl(url: string): boolean {
try {
const parsed = new URL(url);
return parsed.protocol === 'https:';
} catch {
return false;
}
}
static sanitizeString(input: string): string {
return input
.trim()
.replace(/[<>]/g, '')
.substring(0, 1000);
}
static validateApiKey(service: string, apiKey: string): boolean {
const patterns = {
github: /^ghp_[a-zA-Z0-9]{36}$/,
slack: /^xoxb-[0-9]+-[0-9]+-[a-zA-Z0-9]+$/,
weather: /^[a-zA-Z0-9]{16,}$/
};
const pattern = patterns[service];
return pattern ? pattern.test(apiKey) : apiKey.length >= 8;
}
}
This comprehensive guide provides everything needed to integrate external services securely and efficiently in your Qirvo plugins.
Next: Plugin Lifecycle for understanding plugin lifecycle management.