UNPKG

amazon-seller-mcp

Version:

Model Context Protocol (MCP) client for Amazon Selling Partner API

243 lines 10.2 kB
/** * Amazon Selling Partner API Authentication Module */ import axios from 'axios'; import crypto from 'crypto'; import { URL } from 'url'; import { AuthError, AuthErrorType, REGION_ENDPOINTS, } from '../types/auth.js'; /** * Default token cache time (30 minutes) */ const DEFAULT_TOKEN_CACHE_TIME_MS = 30 * 60 * 1000; /** * Amazon Selling Partner API Authentication * * Handles OAuth 2.0 authentication flow, token management, and AWS Signature V4 signing */ export class AmazonAuth { credentials; region; marketplaceId; tokenCacheTimeMs; tokens = null; /** * Create a new AmazonAuth instance * * @param config Authentication configuration */ constructor(config) { this.credentials = config.credentials; this.region = config.region; this.marketplaceId = config.marketplaceId; this.tokenCacheTimeMs = config.tokenCacheTimeMs || DEFAULT_TOKEN_CACHE_TIME_MS; // Validate required credentials this.validateCredentials(); } /** * Validate that all required credentials are provided */ validateCredentials() { const { clientId, clientSecret, refreshToken } = this.credentials; if (!clientId || !clientSecret || !refreshToken) { throw new AuthError('Missing required credentials: clientId, clientSecret, and refreshToken are required', AuthErrorType.INVALID_CREDENTIALS); } // If IAM credentials are provided, validate them if (this.credentials.accessKeyId || this.credentials.secretAccessKey) { if (!this.credentials.accessKeyId || !this.credentials.secretAccessKey) { throw new AuthError('Both accessKeyId and secretAccessKey must be provided if using IAM authentication', AuthErrorType.INVALID_CREDENTIALS); } } } /** * Get the current access token, refreshing if necessary * * @returns Promise resolving to the access token */ async getAccessToken() { // If we have a valid token, return it if (this.tokens && this.tokens.expiresAt > Date.now()) { return this.tokens.accessToken; } // Otherwise, refresh the token const tokens = await this.refreshAccessToken(); return tokens.accessToken; } /** * Refresh the access token using the refresh token * * @returns Promise resolving to the new auth tokens */ async refreshAccessToken() { try { const response = await axios({ method: 'post', url: 'https://api.amazon.com/auth/o2/token', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, data: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: this.credentials.refreshToken, client_id: this.credentials.clientId, client_secret: this.credentials.clientSecret, }).toString(), }); // Calculate token expiration time (subtract 5 minutes for safety margin) const expiresAt = Date.now() + response.data.expires_in * 1000 - 5 * 60 * 1000; // Store the new tokens this.tokens = { accessToken: response.data.access_token, expiresAt, }; return this.tokens; } catch (error) { const axiosError = error; const errorMessage = axiosError.response?.data ? JSON.stringify(axiosError.response.data) : axiosError.message; throw new AuthError(`Failed to refresh access token: ${errorMessage}`, AuthErrorType.TOKEN_REFRESH_FAILED, error instanceof Error ? error : undefined); } } /** * Sign a request with AWS Signature V4 * * @param request Request to sign * @returns Promise resolving to the signed request */ async signRequest(request) { // Ensure we have IAM credentials if (!this.credentials.accessKeyId || !this.credentials.secretAccessKey) { throw new AuthError('AWS IAM credentials (accessKeyId and secretAccessKey) are required for request signing', AuthErrorType.REQUEST_SIGNING_FAILED); } try { // Get the current region endpoint const regionEndpoint = REGION_ENDPOINTS[this.region]; // Parse the URL const url = new URL(request.url); const host = url.hostname; const path = url.pathname + url.search; // Get the current date and time const now = new Date(); const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, ''); const dateStamp = amzDate.substring(0, 8); // Create canonical request const method = request.method.toUpperCase(); const canonicalUri = path; const canonicalQueryString = url.search.substring(1); // Remove leading '?' // Add required headers const headers = { ...request.headers, host, 'x-amz-date': amzDate, }; // If we have a role ARN, add the security token header if (this.credentials.roleArn) { // Note: In a real implementation, we would need to assume the role and get a session token // For simplicity, we're just showing where the token would be added headers['x-amz-security-token'] = 'SESSION_TOKEN'; } // Create canonical headers const canonicalHeaders = Object.keys(headers) .sort() .map((key) => `${key.toLowerCase()}:${headers[key].trim()}`) .join('\n') + '\n'; const signedHeaders = Object.keys(headers) .sort() .map((key) => key.toLowerCase()) .join(';'); // Create payload hash const payload = request.data ? JSON.stringify(request.data) : ''; const payloadHash = crypto.createHash('sha256').update(payload).digest('hex'); // Create canonical request const canonicalRequest = [ method, canonicalUri, canonicalQueryString, canonicalHeaders, signedHeaders, payloadHash, ].join('\n'); // Create string to sign const algorithm = 'AWS4-HMAC-SHA256'; const scope = `${dateStamp}/${regionEndpoint.region}/execute-api/aws4_request`; const stringToSign = [ algorithm, amzDate, scope, crypto.createHash('sha256').update(canonicalRequest).digest('hex'), ].join('\n'); // Calculate signature const signingKey = this.getSignatureKey(this.credentials.secretAccessKey, dateStamp, regionEndpoint.region, 'execute-api'); const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex'); // Create authorization header const authorizationHeader = [ `${algorithm} Credential=${this.credentials.accessKeyId}/${scope}`, `SignedHeaders=${signedHeaders}`, `Signature=${signature}`, ].join(', '); // Add authorization header to request headers.Authorization = authorizationHeader; // Add content-type if not present and we have a payload if (payload && !headers['Content-Type']) { headers['Content-Type'] = 'application/json'; } // Return signed request return { ...request, headers, }; } catch (error) { throw new AuthError(`Failed to sign request: ${error instanceof Error ? error.message : String(error)}`, AuthErrorType.REQUEST_SIGNING_FAILED, error instanceof Error ? error : undefined); } } /** * Generate a signed request for the Amazon Selling Partner API * * @param request Request to sign * @returns Promise resolving to the signed request with access token */ async generateSecuredRequest(request) { try { // Get access token const accessToken = await this.getAccessToken(); // Add access token to headers const requestWithToken = { ...request, headers: { ...request.headers, Authorization: `Bearer ${accessToken}`, }, }; // Sign the request if we have IAM credentials if (this.credentials.accessKeyId && this.credentials.secretAccessKey) { return this.signRequest(requestWithToken); } return requestWithToken; } catch (error) { if (error instanceof AuthError) { throw error; } throw new AuthError(`Failed to generate secured request: ${error instanceof Error ? error.message : String(error)}`, AuthErrorType.UNKNOWN_ERROR, error instanceof Error ? error : undefined); } } /** * Get AWS Signature V4 signing key * * @param key Secret access key * @param dateStamp Date in YYYYMMDD format * @param regionName AWS region name * @param serviceName AWS service name * @returns Signing key */ getSignatureKey(key, dateStamp, regionName, serviceName) { const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest(); const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest(); const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest(); const kSigning = crypto.createHmac('sha256', kService).update('aws4_request').digest(); return kSigning; } } //# sourceMappingURL=amazon-auth.js.map