paypal-node-sdk
Version:
Node SDK with promises and subscription API for PayPal REST v1 APIs
223 lines (203 loc) • 8.85 kB
JavaScript
/* Copyright 2015-2016 PayPal, Inc. */
;
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;
}
}