UNPKG

onairos

Version:

The Onairos Library is a collection of functions that enable Applications to connect and communicate data with Onairos Identities via User Authorization. Integration for developers is seamless, simple and effective for all applications. LLM SDK capabiliti

1,949 lines (1,656 loc) 51.2 kB
# Onairos SDK Integration Examples ## Overview This document provides practical integration examples for the Onairos SDK across different frameworks, platforms, and use cases. All examples follow best practices for security, performance, and user experience. ## Table of Contents 1. [Frontend Integrations](#frontend-integrations) 2. [Backend Integrations](#backend-integrations) 3. [Mobile Integrations](#mobile-integrations) 4. [Full-Stack Examples](#full-stack-examples) 5. [Testing Examples](#testing-examples) 6. [Production Deployment](#production-deployment) ## Frontend Integrations ### React Integration #### Complete React Component with Context ```javascript // contexts/OnairosContext.js import React, { createContext, useContext, useState, useEffect } from 'react'; const OnairosContext = createContext(); export const useOnairos = () => { const context = useContext(OnairosContext); if (!context) { throw new Error('useOnairos must be used within an OnairosProvider'); } return context; }; export const OnairosProvider = ({ children }) => { const [authToken, setAuthToken] = useState(localStorage.getItem('authToken')); const [connections, setConnections] = useState({}); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const apiKey = process.env.REACT_APP_ONAIROS_API_KEY; const baseUrl = process.env.REACT_APP_ONAIROS_BASE_URL; // Initialize SDK const sdk = { request: async (endpoint, options = {}) => { const url = `${baseUrl}${endpoint}`; const headers = { 'x-api-key': apiKey, 'authorization': `Bearer ${authToken}`, 'content-type': 'application/json', ...options.headers }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Request failed'); } return response.json(); } }; // YouTube authentication const connectYoutube = async () => { setLoading(true); setError(null); try { // Initialize Google Auth await new Promise((resolve) => { window.gapi.load('auth2', resolve); }); const authInstance = window.gapi.auth2.getAuthInstance(); if (!authInstance) { window.gapi.auth2.init({ client_id: process.env.REACT_APP_GOOGLE_CLIENT_ID, scope: 'https://www.googleapis.com/auth/youtube.readonly openid profile email', access_type: 'offline', prompt: 'consent' }); } const user = await authInstance.signIn(); const authResponse = user.getAuthResponse(); // Send to Onairos const result = await sdk.request('/youtube/native-auth', { method: 'POST', body: JSON.stringify({ accessToken: authResponse.access_token, refreshToken: authResponse.server_auth_code, idToken: authResponse.id_token, userAccountInfo: { username: localStorage.getItem('username'), email: user.getBasicProfile().getEmail(), channelName: user.getBasicProfile().getName() } }) }); if (result.success) { setConnections(prev => ({ ...prev, youtube: result.connectionData })); // Show success message alert('YouTube connected successfully!'); } } catch (error) { setError(error.message); console.error('YouTube connection error:', error); } finally { setLoading(false); } }; // LinkedIn authentication const connectLinkedIn = async () => { setLoading(true); setError(null); try { // LinkedIn OAuth flow would go here // This is a simplified example const linkedinAuth = await performLinkedInAuth(); const result = await sdk.request('/linkedin/native-auth', { method: 'POST', body: JSON.stringify({ accessToken: linkedinAuth.accessToken, refreshToken: linkedinAuth.refreshToken, userAccountInfo: { username: localStorage.getItem('username'), email: linkedinAuth.email, firstName: linkedinAuth.firstName, lastName: linkedinAuth.lastName } }) }); if (result.success) { setConnections(prev => ({ ...prev, linkedin: result.connectionData })); alert('LinkedIn connected successfully!'); } } catch (error) { setError(error.message); console.error('LinkedIn connection error:', error); } finally { setLoading(false); } }; // Check connection health const checkHealth = async () => { try { const result = await sdk.request(`/validation/health-check/${localStorage.getItem('username')}`); return result; } catch (error) { console.error('Health check error:', error); return null; } }; // Repair connections const repairConnections = async (platforms = null) => { setLoading(true); try { const result = await sdk.request(`/validation/repair-connections/${localStorage.getItem('username')}`, { method: 'POST', body: JSON.stringify({ platforms: platforms }) }); if (result.success) { // Update connection statuses result.successfulRepairs.forEach(platform => { setConnections(prev => ({ ...prev, [platform]: { ...prev[platform], status: 'healthy' } })); }); } return result; } catch (error) { setError(error.message); return null; } finally { setLoading(false); } }; // Auto-check health on mount useEffect(() => { if (authToken) { checkHealth().then(result => { if (result) { setConnections(result.platforms); } }); } }, [authToken]); const value = { authToken, setAuthToken, connections, loading, error, connectYoutube, connectLinkedIn, checkHealth, repairConnections }; return ( <OnairosContext.Provider value={value}> {children} </OnairosContext.Provider> ); }; ``` ```javascript // components/ConnectionManager.js import React, { useState, useEffect } from 'react'; import { useOnairos } from '../contexts/OnairosContext'; const ConnectionManager = () => { const { connections, loading, error, connectYoutube, connectLinkedIn, checkHealth, repairConnections } = useOnairos(); const [healthData, setHealthData] = useState(null); const [showDetails, setShowDetails] = useState({}); useEffect(() => { // Check health every 5 minutes const interval = setInterval(async () => { const health = await checkHealth(); setHealthData(health); }, 5 * 60 * 1000); return () => clearInterval(interval); }, []); const getConnectionStatus = (platform) => { const connection = connections[platform]; if (!connection) return 'not_connected'; return connection.status || 'unknown'; }; const getStatusColor = (status) => { switch (status) { case 'healthy': return 'green'; case 'expired_refreshable': return 'yellow'; case 'expired_no_refresh': return 'red'; case 'not_connected': return 'gray'; default: return 'gray'; } }; const handleRepair = async (platform) => { const result = await repairConnections([platform]); if (result) { alert(`Repair completed: ${result.successfulRepairs.length} platforms fixed`); } }; return ( <div className="connection-manager"> <h2>Platform Connections</h2> {error && ( <div className="error-message"> Error: {error} </div> )} {loading && ( <div className="loading"> Loading... </div> )} <div className="platforms"> {/* YouTube */} <div className="platform-card"> <div className="platform-header"> <h3>YouTube</h3> <div className="status-indicator" style={{ backgroundColor: getStatusColor(getConnectionStatus('youtube')) }} /> </div> <div className="platform-details"> {connections.youtube ? ( <div> <p>Channel: {connections.youtube.channelName}</p> <p>Connected: {new Date(connections.youtube.connectedAt).toLocaleDateString()}</p> <p>Status: {getConnectionStatus('youtube')}</p> {getConnectionStatus('youtube') === 'expired_refreshable' && ( <button onClick={() => handleRepair('youtube')}> Refresh Token </button> )} <button onClick={() => setShowDetails(prev => ({ ...prev, youtube: !prev.youtube }))}> {showDetails.youtube ? 'Hide' : 'Show'} Details </button> {showDetails.youtube && ( <div className="connection-details"> <p>Has Refresh Token: {connections.youtube.hasRefreshToken ? 'Yes' : 'No'}</p> <p>Token Expiry: {new Date(connections.youtube.tokenExpiry).toLocaleString()}</p> <p>Last Validated: {connections.youtube.lastValidated ? new Date(connections.youtube.lastValidated).toLocaleString() : 'Never'}</p> </div> )} </div> ) : ( <div> <p>Not connected</p> <button onClick={connectYoutube} disabled={loading}> Connect YouTube </button> </div> )} </div> </div> {/* LinkedIn */} <div className="platform-card"> <div className="platform-header"> <h3>LinkedIn</h3> <div className="status-indicator" style={{ backgroundColor: getStatusColor(getConnectionStatus('linkedin')) }} /> </div> <div className="platform-details"> {connections.linkedin ? ( <div> <p>Name: {connections.linkedin.userName}</p> <p>Connected: {new Date(connections.linkedin.connectedAt).toLocaleDateString()}</p> <p>Status: {getConnectionStatus('linkedin')}</p> {getConnectionStatus('linkedin') === 'expired_refreshable' && ( <button onClick={() => handleRepair('linkedin')}> Refresh Token </button> )} </div> ) : ( <div> <p>Not connected</p> <button onClick={connectLinkedIn} disabled={loading}> Connect LinkedIn </button> </div> )} </div> </div> </div> {/* Health Summary */} {healthData && ( <div className="health-summary"> <h3>Connection Health</h3> <div className="health-metrics"> <div className="metric"> <span>Overall Score:</span> <span>{healthData.summary.overallScore}%</span> </div> <div className="metric"> <span>Connected Platforms:</span> <span>{healthData.summary.connectedPlatforms}</span> </div> <div className="metric"> <span>Healthy Platforms:</span> <span>{healthData.summary.healthyPlatforms}</span> </div> </div> {healthData.recommendations.length > 0 && ( <div className="recommendations"> <h4>Recommendations:</h4> <ul> {healthData.recommendations.map((rec, index) => ( <li key={index} className={`recommendation ${rec.severity}`}> {rec.message} </li> ))} </ul> </div> )} </div> )} </div> ); }; export default ConnectionManager; ``` ```css /* styles/ConnectionManager.css */ .connection-manager { max-width: 800px; margin: 0 auto; padding: 20px; } .error-message { background-color: #fee; border: 1px solid #fcc; padding: 10px; margin: 10px 0; border-radius: 4px; color: #c33; } .loading { text-align: center; padding: 20px; color: #666; } .platforms { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; } .platform-card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .platform-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .platform-header h3 { margin: 0; color: #333; } .status-indicator { width: 12px; height: 12px; border-radius: 50%; border: 1px solid #ddd; } .platform-details { color: #666; } .platform-details p { margin: 5px 0; } .platform-details button { margin: 10px 5px 0 0; padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; } .platform-details button:hover { background: #f5f5f5; } .platform-details button:disabled { opacity: 0.6; cursor: not-allowed; } .connection-details { margin-top: 10px; padding: 10px; background: #f9f9f9; border-radius: 4px; font-size: 0.9em; } .health-summary { margin-top: 30px; padding: 20px; background: #f9f9f9; border-radius: 8px; } .health-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin: 15px 0; } .metric { display: flex; justify-content: space-between; padding: 10px; background: white; border-radius: 4px; border: 1px solid #ddd; } .recommendations { margin-top: 15px; } .recommendations ul { list-style: none; padding: 0; } .recommendation { padding: 8px 12px; margin: 5px 0; border-radius: 4px; border-left: 4px solid; } .recommendation.info { background: #e7f3ff; border-left-color: #0066cc; } .recommendation.warning { background: #fff3cd; border-left-color: #ff9900; } .recommendation.error { background: #f8d7da; border-left-color: #dc3545; } ``` ### Vue.js Integration ```javascript // plugins/onairos.js import { ref, reactive, computed } from 'vue'; export default { install(app, options) { const state = reactive({ authToken: localStorage.getItem('authToken'), connections: {}, loading: false, error: null }); const sdk = { async request(endpoint, options = {}) { const url = `${process.env.VUE_APP_ONAIROS_BASE_URL}${endpoint}`; const headers = { 'x-api-key': process.env.VUE_APP_ONAIROS_API_KEY, 'authorization': `Bearer ${state.authToken}`, 'content-type': 'application/json', ...options.headers }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'Request failed'); } return response.json(); }, async connectYoutube() { state.loading = true; state.error = null; try { // YouTube OAuth implementation const result = await this.request('/youtube/native-auth', { method: 'POST', body: JSON.stringify({ accessToken: 'youtube_token', userAccountInfo: { username: 'user123' } }) }); if (result.success) { state.connections.youtube = result.connectionData; } } catch (error) { state.error = error.message; } finally { state.loading = false; } }, async checkHealth() { try { const result = await this.request(`/validation/health-check/user123`); return result; } catch (error) { console.error('Health check error:', error); return null; } } }; app.config.globalProperties.$onairos = sdk; app.provide('onairos', sdk); app.provide('onairosState', state); } }; ``` ```vue <!-- components/PlatformConnections.vue --> <template> <div class="platform-connections"> <h2>Platform Connections</h2> <div v-if="state.error" class="error"> {{ state.error }} </div> <div class="platforms"> <div class="platform-card" v-for="platform in platforms" :key="platform.name"> <div class="platform-header"> <h3>{{ platform.name }}</h3> <div class="status-indicator" :class="getStatusClass(platform.key)" /> </div> <div class="platform-content"> <div v-if="isConnected(platform.key)"> <p>Status: {{ getConnectionStatus(platform.key) }}</p> <p>Connected: {{ formatDate(getConnection(platform.key).connectedAt) }}</p> <button v-if="needsRefresh(platform.key)" @click="refreshConnection(platform.key)" :disabled="state.loading" > Refresh Token </button> </div> <div v-else> <p>Not connected</p> <button @click="connectPlatform(platform.key)" :disabled="state.loading" > Connect {{ platform.name }} </button> </div> </div> </div> </div> <div v-if="healthData" class="health-summary"> <h3>Health Summary</h3> <div class="health-metrics"> <div class="metric"> <span>Overall Score:</span> <span>{{ healthData.summary.overallScore }}%</span> </div> <div class="metric"> <span>Connected:</span> <span>{{ healthData.summary.connectedPlatforms }}</span> </div> <div class="metric"> <span>Healthy:</span> <span>{{ healthData.summary.healthyPlatforms }}</span> </div> </div> </div> </div> </template> <script> import { inject, ref, onMounted, computed } from 'vue'; export default { name: 'PlatformConnections', setup() { const onairos = inject('onairos'); const state = inject('onairosState'); const healthData = ref(null); const platforms = [ { name: 'YouTube', key: 'youtube' }, { name: 'LinkedIn', key: 'linkedin' }, { name: 'Reddit', key: 'reddit' } ]; const isConnected = (platform) => { return state.connections[platform]?.connected || false; }; const getConnection = (platform) => { return state.connections[platform] || {}; }; const getConnectionStatus = (platform) => { return state.connections[platform]?.status || 'unknown'; }; const getStatusClass = (platform) => { const status = getConnectionStatus(platform); return `status-${status.replace('_', '-')}`; }; const needsRefresh = (platform) => { return getConnectionStatus(platform) === 'expired_refreshable'; }; const formatDate = (dateString) => { return dateString ? new Date(dateString).toLocaleDateString() : 'N/A'; }; const connectPlatform = async (platform) => { switch (platform) { case 'youtube': await onairos.connectYoutube(); break; case 'linkedin': await onairos.connectLinkedIn(); break; default: console.warn(`Connection for ${platform} not implemented`); } }; const refreshConnection = async (platform) => { try { const result = await onairos.request(`/validation/repair-connections/user123`, { method: 'POST', body: JSON.stringify({ platforms: [platform] }) }); if (result.success) { // Update connection status await loadHealthData(); } } catch (error) { state.error = error.message; } }; const loadHealthData = async () => { const result = await onairos.checkHealth(); if (result) { healthData.value = result; state.connections = result.platforms; } }; onMounted(() => { loadHealthData(); // Auto-refresh health data every 5 minutes setInterval(loadHealthData, 5 * 60 * 1000); }); return { state, platforms, healthData, isConnected, getConnection, getConnectionStatus, getStatusClass, needsRefresh, formatDate, connectPlatform, refreshConnection }; } }; </script> <style scoped> .platform-connections { max-width: 800px; margin: 0 auto; padding: 20px; } .error { background-color: #fee; border: 1px solid #fcc; padding: 10px; margin: 10px 0; border-radius: 4px; color: #c33; } .platforms { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; } .platform-card { border: 1px solid #ddd; border-radius: 8px; padding: 20px; background: white; box-shadow: 0 2px 4px rgba(0,0,0,0.1); } .platform-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .platform-header h3 { margin: 0; color: #333; } .status-indicator { width: 12px; height: 12px; border-radius: 50%; border: 1px solid #ddd; } .status-healthy { background-color: #4caf50; } .status-expired-refreshable { background-color: #ff9800; } .status-expired-no-refresh { background-color: #f44336; } .status-not-connected { background-color: #9e9e9e; } .platform-content { color: #666; } .platform-content p { margin: 5px 0; } .platform-content button { margin: 10px 5px 0 0; padding: 8px 16px; border: 1px solid #ddd; border-radius: 4px; background: white; cursor: pointer; } .platform-content button:hover { background: #f5f5f5; } .platform-content button:disabled { opacity: 0.6; cursor: not-allowed; } .health-summary { margin-top: 30px; padding: 20px; background: #f9f9f9; border-radius: 8px; } .health-metrics { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; margin: 15px 0; } .metric { display: flex; justify-content: space-between; padding: 10px; background: white; border-radius: 4px; border: 1px solid #ddd; } </style> ``` ## Backend Integrations ### Node.js/Express Complete Implementation ```javascript // server.js import express from 'express'; import cors from 'cors'; import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import { OnairosSDK } from './lib/onairos-sdk.js'; import { authenticateUser } from './middleware/auth.js'; import { errorHandler } from './middleware/error-handler.js'; const app = express(); const port = process.env.PORT || 3000; // Security middleware app.use(helmet()); app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:3000'], credentials: true })); // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again later.' }); app.use('/api/', limiter); // Body parsing app.use(express.json({ limit: '10mb' })); app.use(express.urlencoded({ extended: true, limit: '10mb' })); // Initialize Onairos SDK const onairos = new OnairosSDK({ apiKey: process.env.ONAIROS_API_KEY, baseUrl: process.env.ONAIROS_BASE_URL, jwtSecret: process.env.ONAIROS_JWT_SECRET }); // Routes app.use('/api/auth', authenticateUser); // YouTube routes app.post('/api/youtube/connect', authenticateUser, async (req, res, next) => { try { const { accessToken, refreshToken, userAccountInfo } = req.body; // Validate required fields if (!accessToken || !userAccountInfo?.username) { return res.status(400).json({ success: false, error: 'Missing required fields: accessToken and userAccountInfo.username' }); } const result = await onairos.youtube.authenticate({ accessToken, refreshToken, userAccountInfo: { ...userAccountInfo, username: req.user.username // Use authenticated user's username } }); res.json(result); } catch (error) { next(error); } }); app.get('/api/youtube/status', authenticateUser, async (req, res, next) => { try { const result = await onairos.youtube.getConnectionStatus(req.user.username); res.json(result); } catch (error) { next(error); } }); app.post('/api/youtube/refresh', authenticateUser, async (req, res, next) => { try { const result = await onairos.youtube.refreshToken(req.user.username); res.json(result); } catch (error) { next(error); } }); // LinkedIn routes app.post('/api/linkedin/connect', authenticateUser, async (req, res, next) => { try { const { accessToken, refreshToken, userAccountInfo } = req.body; if (!accessToken || !userAccountInfo?.username) { return res.status(400).json({ success: false, error: 'Missing required fields: accessToken and userAccountInfo.username' }); } const result = await onairos.linkedin.authenticate({ accessToken, refreshToken, userAccountInfo: { ...userAccountInfo, username: req.user.username } }); res.json(result); } catch (error) { next(error); } }); app.get('/api/linkedin/status', authenticateUser, async (req, res, next) => { try { const result = await onairos.linkedin.getConnectionStatus(req.user.username); res.json(result); } catch (error) { next(error); } }); // Validation routes app.get('/api/health', authenticateUser, async (req, res, next) => { try { const result = await onairos.validation.healthCheck(req.user.username); res.json(result); } catch (error) { next(error); } }); app.post('/api/repair', authenticateUser, async (req, res, next) => { try { const { platforms } = req.body; const result = await onairos.validation.repairConnections(req.user.username, platforms); res.json(result); } catch (error) { next(error); } }); // Admin routes app.get('/api/admin/system-health', authenticateUser, async (req, res, next) => { try { // Check if user is admin if (!req.user.isAdmin) { return res.status(403).json({ success: false, error: 'Admin access required' }); } const result = await onairos.validation.systemHealth(); res.json(result); } catch (error) { next(error); } }); // Error handling app.use(errorHandler); // 404 handler app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Endpoint not found' }); }); app.listen(port, () => { console.log(`Server running on port ${port}`); console.log(`Environment: ${process.env.NODE_ENV}`); }); ``` ```javascript // lib/onairos-sdk.js import fetch from 'node-fetch'; export class OnairosSDK { constructor(options) { this.apiKey = options.apiKey; this.baseUrl = options.baseUrl; this.jwtSecret = options.jwtSecret; if (!this.apiKey) { throw new Error('API key is required'); } if (!this.baseUrl) { throw new Error('Base URL is required'); } } async request(endpoint, options = {}) { const url = `${this.baseUrl}${endpoint}`; const headers = { 'x-api-key': this.apiKey, 'content-type': 'application/json', ...options.headers }; const response = await fetch(url, { ...options, headers }); if (!response.ok) { const error = await response.json(); throw new Error(error.message || `Request failed with status ${response.status}`); } return response.json(); } // YouTube methods get youtube() { return { authenticate: async (data) => { return this.request('/youtube/native-auth', { method: 'POST', body: JSON.stringify(data) }); }, getConnectionStatus: async (username) => { return this.request(`/youtube/connection-status/${username}`); }, refreshToken: async (username) => { return this.request('/youtube/refresh-token', { method: 'POST', body: JSON.stringify({ username }) }); }, validateConnection: async (username) => { return this.request(`/youtube/validate-connection/${username}`, { method: 'POST' }); } }; } // LinkedIn methods get linkedin() { return { authenticate: async (data) => { return this.request('/linkedin/native-auth', { method: 'POST', body: JSON.stringify(data) }); }, getConnectionStatus: async (username) => { return this.request(`/linkedin/connection-status/${username}`); }, refreshToken: async (username) => { return this.request('/linkedin/refresh-token', { method: 'POST', body: JSON.stringify({ username }) }); }, validateConnection: async (username) => { return this.request(`/linkedin/validate-connection/${username}`, { method: 'POST' }); } }; } // Validation methods get validation() { return { healthCheck: async (username) => { return this.request(`/validation/health-check/${username}`); }, repairConnections: async (username, platforms = null) => { return this.request(`/validation/repair-connections/${username}`, { method: 'POST', body: JSON.stringify({ platforms }) }); }, migrationStatus: async (username) => { return this.request(`/validation/migration-status/${username}`); }, systemHealth: async () => { return this.request('/validation/system-health'); } }; } } ``` ```javascript // middleware/auth.js import jwt from 'jsonwebtoken'; export const authenticateUser = (req, res, next) => { const authHeader = req.headers.authorization; if (!authHeader) { return res.status(401).json({ success: false, error: 'Authorization header required' }); } const token = authHeader.replace('Bearer ', ''); try { const decoded = jwt.verify(token, process.env.ONAIROS_JWT_SECRET); // Attach user info to request req.user = { id: decoded.userId || decoded.id, username: decoded.username, email: decoded.email, userType: decoded.userType, isAdmin: decoded.permissions?.includes('admin:*') || false }; next(); } catch (error) { return res.status(401).json({ success: false, error: 'Invalid or expired token' }); } }; ``` ```javascript // middleware/error-handler.js export const errorHandler = (err, req, res, next) => { console.error('Error:', err); // Handle specific error types if (err.name === 'ValidationError') { return res.status(400).json({ success: false, error: 'Validation failed', details: err.message }); } if (err.name === 'JsonWebTokenError') { return res.status(401).json({ success: false, error: 'Invalid token' }); } if (err.name === 'TokenExpiredError') { return res.status(401).json({ success: false, error: 'Token expired' }); } // Handle rate limit errors if (err.message.includes('Rate limit')) { return res.status(429).json({ success: false, error: 'Rate limit exceeded', guidance: 'Please wait before making more requests' }); } // Generic error handler res.status(500).json({ success: false, error: 'Internal server error', requestId: req.id || 'unknown' }); }; ``` ## Testing Examples ### Jest Unit Tests ```javascript // tests/onairos-sdk.test.js import { OnairosSDK } from '../lib/onairos-sdk.js'; import fetch from 'node-fetch'; jest.mock('node-fetch'); describe('OnairosSDK', () => { let sdk; beforeEach(() => { sdk = new OnairosSDK({ apiKey: 'ona_test_api_key', baseUrl: 'https://api.test.onairos.uk', jwtSecret: 'test_jwt_secret' }); fetch.mockClear(); }); describe('constructor', () => { it('should throw error if API key is missing', () => { expect(() => { new OnairosSDK({ baseUrl: 'https://api.test.onairos.uk' }); }).toThrow('API key is required'); }); it('should throw error if base URL is missing', () => { expect(() => { new OnairosSDK({ apiKey: 'test_key' }); }).toThrow('Base URL is required'); }); }); describe('request method', () => { it('should make successful request', async () => { const mockResponse = { success: true, data: { test: 'data' } }; fetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse }); const result = await sdk.request('/test-endpoint'); expect(fetch).toHaveBeenCalledWith( 'https://api.test.onairos.uk/test-endpoint', { headers: { 'x-api-key': 'ona_test_api_key', 'content-type': 'application/json' } } ); expect(result).toEqual(mockResponse); }); it('should handle request errors', async () => { const mockError = { success: false, error: 'Test error' }; fetch.mockResolvedValueOnce({ ok: false, json: async () => mockError }); await expect(sdk.request('/test-endpoint')).rejects.toThrow('Test error'); }); }); describe('YouTube methods', () => { it('should authenticate YouTube connection', async () => { const mockResponse = { success: true, connectionData: { platform: 'youtube' } }; fetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse }); const result = await sdk.youtube.authenticate({ accessToken: 'youtube_token', userAccountInfo: { username: 'test_user' } }); expect(fetch).toHaveBeenCalledWith( 'https://api.test.onairos.uk/youtube/native-auth', { method: 'POST', body: JSON.stringify({ accessToken: 'youtube_token', userAccountInfo: { username: 'test_user' } }), headers: { 'x-api-key': 'ona_test_api_key', 'content-type': 'application/json' } } ); expect(result).toEqual(mockResponse); }); it('should get connection status', async () => { const mockResponse = { success: true, connectionHealth: { status: 'healthy' } }; fetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse }); const result = await sdk.youtube.getConnectionStatus('test_user'); expect(fetch).toHaveBeenCalledWith( 'https://api.test.onairos.uk/youtube/connection-status/test_user', { headers: { 'x-api-key': 'ona_test_api_key', 'content-type': 'application/json' } } ); expect(result).toEqual(mockResponse); }); }); describe('Validation methods', () => { it('should perform health check', async () => { const mockResponse = { success: true, summary: { overallScore: 85 } }; fetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse }); const result = await sdk.validation.healthCheck('test_user'); expect(fetch).toHaveBeenCalledWith( 'https://api.test.onairos.uk/validation/health-check/test_user', { headers: { 'x-api-key': 'ona_test_api_key', 'content-type': 'application/json' } } ); expect(result).toEqual(mockResponse); }); it('should repair connections', async () => { const mockResponse = { success: true, successfulRepairs: ['youtube'] }; fetch.mockResolvedValueOnce({ ok: true, json: async () => mockResponse }); const result = await sdk.validation.repairConnections('test_user', ['youtube']); expect(fetch).toHaveBeenCalledWith( 'https://api.test.onairos.uk/validation/repair-connections/test_user', { method: 'POST', body: JSON.stringify({ platforms: ['youtube'] }), headers: { 'x-api-key': 'ona_test_api_key', 'content-type': 'application/json' } } ); expect(result).toEqual(mockResponse); }); }); }); ``` ### Integration Tests ```javascript // tests/integration/api.test.js import request from 'supertest'; import app from '../../server.js'; import jwt from 'jsonwebtoken'; describe('API Integration Tests', () => { let authToken; beforeAll(() => { // Create test JWT token authToken = jwt.sign( { userId: 'test_user_id', username: 'test_user', email: 'test@example.com', userType: 'onairos' }, process.env.ONAIROS_JWT_SECRET, { expiresIn: '1h' } ); }); describe('YouTube endpoints', () => { it('should connect YouTube account', async () => { const response = await request(app) .post('/api/youtube/connect') .set('Authorization', `Bearer ${authToken}`) .send({ accessToken: 'mock_youtube_token', refreshToken: 'mock_refresh_token', userAccountInfo: { username: 'test_user', email: 'test@example.com', channelName: 'Test Channel' } }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.connectionData.platform).toBe('youtube'); }); it('should get YouTube connection status', async () => { const response = await request(app) .get('/api/youtube/status') .set('Authorization', `Bearer ${authToken}`); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.platform).toBe('youtube'); }); it('should refresh YouTube token', async () => { const response = await request(app) .post('/api/youtube/refresh') .set('Authorization', `Bearer ${authToken}`); expect(response.status).toBe(200); expect(response.body.success).toBe(true); }); }); describe('Health check endpoints', () => { it('should perform health check', async () => { const response = await request(app) .get('/api/health') .set('Authorization', `Bearer ${authToken}`); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.summary).toBeDefined(); expect(response.body.platforms).toBeDefined(); }); it('should repair connections', async () => { const response = await request(app) .post('/api/repair') .set('Authorization', `Bearer ${authToken}`) .send({ platforms: ['youtube'] }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); expect(response.body.repairResults).toBeDefined(); }); }); describe('Authentication', () => { it('should reject requests without auth token', async () => { const response = await request(app) .get('/api/health'); expect(response.status).toBe(401); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Authorization header required'); }); it('should reject requests with invalid token', async () => { const response = await request(app) .get('/api/health') .set('Authorization', 'Bearer invalid_token'); expect(response.status).toBe(401); expect(response.body.success).toBe(false); expect(response.body.error).toBe('Invalid or expired token'); }); }); }); ``` ### E2E Tests with Cypress ```javascript // cypress/integration/platform-connections.spec.js describe('Platform Connections', () => { beforeEach(() => { // Mock authentication cy.window().then((win) => { win.localStorage.setItem('authToken', 'mock_jwt_token'); }); // Mock API responses cy.intercept('GET', '/api/health', { fixture: 'health-check-response.json' }).as('healthCheck'); cy.intercept('POST', '/api/youtube/connect', { fixture: 'youtube-connect-response.json' }).as('youtubeConnect'); cy.visit('/'); }); it('should display platform connections', () => { cy.get('[data-cy=platform-connections]').should('be.visible'); cy.get('[data-cy=youtube-card]').should('be.visible'); cy.get('[data-cy=linkedin-card]').should('be.visible'); }); it('should connect YouTube account', () => { cy.get('[data-cy=youtube-connect-btn]').click(); // Mock Google OAuth flow cy.window().then((win) => { win.gapi = { auth2: { getAuthInstance: () => ({ signIn: () => Promise.resolve({ getAuthResponse: () => ({ access_token: 'mock_access_token', server_auth_code: 'mock_refresh_token', id_token: 'mock_id_token' }), getBasicProfile: () => ({ getEmail: () => 'test@example.com', getName: () => 'Test User' }) }) }) } }; }); cy.wait('@youtubeConnect'); cy.get('[data-cy=youtube-status]').should('contain', 'Connected'); }); it('should show health metrics', () => { cy.wait('@healthCheck'); cy.get('[data-cy=health-summary]').should('be.visible'); cy.get('[data-cy=overall-score]').should('contain', '85%'); }); it('should repair connections', () => { cy.intercept('POST', '/api/repair', { success: true, successfulRepairs: ['youtube'] }).as('repairConnections'); cy.get('[data-cy=repair-btn]').click(); cy.wait('@repairConnections'); cy.get('[data-cy=repair-success]').should('be.visible'); }); }); ``` ## Production Deployment ### Docker Configuration ```dockerfile # Dockerfile FROM node:18-alpine WORKDIR /app # Copy package files COPY package*.json ./ RUN npm ci --only=production # Copy source code COPY . . # Create non-root user RUN addgroup -g 1001 -S nodejs RUN adduser -S nextjs -u 1001 # Set permissions RUN chown -R nextjs:nodejs /app USER nextjs EXPOSE 3000 CMD ["npm", "start"] ``` ```yaml # docker-compose.yml version: '3.8' services: app: build: . ports: - "3000:3000" environment: - NODE_ENV=production - ONAIROS_API_KEY=${ONAIROS_API_KEY} - ONAIROS_BASE_URL=${ONAIROS_BASE_URL} - ONAIROS_JWT_SECRET=${ONAIROS_JWT_SECRET} depends_on: - redis - mongodb restart: unless-stopped redis: image: redis:6-alpine ports: - "6379:6379" restart: unless-stopped mongodb: image: mongo:5 ports: - "27017:27017" volumes: - mongo_data:/data/db restart: unless-stopped nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./ssl:/etc/nginx/ssl depends_on: - app restart: unless-stopped volumes: mongo_data: ``` ### Environment Configuration ```bash # .env.production NODE_ENV=production # Onairos SDK Configuration ONAIROS_API_KEY=ona_production_api_key_here ONAIROS_BASE_URL=https://api2.onairos.uk ONAIROS_JWT_SECRET=your_production_jwt_secret # Database Configuration MONGODB_URI=mongodb://mongodb:27017/onairos_production REDIS_URL=redis://redis:6379 # Security Configuration ALLOWED_ORIGINS=https://yourdomain.com,https://www.yourdomain.com SESSION_SECRET=your_session_secret BCRYPT_ROUNDS=12 # Rate Limiting RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_MAX=100 # Monitoring LOG_LEVEL=info METRICS_ENABLED=true HEALTH_CHECK_INTERVAL=30000 # OAuth Configuration GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret LINKEDIN_CLIENT_ID=your_linkedin_client_id LINKEDIN_CLIENT_SECRET=your_linkedin_client_secret ``` ### Kubernetes Deployment ```yaml # k8s/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: onairos-sdk-app spec: replicas: 3 selector: matchLabels: app: onairos-sdk-app template: metadata: labels: app: onairos-sdk-app spec: containers: - name: app image: your-registry/onairos-sdk-app:latest ports: - containerPort: 3000 env: - name: NODE_ENV value: "production" - name: ONAIROS_API_KEY valueFrom: secretKeyRef: name: onairos-secrets key: api-key - name: ONAIROS_JWT_SECRET valueFrom: secretKeyRef: name: onairos-secrets key: jwt-secret livenessProbe: httpGet: path: /health port: 3000 initialDelaySeconds: 30 periodSeconds: 10 readinessProbe: httpGet: path: /ready port: 3000 initialDelaySeconds: 5 periodSeconds: 5 resources: requests: memory: "256Mi" cpu: "250m" limits: memory: "512Mi" cpu: "500m" --- apiVersion: v1 kind: Service metadata: name: onairos-sdk-service spec: selector: app: onairos-sdk-app ports: - port: 80 targetPort: 3000 type: LoadBalancer ``` ### Monitoring and Logging ```javascript // lib/monitoring.js import winston from 'winston'; import prometheus from 'prom-client'; // Configure logging export const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), new winston.transports.Console({ format: winston.format.simple() }) ] }); // Configure metrics const register = new prometheus.Registry(); export const metrics = { httpRequests: new prometheus.Counter({ name: 'http_requests_total', help: 'Total number of HTTP requests', labelNames: ['method', 'route', 'status_code'], registers: [register] }), httpDuration: new prometheus.Histogram({ name: 'http_request_duration_seconds', help: 'Duration of HTTP requests in seconds', labelNames: ['method', 'route', 'status_code'], buckets: [0.1, 0.5, 1, 2, 5], registers: [register] }), onairosApiCalls: new prometheus.Counter({ name: 'onairos_api_calls_total',