Comparing version 0.6.4 to 0.7.0
{ | ||
"name": "asana", | ||
"main": "dist/asana.js", | ||
"version": "0.6.4", | ||
"version": "0.7.0", | ||
"homepage": "https://github.com/Asana/node-asana", | ||
@@ -6,0 +6,0 @@ "authors": [ |
@@ -20,12 +20,23 @@ /** | ||
/** | ||
* Establishes credentials. If credentials could be out of date and it is | ||
* possible to refresh them, does so. | ||
* Establishes credentials. | ||
* | ||
* @return {Promise} Resolves when credentials have been successfully | ||
* established and `authenticateRequest` calls can expect to succeed. | ||
* @return {Promise} Resolves when initial credentials have been | ||
* completed and `authenticateRequest` calls can expect to succeed. | ||
*/ | ||
Authenticator.prototype.ensureCredentials = function() { | ||
Authenticator.prototype.establishCredentials = function() { | ||
throw new Error('not implemented'); | ||
}; | ||
/** | ||
* Attempts to refresh credentials, if possible, given the current credentials. | ||
* | ||
* @return {Promise} Resolves to `true` if credentials have been successfully | ||
* established and `authenticateRequests` can expect to succeed, else | ||
* resolves to `false`. | ||
*/ | ||
Authenticator.prototype.refreshCredentials = function() { | ||
throw new Error('not implemented'); | ||
}; | ||
module.exports = Authenticator; |
@@ -23,6 +23,12 @@ var util = require('util'); | ||
BasicAuthenticator.prototype.ensureCredentials = function() { | ||
BasicAuthenticator.prototype.establishCredentials = function() { | ||
// Credentials are already built-in. | ||
return Bluebird.resolve(); | ||
}; | ||
BasicAuthenticator.prototype.refreshCredentials = function() { | ||
// We have no way of refreshing credentials. | ||
return Bluebird.resolve(false); | ||
}; | ||
module.exports = BasicAuthenticator; |
@@ -10,2 +10,3 @@ var util = require('util'); | ||
* of `flow` or `credentials`. | ||
* @option {App} app The app being authenticated for. | ||
* @option {OauthFlow} [flow] The flow to use to get credentials | ||
@@ -29,2 +30,3 @@ * when needed. | ||
this.flow = options.flow || null; | ||
this.app = options.app; | ||
} | ||
@@ -57,3 +59,4 @@ | ||
*/ | ||
OauthAuthenticator.prototype.ensureCredentials = function() { | ||
OauthAuthenticator.prototype.establishCredentials = function() { | ||
/* jshint camelcase: false */ | ||
var me = this; | ||
@@ -67,8 +70,51 @@ if (me.flow) { | ||
} else { | ||
// We were given a fixed set of credentials and don't know how to get | ||
// new ones, so assume what we have is ok. | ||
return Bluebird.resolve(); | ||
if (me.credentials.access_token) { | ||
// Assume what we have is ok. | ||
return Bluebird.resolve(); | ||
} else if (me.credentials.refresh_token) { | ||
// We were given a refresh token but NOT an access token. Get access. | ||
return me.refreshCredentials(); | ||
} else { | ||
// What kind of credentials did we get anyway? | ||
return Bluebird.reject(new Error('Invalid credentials')); | ||
} | ||
} | ||
}; | ||
/** | ||
* Attempts to refresh credentials, if possible, given the current credentials. | ||
* @return {Promise} Resolves to `true` if credentials have been successfully | ||
* established and `authenticateRequests` can expect to succeed, else | ||
* resolves to `false`. | ||
*/ | ||
OauthAuthenticator.prototype.refreshCredentials = function() { | ||
/* jshint camelcase: false */ | ||
var me = this; | ||
if (me.credentials && me.credentials.refresh_token) { | ||
// We have a refresh token. Use it to get a new access token. | ||
// Only have one outstanding request, any simultaneous requests waiting on | ||
// refresh should gate on this promise. | ||
if (!me.refreshPromise) { | ||
var refreshToken = me.credentials.refresh_token; | ||
me.refreshPromise = me.app.accessTokenFromRefreshToken(refreshToken).then( | ||
function(credentials) { | ||
// Update credentials, but hang on to refresh token. | ||
if (!credentials.refresh_token) { | ||
credentials.refresh_token = refreshToken; | ||
} | ||
me.credentials = credentials; | ||
me.refreshPromise = null; | ||
return true; | ||
}); | ||
} | ||
return me.refreshPromise; | ||
} else if (me.flow) { | ||
// Try running the flow again to get credentials. | ||
return this.ensureCredentials(); | ||
} else { | ||
// We are unable to refresh credentials automatically. | ||
return Bluebird.resolve(false); | ||
} | ||
}; | ||
module.exports = OauthAuthenticator; |
@@ -23,4 +23,17 @@ /* jshint browser:true */ | ||
var me = this; | ||
var popup, popupTimer, listener; | ||
function cleanup() { | ||
if (popup && popupTimer) { | ||
clearInterval(popupTimer); | ||
popupTimer = null; | ||
} | ||
if (listener) { | ||
window.removeEventListener('message', listener, false); | ||
listener = null; | ||
} | ||
} | ||
me._authorizationPromise = new Bluebird(function(resolve, reject) { | ||
var listener = function(event) { | ||
listener = function(event) { | ||
var receivedUrl; | ||
@@ -37,3 +50,3 @@ try { | ||
state = null; // don't ever respond to again | ||
window.removeEventListener('message', listener, false); | ||
cleanup(); | ||
if (params.error) { | ||
@@ -48,5 +61,34 @@ reject(new OauthError(params)); | ||
window.addEventListener('message', listener, false); | ||
window.open(authUrl, 'asana_oauth', me._popupParams(800, 600)); | ||
// TODO: listen for when window is closed so we don't hang forever if | ||
// user closes popup or it is blocked? | ||
popup = window.open(authUrl, 'asana_oauth', me._popupParams(800, 600)); | ||
// Detect popup blocking and fail. | ||
if (!popup) { | ||
cleanup(); | ||
reject(new OauthError({ | ||
'error': 'access_denied', | ||
'error_description': 'The popup window containing the ' + | ||
'authorization UI was blocked by the browser.' | ||
})); | ||
return; | ||
} | ||
// Detect popup closure (which may not be handled by the content, because | ||
// it may never load) and fail. If the popup posts a message to us, we | ||
// SHOULD get that message before it closes and this interval fires, | ||
// but just in case we wait for two successive intervals. | ||
var seenClosed = false; | ||
popupTimer = setInterval(function() { | ||
if (popup.closed) { | ||
if (seenClosed) { | ||
cleanup(); | ||
reject(new OauthError({ | ||
'error': 'access_denied', | ||
'error_description': 'The popup window containing the ' + | ||
'authorization UI was closed by the user.' | ||
})); | ||
} else { | ||
seenClosed = true; | ||
} | ||
} | ||
}, 500); | ||
}); | ||
@@ -53,0 +95,0 @@ return Promise.resolve(); |
@@ -126,4 +126,6 @@ var Dispatcher = require('./dispatcher'); | ||
if (options.credentials) { | ||
authenticator = | ||
new OauthAuthenticator({ credentials: options.credentials }); | ||
authenticator = new OauthAuthenticator({ | ||
app: this.app, | ||
credentials: options.credentials | ||
}); | ||
} else { | ||
@@ -135,3 +137,6 @@ var FlowType = options.flowType || autoDetect(); | ||
var flow = new FlowType(options); | ||
authenticator = new OauthAuthenticator({ flow: flow }); | ||
authenticator = new OauthAuthenticator({ | ||
app: this.app, | ||
flow: flow | ||
}); | ||
} | ||
@@ -138,0 +143,0 @@ this.dispatcher.setAuthenticator(authenticator); |
@@ -26,2 +26,5 @@ var errors = require('./errors'); | ||
* errors by sleeping and retrying after the waiting period. | ||
* @option {Function} [handleUnauthorized] Automatically handle | ||
* `NoAuthorization` with the callback. If the callback returns `true` | ||
* (or a promise resolving to `true), will retry the request. | ||
* @option {String} [asanaBaseUrl] Base URL for Asana, for debugging | ||
@@ -46,2 +49,10 @@ */ | ||
this.retryOnRateLimit = options.retryOnRateLimit || false; | ||
/** | ||
* Handler for unauthorized requests which may seek reauthorization. | ||
* Default behavior is available if configured with an Oauth authenticator | ||
* that has a refresh token, and will refresh the current access token. | ||
* @type {Function} | ||
*/ | ||
this.handleUnauthorized = (options.handleUnauthorized !== undefined) ? | ||
options.handleUnauthorized : Dispatcher.maybeReauthorize; | ||
} | ||
@@ -56,2 +67,15 @@ | ||
/** | ||
* Default handler for requests that are considered unauthorized. | ||
* Requests that the authenticator try to refresh its credentials if | ||
* possible. | ||
* @return {Promise<boolean>} True iff refresh was successful, false if not. | ||
*/ | ||
Dispatcher.maybeReauthorize = function() { | ||
if (!this.authenticator) { | ||
return false; | ||
} | ||
return this.authenticator.refreshCredentials(); | ||
}; | ||
/** | ||
* Creates an Asana API Url by concatenating the ROOT_URL with path provided. | ||
@@ -85,3 +109,3 @@ * @param {String} path The path | ||
} | ||
return this.authenticator.ensureCredentials(); | ||
return this.authenticator.establishCredentials(); | ||
}; | ||
@@ -98,10 +122,10 @@ | ||
var me = this; | ||
// TODO: actually honor these options as overriding defaults | ||
dispatchOptions = dispatchOptions || {}; | ||
if (me.authenticator !== null) { | ||
me.authenticator.authenticateRequest(params); | ||
} | ||
return new Bluebird(function (resolve, reject) { | ||
function doRequest() { | ||
if (me.authenticator !== null) { | ||
me.authenticator.authenticateRequest(params); | ||
} | ||
request(params, function(err, res, payload) { | ||
@@ -113,8 +137,24 @@ if (err) { | ||
var error = new STATUS_MAP[res.statusCode](payload); | ||
if (me.retryOnRateLimit && | ||
error instanceof (errors.RateLimitEnforced)) { | ||
// Maybe attempt retry for rate limiting. | ||
// Delay a half-second more in case of rounding error. | ||
// TODO: verify Asana is always being conservative with duration | ||
setTimeout(doRequest, error.retryAfterSeconds * 1000 + 500); | ||
} else if (me.handleUnauthorized && | ||
error instanceof (errors.NoAuthorization)) { | ||
// Maybe attempt to retry unauthorized requests after getting | ||
// a new access token. | ||
Bluebird.resolve(me.handleUnauthorized()).then(function(reauth) { | ||
if (reauth) { | ||
doRequest(); | ||
} else { | ||
reject(error); | ||
} | ||
}); | ||
} else { | ||
// Not an error we can handle. | ||
return reject(error); | ||
@@ -121,0 +161,0 @@ } |
{ | ||
"name": "asana", | ||
"version": "0.6.4", | ||
"version": "0.7.0", | ||
"description": "A node.js client for the Asana API", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
1095798
25348