UNPKG

amazon-sp-api-timeout

Version:

Amazon Selling Partner API client

745 lines (705 loc) 30.7 kB
const CustomError = require('./CustomError'); const request = require('./request'); const Signer = require('./Signer'); const {XMLParser} = require('fast-xml-parser/src/fxp'); const Credentials = require('./Credentials'); const endpoints = require('./endpoints'); const operations = require('./operations')(endpoints); const utils = require('./utils'); const crypto = require('crypto'); const csv = require('csvtojson'); const fs = require('fs'); const zlib = require('zlib'); const iconv = require('iconv-lite'); const client_version = require('../package.json').version; const node_version = process.version; const os = require('os'); // Provide credentials as environment variables OR create a path and file ~/.amzspapi/credentials (located in your user folder) // If you don't provide access_token or role_credentials, the first call to an API endpoint will request them with a TTL of 1 hour // Tokens are reused for the class instance // Retrieve the tokens via getters if you want to use them across multiple instances of the SellingPartner class class SellingPartner { // config object params: // region:'<REGION>', // Required: The region to use for the SP-API endpoints. Must be one of: "eu", "na" or "fe" // refresh_token:'<REFRESH_TOKEN>', // Optional: The refresh token of your app user. Required if "only_grantless_operations" option is set to "false". // access_token:'<ACCESS_TOKEN>', // Optional: The temporary access token requested with the refresh token of the app user. // role_credentials:{ // Optional: The temporary role credentials for the sellingpartner api role of the iam user. // id:'<TEMPORARY_ROLE_ACCESS_ID>', // secret:'<TEMPORARY_ROLE_ACCESS_SECRET>', // security_token:'<TEMPORARY_ROLE_SECURITY_TOKEN>' // }, // endpoints_versions:{ // Optional: Defines the version to use for an endpoint as key/value pairs, i.e. "reports":"2021-06-30". // ... // }, // credentials:{ // Optional: The app client and aws user credentials. Should only be used if you have no means of using environment vars or credentials file! // SELLING_PARTNER_APP_CLIENT_ID:'<APP_CLIENT_ID>', // SELLING_PARTNER_APP_CLIENT_SECRET:'<APP_CLIENT_SECRET>', // AWS_ACCESS_KEY_ID:'<AWS_USER_ID>', // AWS_SECRET_ACCESS_KEY:'<AWS_USER_SECRET>', // AWS_SELLING_PARTNER_ROLE:'<AWS_SELLING_PARTNER_API_ROLE>' // }, // options:{ // credentials_path:'~/.amzspapi/credentials', // Optional: A custom absolute path to your credentials file location. // auto_request_tokens:true, // Optional: Whether or not the client should retrieve new access and role credentials if non given or expired. // auto_request_throttled:true, // Optional: Whether or not the client should automatically retry a request when throttled. // version_fallback:true, // Optional: Whether or not the client should try to use an older version of an endpoint if the operation is not defined for the desired version. // use_sandbox:false, // Optional: Whether or not to use the sandbox endpoint. // only_grantless_operations:false // Optional: Whether or not to only use grantless operations. // user_agent:'amazon-sp-api/<CLIENT_VERSION> (Language=Node.js/<NODE_VERSION>; Platform=<OS_PLATFORM>/<OS_RELEASE>)', // A custom user-agent header. // debug_log:false // Optional: Whether or not the client should print console logs for debugging purposes. // } // TODO: We could add something like a debug option, that will enable included info logs, i.e. sandbox activated, credentials load type (env, file, config), etc. constructor(config){ this._region = config.region; this._refresh_token = config.refresh_token; this._access_token = config.access_token; this._role_credentials = config.role_credentials; // Will hold access tokens for grantless operations (with scope as key) this._grantless_tokens = {}; this._options = Object.assign({ auto_request_tokens:true, auto_request_throttled:true, use_sandbox:false, only_grantless_operations:false, version_fallback:true, user_agent:'amazon-sp-api/' + client_version + ' (Language=Node.js/' + node_version + '; Platform=' + os.type() + '/' + os.release() + ')', debug_log:false }, config.options); this._endpoints_versions = this._validateEndpointsVersions(Object.assign({}, config.endpoints_versions)); this._credentials = new Credentials(config.credentials, this._options.credentials_path, this._options.debug_log).load(); this._xml_parser = new XMLParser(); if (!this._region || !/^(eu|na|fe)$/.test(this._region)){ throw new CustomError({ code:'NO_VALID_REGION_PROVIDED', message:'Please provide one of: "eu", "na" or "fe"' }); } if (!this._refresh_token && !this._options.only_grantless_operations){ throw new CustomError({ code:'NO_REFRESH_TOKEN_PROVIDED', message:'Please provide a refresh token or set "only_grantless_operations" option to true' }); } } get access_token(){ return this._access_token; } get role_credentials(){ return this._role_credentials; } get endpoints(){ return endpoints; } // Make sure that all defined endpoints and its defined versions exist _validateEndpointsVersions(endpoints_versions){ let invalid_endpoints = Object.keys(endpoints_versions).filter((endpoint) => { return !endpoints[endpoint]; }); if (invalid_endpoints.length){ throw new CustomError({ code:'VERSION_DEFINED_FOR_INVALID_ENDPOINTS', message:'One or more endpoints are not valid. These endpoints don\'t exist: "' + invalid_endpoints.join('","') + '"' }); } let invalid_endpoints_versions = Object.keys(endpoints_versions).filter((endpoint) => { return !endpoints[endpoint].__versions.includes(endpoints_versions[endpoint]); }); if (invalid_endpoints_versions.length){ throw new CustomError({ code:'INVALID_VERSION_FOR_ENDPOINTS', message:'The provided version for the following endpoint(s) is not valid: "' + invalid_endpoints_versions.join('","') + '"' }); } return endpoints_versions; } async _wait(restore_rate){ return new Promise((resolve, reject) => { setTimeout(resolve, restore_rate * 1000); }); } async _unzip(buffer){ return new Promise((resolve, reject) => { zlib.gunzip(buffer, (err, unzipped_buffer) => { if (err){ reject(err); } resolve(unzipped_buffer); }); }); } async _saveFile(content, options){ return new Promise((resolve, reject) => { if (options.json){ content = JSON.stringify(content); } fs.writeFile(options.file, content, (err) => { err ? reject(err) : resolve(); }) }); } async _readFile(file, content_type){ return new Promise((resolve, reject) => { let regexp_charset = /charset=([^;]*)/; let content_match = content_type.match(regexp_charset); let encoding = content_match && content_match[1] ? content_match[1] : 'utf-8'; // fs.readFile doesn't accept ISO-8859-1 as encoding value --> use latin1 as value which is the same if (encoding.toUpperCase() === 'ISO-8859-1'){ encoding = 'latin1'; } fs.readFile(file, encoding, (err, content) => { err ? reject(err) : resolve(content); }) }); } _validateEncryptionDetails(details){ if (!details || !details.url){ throw new CustomError({ code:'DOWNLOAD_INFORMATION_MISSING', message:'Please provide url' }); } let encryption = details.encryptionDetails; // Docs state that no other encryption standards should be possible, but check if its correct anyway if (encryption && encryption.standard !== 'AES'){ throw new CustomError({ code:'UNKNOWN_ENCRYPTION_STANDARD', message:'Cannot decrypt ' + details.encryptionDetails.standard + ', expecting AES' }); } let compression = details.compressionAlgorithm; // Docs state that no other zip standards should be possible, but check if its correct anyway if (compression && compression !== 'GZIP'){ throw new CustomError({ code:'UNKNOWN_ZIP_STANDARD', message:'Cannot unzip ' + compression + ', expecting GZIP' }); } } _validateUpOrDownloadSuccess(res, request_type){ if (res.statusCode !== 200){ let json_res; try { json_res = this._xml_parser.parse(res.body); } catch(e){ throw new CustomError({ code:request_type + '_ERROR', message:res.body }); } if (json_res && json_res.Error){ throw new CustomError({ code:json_res.Error.Code, message:json_res.Error.Message }); } else { throw new CustomError({ code:request_type + '_ERROR', message:json_res }); } } } _decryptBuffer(details, chunks){ if (details.encryptionDetails){ let encrypted_buffer = Buffer.concat(chunks); let decipher = crypto.createDecipheriv( 'aes-256-cbc', Buffer.from(details.encryptionDetails.key, 'base64'), Buffer.from(details.encryptionDetails.initializationVector, 'base64') ); return Buffer.concat([decipher.update(encrypted_buffer), decipher.final()]); } return Buffer.concat(chunks); } // Decode buffer with given charset _decodeBuffer(decrypted_buffer, headers, charset){ // Try to extract charset from header if no charset explicitly defined if (!charset && headers && headers['content-type']){ let charset_match = headers['content-type'].match(/\.*charset=([^;]*)/); if (charset_match && charset_match[1]){ charset = charset_match[1]; } } // Use utf8 as default charset if no charset given by options or in headers if (!charset){ charset = 'utf8'; } try { return iconv.decode(decrypted_buffer, charset); } catch(e){ throw new CustomError({ code:'DECODE_ERROR', message:e.message }); } } _constructRefreshAccessTokenBody(scope){ let body = { client_id:this._credentials.app_client.id, client_secret:this._credentials.app_client.secret }; let valid_scopes = ['sellingpartnerapi::notifications', 'sellingpartnerapi::migration']; if (scope){ // Make sure that scope is valid if (!valid_scopes.includes(scope)){ throw new CustomError({ code:'INVALID_SCOPE_ERROR', message:'Scope for requesting token for grantless operations is invalid. Please provide one of: ' + valid_scopes.join(',') }); } body.grant_type = 'client_credentials'; body.scope = scope; } else if (!this._options.only_grantless_operations){ body.grant_type = 'refresh_token'; body.refresh_token = this._refresh_token; } else { throw new CustomError({ code:'NO_SCOPE_PROVIDED', message:'Grantless tokens require a scope. Please provide one of: ' + valid_scopes.join(',') }); } return JSON.stringify(body); } _tokenExists(scope){ return (this._access_token && !scope) || (scope && this._grantless_tokens[scope]); } async _validateAccessTokenAndRoleCredentials(scope){ if (this._options.auto_request_tokens){ if (!this._tokenExists(scope)){ await this.refreshAccessToken(scope); } if (!this._role_credentials){ await this.refreshRoleCredentials(); } } if (!this._tokenExists(scope) || !this._role_credentials){ throw new CustomError({ code:'NO_ACCESS_TOKEN_AND_OR_ROLE_CREDENTIALS_PRESENT', message:'Did you turn off "auto_request_tokens" and forgot to refresh the access token and/or role credentials and/or the scope for a grantless token?' }); } } _validateMethod(method){ if (!method || !/^(GET|POST|PUT|DELETE|PATCH)$/.test(method.toUpperCase())){ throw new CustomError({ code:'NO_VALID_METHOD_PROVIDED', message:'Please provide a valid HTTP Method ("GET","POST","PUT","DELETE" or "PATCH") when using "api_path"' }); } return method.toUpperCase(); } _validateOperationAndEndpoint(operation, endpoint){ if (!operation){ throw new CustomError({ code:'NO_OPERATION_GIVEN', message:'Please provide an operation to call' }); } // Split operation in endpoint and operation if shorthand dot notation if (operation.includes('.')){ let op_split = operation.split('.'); endpoint = op_split[0]; operation = op_split[1]; } // Still possible to use operation without an endpoint, but deprecated and discouraged if (!endpoint){ utils.warn('OPERATION_ONLY'); endpoint = operations[operation]; } if (!operations[operation]){ throw new CustomError({ code:'OPERATION_NOT_FOUND', message:'No operation found: ' + operation }); } if (!endpoints[endpoint]){ throw new CustomError({ code:'ENDPOINT_NOT_FOUND', message:'No endpoint found: ' + endpoint }); } if (!endpoints[endpoint].__operations.includes(operation)){ throw new CustomError({ code:'INVALID_OPERATION_FOR_ENDPOINT', message:'The operation "' + operation + '" is not valid for endpoint "' + endpoint + '"' }); } return {operation, endpoint}; } _getFallbackVersion(operation, endpoint, version){ // Make sure to only look for the operation in older versions // --> we don't want to break stuff by accidently calling a newer version than expected! let version_index = endpoints[endpoint].__versions.indexOf(version); let fallback_version = endpoints[endpoint].__versions.slice(0, version_index).reverse().find((__version) => { return endpoints[endpoint][__version][operation]; }); // Throw error if version_fallback is disabled or no fallback version was found if (!this._options.version_fallback || !fallback_version){ throw new CustomError({ code:'OPERATION_NOT_FOUND_FOR_VERSION', message:'Operation "' + operation + '" not found for version "' + version + '"' }); } return fallback_version; } // Logic if version was explicitly set in .callAPI options _validateLocallySetVersion(operation, endpoint, version){ // Throw error if the explicitly specified version in .callAPI can't be found for the endpoint if (!endpoints[endpoint].__versions.includes(version)){ throw new CustomError({ code:'INVALID_VERSION', message:'Invalid version "' + version + '" for endpoint "' + endpoint + '" and operation "' + operation + '". Should be one of: "' + endpoints[endpoint].__versions.join('","') + '"' }); } // If operation is not supported for the version: // --> try to find an older version of the endpoint that supports the operation if (!endpoints[endpoint][version][operation]){ return this._getFallbackVersion(operation, endpoint, version); } return version; } // Logic if version was NOT explicitly set in .callAPI options _validateGloballySetVersion(operation, endpoint){ // If no version for the endpoint was set in constructor config: // --> find the oldest version that supports the operation if (!this._endpoints_versions[endpoint]){ // We can directly return the version as its impossible for a valid operation to have no version return endpoints[endpoint].__versions.find((__version) => { return endpoints[endpoint][__version][operation]; }); } // Get the version specified for the endpoint in constructor config let version = this._endpoints_versions[endpoint]; // If operation is not supported for the version: // --> try to find an older version of the endpoint that supports the operation if (!endpoints[endpoint][version][operation]){ return this._getFallbackVersion(operation, endpoint, version); } return version; } _validateAndGetVersion(operation, endpoint, version){ return version ? this._validateLocallySetVersion(operation, endpoint, version) : this._validateGloballySetVersion(operation, endpoint); } _validateOperationAllowance(scope){ if (this._options.only_grantless_operations && !scope){ throw new CustomError({ code:'INVALID_OPERATION_ERROR', message:'Operation is not grantless. Set "only_grantless_operations" to false and provide a "refresh_token" to be able to call the operation.' }); } } _constructExchangeBody(auth_code){ if (!auth_code){ throw new CustomError({ code:'NO_AUTH_CODE_PROVIDED', message:'Please provide an authorization code received from a "getAuthorizationCode" operation to exchange it for a "refresh_token".' }); } let body = { grant_type:'authorization_code', code:auth_code, client_id:this._credentials.app_client.id, client_secret:this._credentials.app_client.secret }; return JSON.stringify(body); } // Exchange an authorization code received from a getAuthorizationCode operation for a refresh token async exchange(auth_code){ let res = await request({ method:'POST', url:'https://api.amazon.com/auth/o2/token', body:this._constructExchangeBody(auth_code), headers:{ 'Content-Type':'application/json' } }); let json_res; try { json_res = JSON.parse(res.body); } catch (e){ throw new CustomError({ code:'EXCHANGE_AUTH_CODE_PARSE_ERROR', message:res.body }); } if (json_res.error){ throw new CustomError({ code:json_res.error, message:json_res.error_description }); } return json_res; } // If scope is provided a token for a grantless operation is requested // scope should be one of: ['sellingpartnerapi::notifications', 'sellingpartnerapi::migration'] async refreshAccessToken(scope){ let res = await request({ method:'POST', url:'https://api.amazon.com/auth/o2/token', body:this._constructRefreshAccessTokenBody(scope), headers:{ 'Content-Type':'application/json' } }); let json_res; try { json_res = JSON.parse(res.body); } catch (e){ throw new CustomError({ code:'REFRESH_ACCESS_TOKEN_PARSE_ERROR', message:res.body }); } if (json_res.access_token){ if (!scope){ this._access_token = json_res.access_token; } else { this._grantless_tokens[scope] = json_res.access_token; } } else if (json_res.error){ throw new CustomError({ code:json_res.error, message:json_res.error_description }); } else { throw new CustomError({ code:'UNKNOWN_REFRESH_ACCESS_TOKEN_ERROR', message:res.body }); } } async refreshRoleCredentials(){ let signed_request = new Signer(this._region, this._options.use_sandbox, this._options.user_agent).signRoleCredentialsRequest(this._credentials.aws_user); let res = await request(signed_request); let json_res; try { json_res = this._xml_parser.parse(res.body); } catch(e){ throw new CustomError({ code:'XML_PARSE_ERROR', message:res.body }); } if (json_res && json_res.AssumeRoleResponse && json_res.AssumeRoleResponse.AssumeRoleResult && json_res.AssumeRoleResponse.AssumeRoleResult.Credentials){ let role_credentials = json_res.AssumeRoleResponse.AssumeRoleResult.Credentials; this._role_credentials = { id:role_credentials.AccessKeyId, secret:role_credentials.SecretAccessKey, security_token:role_credentials.SessionToken }; } else if (json_res && json_res.ErrorResponse && json_res.ErrorResponse.Error){ throw new CustomError({ code:json_res.ErrorResponse.Error.Code, message:json_res.ErrorResponse.Error.Message }); } else { throw new CustomError({ code:'NO_ROLE_CREDENTIALS_RECEIVED', message:res.body }); } } // req_params object: // operation:'<OPERATION_TO_CALL>', // Optional: The operation you want to request. May also include endpoint as shorthand dot notation. Required if "api_path" is not defined. // endpoint:'<ENDPOINT_OF_OPERATION>', // Optional: The endpoint of the operation. Required if endpoint is not part of operation as shorthand dot notation and if "api_path" is not defined. // path:{ // Optional: The input paramaters added to the path of the operation. // ... // }, // query:{ // Optional: The input paramaters added to the query string of the operation. // ... // }, // body:{ // Optional: The input paramaters added to the body of the operation. // ... // }, // api_path:'<FULL_PATH_OF_OPERATION>', // Optional: The full path of an operation. Required if "operation" is not defined. // method:'GET' // The HTTP method to use. Required only if "api_path" is defined. Must be one of: "GET", "POST", "PUT", "DELETE" or "PATCH". // restricted_data_token:'<RESTRICTED_DATA_TOKEN>' // Optional: A token received from a "createRestrictedDataToken" operation for receiving PII from a restricted operation. // options:{ // version:'<OPERATION_ENDPOINT_VERSION>', // Optional: The endpoint’s version that should be used when calling the operation. Will be preferred over an "endpoints_versions" setting. // restore_rate:'<RESTORE_RATE_IN_SECONDS>', // Optional: The restore rate (in seconds) that should be used when calling the operation. Will be preferred over the default restore rate of the operation. // raw_result:false // Whether or not the client should return the "raw" result, which will include the raw body, buffer chunks, statuscode and headers of the result. // } async callAPI(req_params){ let options = Object.assign({}, req_params.options); if (req_params.api_path){ req_params.method = this._validateMethod(req_params.method); } else { let {operation, endpoint} = this._validateOperationAndEndpoint(req_params.operation, req_params.endpoint); let version = this._validateAndGetVersion(operation, endpoint, options.version); req_params = { ...endpoints[endpoint][version][operation](req_params) }; } // Use user-defined restore_rate if specified, otherwise use default for operation if (options.restore_rate && !isNaN(options.restore_rate)){ req_params.restore_rate = options.restore_rate; } // Scope will only be defined for grantless operations let scope = req_params.scope; this._validateOperationAllowance(scope); await this._validateAccessTokenAndRoleCredentials(scope); // Make sure to use the correct token for the request let token_for_request = this._access_token; if (scope){ token_for_request = this._grantless_tokens[scope]; } else if (req_params.restricted_data_token){ token_for_request = req_params.restricted_data_token; } let signed_request = new Signer(this._region, this._options.use_sandbox, this._options.user_agent).signAPIRequest(token_for_request, this._role_credentials, req_params); let res = await request(signed_request); if (options.raw_result){ return res; } if (res.statusCode === 204 && req_params.method === 'DELETE'){ return {success:true}; } let json_res; try { json_res = JSON.parse(res.body.replace(/\n/g, '')); } catch (e){ throw new CustomError({ code:'JSON_PARSE_ERROR', message:res.body }); } if (json_res.errors){ let error = json_res.errors[0]; // Refresh tokens when expired and auto_request_tokens is true if (res.statusCode === 403 && error.code === 'Unauthorized'){ if (this._options.auto_request_tokens){ if (/access token.*expired/.test(error.details)){ if (this._options.debug_log){ console.log('Access token expired, refreshing it now'); } await this.refreshAccessToken(scope); return await this.callAPI(req_params); } else if (/security token.*expired/.test(error.message)){ if (this._options.debug_log){ console.log('Role credentials security token expired, refreshing it now'); } await this.refreshRoleCredentials(); return await this.callAPI(req_params); } } // Retry when call is throttled and auto_request_throttled is true } else if (res.statusCode === 429 && error.code === 'QuotaExceeded' && this._options.auto_request_throttled){ // Wait the restore rate before retrying the call if dynamic or static restore rate is set if (res.headers['x-amzn-ratelimit-limit'] || req_params.restore_rate){ // Use dynamic restore rate from result header if given --> otherwise use defined default restore_rate of the operation let restore_rate = res.headers['x-amzn-ratelimit-limit'] ? (1 / (res.headers['x-amzn-ratelimit-limit'] * 1)) : req_params.restore_rate; if (this._options.debug_log){ console.log('Request throttled, retrying a call of "' + (req_params.operation || req_params.api_path) + '" in ' + restore_rate + ' seconds...'); } await this._wait(restore_rate); } return await this.callAPI(req_params); } else if (error.code === 'InternalFailure' && this._options.use_sandbox){ throw new CustomError({ code:'INVALID_SANDBOX_PARAMETERS', message:'You\'re in SANDBOX mode, make sure sandbox parameters are correct, as in Amazon SP API documentation: https://github.com/amzn/selling-partner-api-docs/blob/main/guides/developer-guide/SellingPartnerApiDeveloperGuide.md#how-to-make-a-sandbox-call-to-the-selling-partner-api' }); } throw new CustomError(error); } // If there is a pagination outside payload (like for getInventorySummaries), this will include it with the result if (json_res.pagination && json_res.payload){ return Object.assign(json_res.pagination, json_res.payload); } // Some calls do not return response in payload but directly (i.e. operation "getSmallAndLightEligibilityBySellerSKU")! return json_res.payload || json_res; } // Download a report or feed result // Options object: // json:false, // Optional: Whether or not the content should be transformed to json before returning it (from tab delimited flat-file or XML). // unzip:true, // Optional: Whether or not the content should be unzipped before returning it. // file:'<FILE_PATH>', // Optional: The absolute file path to save the report to. Even when saved to disk the report content is still returned. // charset:'utf8' // Optional: The charset to use for decoding the content. Is ignored when content is compressed and unzip is set to false. async download(details, options = {}){ options = Object.assign({ unzip:true }, options); this._validateEncryptionDetails(details); // Result will be a tab-delimited flat file or an xml document let res = await request({ url:details.url }); this._validateUpOrDownloadSuccess(res, 'DOWNLOAD'); let decrypted_buffer = this._decryptBuffer(details, res.chunks); // Decompress if content is compressed and unzip option is true if (details.compressionAlgorithm && options.unzip){ decrypted_buffer = await this._unzip(decrypted_buffer); } let decrypted = decrypted_buffer; if (!details.compressionAlgorithm || options.unzip){ decrypted = this._decodeBuffer(decrypted, res.headers, options.charset); if (options.json){ // Transform content to json --> take content type from which to transform to json from result header try { if (res.headers['content-type'].includes('xml')){ decrypted = this._xml_parser.parse(decrypted); } else if (res.headers['content-type'].includes('plain')){ decrypted = await csv({ delimiter:'\t', quote:'off' }).fromString(decrypted); } } catch(e){ throw new CustomError({ code:'PARSE_ERROR', message:'Could not parse result to JSON.', details:decrypted }); } } } if (options.file){ await this._saveFile(decrypted, options); } return decrypted; } // Upload a tab-delimited flat file or an xml document // Feed object: // content:'<CONTENT>', // Optional: The content to upload as a string. Required if "file" is not provided. // file:'<FILE_PATH>', // Optional: The absolute file path to the feed content document to upload. Required if "content" is not provided. // contentType:'<CONTENT_TYPE>' // Optional: The contentType of the content to upload. Should be one of "text/xml" or "text/tab-separated-values" and the charset of the content, i.e. "text/xml; charset=utf-8". async upload(details, feed){ this._validateEncryptionDetails(details); if (!feed || (!feed.content && !feed.file)){ throw new CustomError({ code:'NO_FEED_CONTENT_PROVIDED', message:'Please provide "content" (string) or "file" (absolute path) of feed.' }); } if (!feed.contentType){ throw new CustomError({ code:'NO_FEED_CONTENT_TYPE_PROVIDED', message:'Please provide "contentType" of feed (should be identical to the contentType used in "createFeedDocument" operation).' }); } let feed_content = feed.content || await this._readFile(feed.file, feed.contentType); let content_buffer; if (details.encryptionDetails) { // Encrypt content to upload let cipher = crypto.createCipheriv( 'aes-256-cbc', Buffer.from(details.encryptionDetails.key, 'base64'), Buffer.from(details.encryptionDetails.initializationVector, 'base64') ); content_buffer = Buffer.concat([cipher.update(feed_content), cipher.final()]); } else { content_buffer = Buffer.from(feed_content); } // Upload encrypted content let res = await request({ url:details.url, method:'PUT', headers:{ 'Content-Type':feed.contentType }, body:content_buffer }); this._validateUpOrDownloadSuccess(res, 'UPLOAD'); return {success:true}; } }; module.exports = SellingPartner;