UNPKG

amazon-mcp-server

Version:

Model Context Protocol server for Amazon Seller API

223 lines (197 loc) 6.8 kB
import axios from 'axios'; /** * Manages authentication with the Amazon Seller API * Handles token refresh and provides authenticated request methods */ export class AmazonAuthManager { private clientId: string; private clientSecret: string; private refreshToken: string; private accessToken: string | null = null; private tokenExpiration: Date | null = null; private tokenRefreshPromise: Promise<string> | null = null; private refreshIntervalId: NodeJS.Timeout | null = null; /** * Creates a new instance of the Amazon Auth Manager * @param clientId Amazon client ID * @param clientSecret Amazon client secret * @param refreshToken Amazon refresh token */ constructor(clientId: string, clientSecret: string, refreshToken: string) { this.clientId = clientId; this.clientSecret = clientSecret; this.refreshToken = refreshToken; // Start the token refresh cycle this.startTokenRefreshCycle(); } /** * Gets a valid access token, refreshing if necessary * @returns Promise resolving to a valid access token */ public async getAccessToken(): Promise<string> { // If we already have a refresh promise in progress, return that if (this.tokenRefreshPromise) { return this.tokenRefreshPromise; } // If the token exists and isn't expired (with a 5-minute margin) if (this.accessToken && this.tokenExpiration && this.tokenExpiration.getTime() > Date.now() + 300000) { return this.accessToken; } // Otherwise, refresh the token this.tokenRefreshPromise = this.refreshAccessToken(); try { return await this.tokenRefreshPromise; } finally { // Reset the promise once completed (success or failure) this.tokenRefreshPromise = null; } } /** * Makes an authorized request to the Amazon Seller API * @param method HTTP method * @param endpoint API endpoint * @param params Query parameters * @param data Request body * @returns Promise resolving to the API response */ public async makeAuthorizedRequest( method: string, endpoint: string, params?: any, data?: any ): Promise<any> { try { const accessToken = await this.getAccessToken(); const response = await axios({ method, url: endpoint, params, data, headers: { 'x-amz-access-token': accessToken, 'Content-Type': 'application/json' } }); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 401) { // Invalid token, force a refresh and retry once this.tokenRefreshPromise = null; this.accessToken = null; const newAccessToken = await this.getAccessToken(); return axios({ method, url: endpoint, params, data, headers: { 'x-amz-access-token': newAccessToken, 'Content-Type': 'application/json' } }).then(response => response.data); } throw error; } } /** * Starts the automatic token refresh cycle * Refreshes immediately and then every 55 minutes */ private startTokenRefreshCycle(): void { // Refresh immediately on startup this.getAccessToken().catch(err => { console.error('Initial token refresh failed:', err); }); // Schedule a refresh every 55 minutes (3300000 ms) // This ensures we always have a valid token this.refreshIntervalId = setInterval(() => { this.tokenRefreshPromise = null; // Force a new refresh this.getAccessToken().catch(err => { console.error('Scheduled token refresh failed:', err); }); }, 3300000); } /** * Refreshes the access token using the refresh token * @returns Promise resolving to the new access token */ private async refreshAccessToken(): Promise<string> { try { const response = await axios.post('https://api.amazon.com/auth/o2/token', { grant_type: 'refresh_token', refresh_token: this.refreshToken, client_id: this.clientId, client_secret: this.clientSecret }); this.accessToken = response.data.access_token; // Amazon tokens expire after 1 hour, we schedule renewal after 55 minutes const expiresInMs = (response.data.expires_in || 3600) * 1000; this.tokenExpiration = new Date(Date.now() + expiresInMs); if (!this.accessToken) { throw new Error('No access token received from Amazon API'); } return this.accessToken; } catch (error) { console.error('Failed to refresh Amazon access token:', error); throw new Error('Authentication failed with Amazon API'); } } /** * Handles authentication errors with appropriate recovery strategies * @param error The error to handle */ private async handleAuthError(error: any): Promise<void> { if (axios.isAxiosError(error)) { if (error.response?.status === 401) { // Expired token, force a refresh this.tokenRefreshPromise = null; this.accessToken = null; await this.getAccessToken(); } else if (error.response?.status === 403) { // Permission issue, log error but don't retry console.error('Permission error with Amazon API:', error.response.data); throw new Error('Insufficient permissions for Amazon API'); } else if (!error.response) { // Network error, implement exponential backoff retry await this.retryWithExponentialBackoff(() => this.refreshAccessToken()); } } } /** * Retries a function with exponential backoff * @param fn Function to retry * @param maxRetries Maximum number of retries * @param initialDelay Initial delay in milliseconds * @returns Promise resolving to the function result */ private async retryWithExponentialBackoff( fn: () => Promise<any>, maxRetries = 5, initialDelay = 1000 ): Promise<any> { let retries = 0; let delay = initialDelay; while (retries < maxRetries) { try { return await fn(); } catch (error) { retries++; if (retries >= maxRetries) throw error; // Wait with exponential backoff await new Promise(resolve => setTimeout(resolve, delay)); delay *= 2; // Double the delay each retry } } } /** * Stops the token refresh cycle * Call this when shutting down the server */ public stopTokenRefreshCycle(): void { if (this.refreshIntervalId) { clearInterval(this.refreshIntervalId); this.refreshIntervalId = null; } } }