UNPKG

paypal-node-sdk

Version:

Node SDK with promises and subscription API for PayPal REST v1 APIs

223 lines (203 loc) 8.85 kB
/* Copyright 2015-2016 PayPal, Inc. */ "use strict"; var client = require('./client'); var utils = require('./utils'); var configuration = require('./configure'); /** * token_persist client id to access token cache, used to reduce access token round trips * @type {Object} */ var token_persist = {}; /** * Set up configuration globally such as client_id and client_secret, * by merging user provided configurations otherwise use default settings * @param {Object} options Configuration parameters passed as object * @return {undefined} */ var configure = exports.configure = function configure(options) { if (options !== undefined && typeof options === 'object') { configuration.default_options = utils.merge(configuration.default_options, options); } if (configuration.default_options.mode !== 'sandbox' && configuration.default_options.mode !== 'live') { throw new Error('Mode must be "sandbox" or "live"'); } }; /** * Generate new access token by making a POST request to /oauth2/token by * exchanging base64 encoded client id/secret pair or valid refresh token. * * Otherwise authorization code from a mobile device can be exchanged for a long * living refresh token used to charge user who has consented to future payments. * @param {Object|Function} config Configuration parameters such as authorization code or refresh token * @param {Function} cb Callback function * @return {String} Access token or Refresh token */ var generateToken = exports.generateToken = function generateToken(config, cb) { if (typeof config === "function") { cb = config; config = configuration.default_options; } else if (!config) { config = configuration.default_options; } else { config = utils.merge(config, configuration.default_options, true); } var payload = 'grant_type=client_credentials'; if (config.authorization_code) { payload = 'grant_type=authorization_code&response_type=token&redirect_uri=urn:ietf:wg:oauth:2.0:oob&code=' + config.authorization_code; } else if (config.refresh_token) { payload = 'grant_type=refresh_token&refresh_token=' + config.refresh_token; } var basicAuthString = 'Basic ' + new Buffer(config.client_id + ':' + config.client_secret).toString('base64'); var http_options = { schema: config.schema || configuration.default_options.schema, host: utils.getDefaultApiEndpoint(config.mode) || config.host || configuration.default_options.host, port: config.port || configuration.default_options.port, headers: utils.merge({ 'Authorization': basicAuthString, 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, configuration.default_options.headers, true) }; client.invoke('POST', '/v1/oauth2/token', payload, http_options, function (err, res) { var token = null; if (res) { if (!config.authorization_code && !config.refresh_token) { var seconds = new Date().getTime() / 1000; token_persist[config.client_id] = res; token_persist[config.client_id].created_at = seconds; } if (!config.authorization_code) { token = res.token_type + ' ' + res.access_token; } else { token = res.refresh_token; } } cb(err, token); }); }; /* Update authorization header with new token obtained by calling generateToken */ /** * Updates http Authorization header to newly created access token * @param {Object} http_options Configuration parameters such as authorization code or refresh token * @param {Function} error_callback * @param {Function} callback */ function updateToken(http_options, error_callback, callback) { generateToken(http_options, function (error, token) { if (error) { error_callback(error, token); } else { http_options.headers.Authorization = token; callback(); } }); } /** * Makes a PayPal REST API call. Reuses valid access tokens to reduce * round trips, handles 401 error and token expiration. * @param {String} http_method A HTTP Verb e.g. GET or POST * @param {String} path Url endpoint for API request * @param {Data} data Payload associated with API request * @param {Object|Function} http_options Configurations for settings and Auth * @param {Function} cb Callback function */ var executeHttp = exports.executeHttp = function executeHttp(http_method, path, data, http_options, cb) { if (typeof http_options === "function") { cb = http_options; http_options = null; } if (!http_options) { http_options = configuration.default_options; } else { http_options = utils.merge(http_options, configuration.default_options, true); } //Get host endpoint using mode http_options.host = utils.getDefaultApiEndpoint(http_options.mode) || http_options.host; function retryInvoke() { client.invoke(http_method, path, data, http_options, cb); } // correlation-id is deprecated in favor of client-metadata-id if (http_options.client_metadata_id) { http_options.headers['Paypal-Client-Metadata-Id'] = http_options.client_metadata_id; } else if (http_options.correlation_id) { http_options.headers['Paypal-Client-Metadata-Id'] = http_options.correlation_id; } // If client_id exists with an unexpired token and a refresh token is not provided, reuse cached token if (http_options.client_id in token_persist && !utils.checkExpiredToken(token_persist[http_options.client_id]) && !http_options.refresh_token) { http_options.headers.Authorization = "Bearer " + token_persist[http_options.client_id].access_token; client.invoke(http_method, path, data, http_options, function (error, response) { // Don't reprompt already authenticated user for login by updating Authorization header // if token expires if (error && error.httpStatusCode === 401 && http_options.client_id && http_options.headers.Authorization) { http_options.headers.Authorization = null; updateToken(http_options, cb, retryInvoke); } else { cb(error, response); } }); } else { updateToken(http_options, cb, retryInvoke); } }; /** * Makes a PayPal REST API call as async call. * * Reuses valid access tokens to reduce round trips, handles 401 error and token expiration. * * @param {String} httpMethod A HTTP Verb e.g. "GET" or "POST" * @param {String} path URL path endpoint for API request, e.g. "/v1/payment/capture" * @param {Data} data Payload associated with API request. Depends on API call. * @param {Object} httpOptions Configurations for settings and Auth (Optional) * @returns {JSON|Object} The server response (if successful) * @throws {Error} Error from server (if failed) * `message` error message from server * Normally also has properties: * `code` with an HTTP error code * `error` with the error JSON from the server */ var executeAsync = exports.executeAsync = async function executeAsync(httpMethod, path, data, httpOptions) { let stack = new Error().stack; return new Promise((resolve, reject) => { executeHttp(httpMethod, path, data, httpOptions, (error, result) => { if (error) { reject(convertPayPalErrorToException(error, stack)); } else { delete result.links; delete result.httpStatusCode; resolve(result); } }); }); }; /** * Extracts error message in JSON from PayPal * @param error {JSON} Server response * @param stack {Error.stack} * @returns {Exception} */ function convertPayPalErrorToException(error, stack) { if (error.response && error.response.message) { // error.message might just be "Response Code : 400", see PayPal SDK client.js line 130. let message = error.response.message; if (error.response.details && error.response.details.length) { message += ": " + error.response.details.map(d => d.description ? d.description + " (" + d.issue + ")" : d.issue).join("; "); } delete error.response.links; let ex = new Error(message); ex.code = error.response.httpStatusCode || 402; ex.stack = stack; ex.details = error; return ex; } else if (typeof(error) == "string") { let ex = new Error(error); ex.stack = stack; ex.code = 402; return ex; } else { error.stack = stack; return error; } }