UNPKG

failover-sdk

Version:

One-line API failover with zero downtime. Native Rust performance with TypeScript interface.

700 lines 27.8 kB
"use strict"; // Provider helpers and API interfaces // Adapted from failover-js but with cloud-first approach (no native dependencies) var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.payments = payments; exports.email = email; exports.custom = custom; // Helper function to inject authentication headers function getAuthHeaders(provider, service) { const headers = {}; if (service === 'payments') { switch (provider) { case 'stripe': if (process.env.STRIPE_SECRET_KEY) { headers['Authorization'] = `Bearer ${process.env.STRIPE_SECRET_KEY}`; } break; case 'square': if (process.env.SQUARE_ACCESS_TOKEN) { headers['Authorization'] = `Bearer ${process.env.SQUARE_ACCESS_TOKEN}`; } break; case 'paypal': // PayPal uses different auth mechanism (client credentials) // Would need separate implementation for OAuth break; case 'adyen': if (process.env.ADYEN_API_KEY) { headers['X-API-Key'] = process.env.ADYEN_API_KEY; } break; } } else if (service === 'email') { switch (provider) { case 'sendgrid': if (process.env.SENDGRID_API_KEY) { headers['Authorization'] = `Bearer ${process.env.SENDGRID_API_KEY}`; } break; case 'resend': if (process.env.RESEND_API_KEY) { headers['Authorization'] = `Bearer ${process.env.RESEND_API_KEY}`; } break; case 'mailgun': if (process.env.MAILGUN_API_KEY) { headers['Authorization'] = `Basic ${Buffer.from(`api:${process.env.MAILGUN_API_KEY}`).toString('base64')}`; } break; case 'postmark': if (process.env.POSTMARK_API_KEY) { headers['X-Postmark-Server-Token'] = process.env.POSTMARK_API_KEY; } break; } } return headers; } // Load the full failover config from file async function loadFullConfig() { const { promises: fs } = await Promise.resolve().then(() => __importStar(require('fs'))); const { join } = await Promise.resolve().then(() => __importStar(require('path'))); const configPaths = [ join(process.cwd(), 'failover.config.json'), join(process.cwd(), 'apps/api/failover.config.json'), join(process.cwd(), 'apps/web/failover.config.json'), ]; for (const configPath of configPaths) { try { const configExists = await fs .access(configPath) .then(() => true) .catch(() => false); if (!configExists) continue; const configContent = await fs.readFile(configPath, 'utf-8'); return JSON.parse(configContent); } catch (error) { continue; } } return null; } // Cloud-first fallback function with real failover logic async function cloudFallback(request) { const startTime = Date.now(); const failedProviders = []; let attemptCount = 0; // Load full config to get provider order and URLs const fullConfig = await loadFullConfig(); if (!fullConfig?.services?.[request.service]) { throw new Error(`No config found for service: ${request.service}`); } const serviceConfig = fullConfig.services[request.service]; const providerOrder = serviceConfig.order || []; const providers = serviceConfig.providers || []; console.log(`[Failover] Attempting ${request.service} request with provider order: ${providerOrder.join(' -> ')}`); for (const providerName of providerOrder) { attemptCount++; const provider = providers.find((p) => p.name === providerName && p.enabled); if (!provider) { console.log(`[Failover] Provider ${providerName} not found or disabled, skipping`); continue; } console.log(`[Failover] Attempting ${providerName}...`); try { // Construct the correct URL for this provider let providerUrl = request.url; // Replace the base URL with the provider's base URL if (request.service === 'payments') { if (providerName === 'stripe') { providerUrl = request.url.replace(/https:\/\/[^/]+/, 'https://api.stripe.com'); } else if (providerName === 'square') { // Use sandbox for demo const baseUrl = provider.base_url.includes('sandbox') ? 'https://connect.squareupsandbox.com' : 'https://connect.squareupsandbox.com'; // Force sandbox for demo providerUrl = request.url.replace(/https:\/\/[^/]+/, baseUrl); } } // Inject authentication headers for this specific provider const authHeaders = getAuthHeaders(providerName, request.service); const response = await fetch(providerUrl, { method: request.method, headers: { ...request.headers, ...authHeaders, 'X-Failover-Provider': providerName, 'X-Failover-Service': request.service, 'X-Failover-Operation': request.operation, }, body: request.body, }); const responseBody = await response.text(); // Check if response indicates success (2xx status) if (response.status >= 200 && response.status < 300) { const responseHeaders = {}; response.headers.forEach((value, key) => { responseHeaders[key] = value; }); console.log(`[Failover] Success with ${providerName} after ${attemptCount} attempts`); return { status: response.status, headers: responseHeaders, body: responseBody, provider_used: providerName, attempt_count: attemptCount, total_duration_ms: Date.now() - startTime, debug_info: { failover_triggered: attemptCount > 1, failed_providers: failedProviders, retry_attempts: attemptCount - 1, }, }; } else { // HTTP error status - try next provider failedProviders.push(providerName); console.log(`[Failover] ${providerName} failed with status ${response.status}: ${responseBody}`); continue; } } catch (error) { failedProviders.push(providerName); console.log(`[Failover] ${providerName} failed with error: ${error.message}`); continue; } } // All providers failed console.log(`[Failover] All providers failed: ${failedProviders.join(', ')}`); return { status: 500, headers: {}, body: JSON.stringify({ error: `All providers failed. ${failedProviders.map(p => `${p}: Failed`).join(', ')}`, }), provider_used: failedProviders[0] || 'unknown', attempt_count: attemptCount, total_duration_ms: Date.now() - startTime, debug_info: { failover_triggered: true, failed_providers: failedProviders, retry_attempts: attemptCount - 1, }, }; } // Factory functions for provider helpers function payments() { return { stripe: createStripeHelper(), square: createSquareHelper(), adyen: createAdyenHelper(), paypal: createPayPalHelper(), }; } function email() { return { sendgrid: createSendGridHelper(), mailgun: createMailgunHelper(), postmark: createPostmarkHelper(), amazonses: createAmazonSESHelper(), resend: createResendHelper(), }; } function custom(serviceName) { return { async request(operation, method, url, headers, body) { const request = { service: serviceName, operation, method, url, headers, body: body ? JSON.stringify(body) : undefined, }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Request failed with status ${response.status}: ${response.body}`); } return response.body ? JSON.parse(response.body) : {}; }, }; } // Provider helper implementations (simplified - delegate to cloud fallback) function createStripeHelper() { return { charges: { async create(params) { const body = new URLSearchParams({ amount: params.amount.toString(), currency: params.currency, source: params.source, ...(params.description && { description: params.description }), }).toString(); const request = { service: 'payments', operation: 'charge_create', method: 'POST', url: 'https://api.stripe.com/v1/charges', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body, }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Stripe charge creation failed: ${response.body}`); } return response.body ? JSON.parse(response.body) : {}; }, async retrieve(id) { const request = { service: 'payments', operation: 'charge_retrieve', method: 'GET', url: `https://api.stripe.com/v1/charges/${id}`, headers: {}, }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Stripe charge retrieval failed: ${response.body}`); } return JSON.parse(response.body); }, }, refunds: { async create(params) { const body = new URLSearchParams({ charge: params.charge, ...(params.amount && { amount: params.amount.toString() }), ...(params.reason && { reason: params.reason }), }).toString(); const request = { service: 'payments', operation: 'refund_create', method: 'POST', url: 'https://api.stripe.com/v1/refunds', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body, }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Stripe refund creation failed: ${response.body}`); } return JSON.parse(response.body); }, }, paymentIntents: { async create(params) { const body = new URLSearchParams({ amount: params.amount.toString(), currency: params.currency, ...(params.payment_method && { payment_method: params.payment_method }), ...(params.confirm && { confirm: 'true' }), }).toString(); const request = { service: 'payments', operation: 'payment_intent_create', method: 'POST', url: 'https://api.stripe.com/v1/payment_intents', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body, }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Stripe payment intent creation failed: ${response.body}`); } return JSON.parse(response.body); }, async confirm(id, params) { const body = params ? new URLSearchParams({ ...(params.payment_method && { payment_method: params.payment_method }), }).toString() : ''; const request = { service: 'payments', operation: 'payment_intent_confirm', method: 'POST', url: `https://api.stripe.com/v1/payment_intents/${id}/confirm`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body, }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Stripe payment intent confirmation failed: ${response.body}`); } return JSON.parse(response.body); }, }, }; } function createSquareHelper() { return { payments: { async create(params) { const request = { service: 'payments', operation: 'payment_create', method: 'POST', url: 'https://connect.squareup.com/v2/payments', headers: { 'Content-Type': 'application/json', 'Square-Version': '2025-06-18', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Square payment creation failed: ${response.body}`); } const result = JSON.parse(response.body); return result.payment; }, }, refunds: { async create(params) { const request = { service: 'payments', operation: 'refund_create', method: 'POST', url: 'https://connect.squareup.com/v2/refunds', headers: { 'Content-Type': 'application/json', 'Square-Version': '2025-06-18', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Square refund creation failed: ${response.body}`); } const result = JSON.parse(response.body); return result.refund; }, }, }; } function createAdyenHelper() { return { payments: { async create(params) { const request = { service: 'payments', operation: 'payment_create', method: 'POST', url: 'https://checkout-test.adyen.com/v71/payments', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Adyen payment creation failed: ${response.body}`); } return JSON.parse(response.body); }, }, refunds: { async create(paymentId, params) { const request = { service: 'payments', operation: 'refund_create', method: 'POST', url: `https://checkout-test.adyen.com/v71/payments/${paymentId}/refunds`, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Adyen refund creation failed: ${response.body}`); } return JSON.parse(response.body); }, }, }; } function createPayPalHelper() { return { orders: { async create(params) { const request = { service: 'payments', operation: 'order_create', method: 'POST', url: 'https://api-m.paypal.com/v2/checkout/orders', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`PayPal order creation failed: ${response.body}`); } return JSON.parse(response.body); }, async capture(id) { const request = { service: 'payments', operation: 'order_capture', method: 'POST', url: `https://api-m.paypal.com/v2/checkout/orders/${id}/capture`, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({}), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`PayPal order capture failed: ${response.body}`); } return JSON.parse(response.body); }, }, payments: { async refund(captureId, params) { const request = { service: 'payments', operation: 'refund_create', method: 'POST', url: `https://api-m.paypal.com/v2/payments/captures/${captureId}/refund`, headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`PayPal refund creation failed: ${response.body}`); } return JSON.parse(response.body); }, }, }; } // Email provider implementations function createSendGridHelper() { return { async send(params) { const request = { service: 'email', operation: 'send', method: 'POST', url: 'https://api.sendgrid.com/v3/mail/send', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`SendGrid email sending failed: ${response.body}`); } // SendGrid returns 202 with X-Message-Id header const messageId = response.headers['x-message-id'] || response.headers['X-Message-Id']; return { message_id: messageId }; }, }; } function createMailgunHelper() { return { async send(domain, params) { const formData = new URLSearchParams(); formData.append('from', params.from); if (Array.isArray(params.to)) { params.to.forEach(to => formData.append('to', to)); } else { formData.append('to', params.to); } if (params.cc) { if (Array.isArray(params.cc)) { params.cc.forEach(cc => formData.append('cc', cc)); } else { formData.append('cc', params.cc); } } if (params.bcc) { if (Array.isArray(params.bcc)) { params.bcc.forEach(bcc => formData.append('bcc', bcc)); } else { formData.append('bcc', params.bcc); } } formData.append('subject', params.subject); if (params.text) { formData.append('text', params.text); } if (params.html) { formData.append('html', params.html); } const request = { service: 'email', operation: 'send', method: 'POST', url: `https://api.mailgun.net/v3/${domain}/messages`, headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: formData.toString(), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Mailgun email sending failed: ${response.body}`); } return JSON.parse(response.body); }, }; } function createPostmarkHelper() { return { async send(params) { const request = { service: 'email', operation: 'send', method: 'POST', url: 'https://api.postmarkapp.com/email', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Postmark email sending failed: ${response.body}`); } return JSON.parse(response.body); }, }; } function createAmazonSESHelper() { return { async send(params) { const request = { service: 'email', operation: 'send', method: 'POST', url: 'https://email.us-east-1.amazonaws.com/', headers: { 'Content-Type': 'application/x-amz-json-1.0', 'X-Amz-Target': 'AWSCognitoIdentityProviderService.SendEmail', }, body: JSON.stringify(params), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Amazon SES email sending failed: ${response.body}`); } return JSON.parse(response.body); }, }; } function createResendHelper() { return { async send(params) { // Normalize array fields to arrays const normalizedParams = { ...params, to: Array.isArray(params.to) ? params.to : [params.to], ...(params.cc && { cc: Array.isArray(params.cc) ? params.cc : [params.cc] }), ...(params.bcc && { bcc: Array.isArray(params.bcc) ? params.bcc : [params.bcc] }), ...(params.reply_to && { reply_to: Array.isArray(params.reply_to) ? params.reply_to : [params.reply_to], }), }; const request = { service: 'email', operation: 'send', method: 'POST', url: 'https://api.resend.com/emails', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(normalizedParams), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Resend email sending failed: ${response.body}`); } return JSON.parse(response.body); }, async batch(params) { // Normalize each email in the batch const normalizedEmails = params.emails.map(email => ({ ...email, to: Array.isArray(email.to) ? email.to : [email.to], ...(email.cc && { cc: Array.isArray(email.cc) ? email.cc : [email.cc] }), ...(email.bcc && { bcc: Array.isArray(email.bcc) ? email.bcc : [email.bcc] }), ...(email.reply_to && { reply_to: Array.isArray(email.reply_to) ? email.reply_to : [email.reply_to], }), })); const request = { service: 'email', operation: 'batch_send', method: 'POST', url: 'https://api.resend.com/emails/batch', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(normalizedEmails), }; const response = await cloudFallback(request); if (response.status >= 400) { throw new Error(`Resend batch email sending failed: ${response.body}`); } return JSON.parse(response.body); }, }; } //# sourceMappingURL=providers.js.map