failover-sdk
Version:
One-line API failover with zero downtime. Native Rust performance with TypeScript interface.
700 lines • 27.8 kB
JavaScript
;
// 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