box-node-sdk 1.29.1 → 1.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/lib/api-request.js +10 -22
- package/lib/box-client.js +19 -6
- package/lib/managers/enterprise.js +2 -0
- package/lib/managers/files.js +21 -4
- package/lib/managers/folders.js +1 -1
- package/lib/managers/metadata.js +31 -4
- package/lib/managers/webhooks.js +2 -2
- package/lib/token-manager.js +95 -25
- package/lib/util/exponential-backoff.js +29 -0
- package/lib/util/paging-iterator.js +30 -6
- package/lib/util/url-path.js +10 -1
- package/package.json +21 -19
- package/lib/.DS_Store +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.33.0 [2020-06-25]
|
|
4
|
+
|
|
5
|
+
- Add path parameter sanitization ([#505](https://github.com/box/box-node-sdk/pull/505))
|
|
6
|
+
- Add support for all streams for uploading files ([#519](https://github.com/box/box-node-sdk/pull/519))
|
|
7
|
+
|
|
8
|
+
## 1.32.0 [2020-04-01]
|
|
9
|
+
|
|
10
|
+
- Temporarily removed Node 4 and Node 5 builds from Travis, due to tests not passing. Will investigate, going forward ([#495](https://github.com/box/box-node-sdk/pull/495)).
|
|
11
|
+
- Fixed an issue where an error is thrown during a retry when a response is not returned by the previous call ([#477](https://github.com/box/box-node-sdk/pull/477)).
|
|
12
|
+
- Added the ability to [query](./docs/metadata.md#query) Box items based on their metadata ([#487](https://github.com/box/box-node-sdk/pull/487)).
|
|
13
|
+
|
|
14
|
+
## 1.31.0 [2020-02-13]
|
|
15
|
+
|
|
16
|
+
- Fixed Authentication Request Retries
|
|
17
|
+
- Added marker-based paging for users endpoints
|
|
18
|
+
- Added `getNextMarker()` to PagingIterator to get the next marker
|
|
19
|
+
|
|
20
|
+
## 1.30.0 [2019-11-21]
|
|
21
|
+
|
|
22
|
+
- Deprecated Batch API methods
|
|
23
|
+
- Added support for [token exchange](./lib/box-client.js#L495) using shared links
|
|
24
|
+
|
|
3
25
|
## 1.29.1 [2019-08-22]
|
|
4
26
|
|
|
5
27
|
- Fixed an issue where JWT authentication requests could fail after being rate limited
|
package/lib/api-request.js
CHANGED
|
@@ -15,7 +15,8 @@ var assert = require('assert'),
|
|
|
15
15
|
request = require('request'),
|
|
16
16
|
EventEmitter = require('events').EventEmitter,
|
|
17
17
|
Config = require('./util/config'),
|
|
18
|
-
httpStatusCodes = require('http-status')
|
|
18
|
+
httpStatusCodes = require('http-status'),
|
|
19
|
+
getRetryTimeout = require('./util/exponential-backoff');
|
|
19
20
|
|
|
20
21
|
// ------------------------------------------------------------------------------
|
|
21
22
|
// Typedefs and Callbacks
|
|
@@ -94,9 +95,6 @@ var retryableStatusCodes = {};
|
|
|
94
95
|
retryableStatusCodes[httpStatusCodes.REQUEST_TIMEOUT] = true;
|
|
95
96
|
retryableStatusCodes[httpStatusCodes.TOO_MANY_REQUESTS] = true;
|
|
96
97
|
|
|
97
|
-
// Retry intervals are between 50% and 150% of the exponentially increasing base amount
|
|
98
|
-
const RETRY_RANDOMIZATION_FACTOR = 0.5;
|
|
99
|
-
|
|
100
98
|
/**
|
|
101
99
|
* Returns true if the response info indicates a temporary/transient error.
|
|
102
100
|
*
|
|
@@ -157,21 +155,6 @@ function cleanSensitiveHeaders(requestObj) {
|
|
|
157
155
|
}
|
|
158
156
|
}
|
|
159
157
|
|
|
160
|
-
/**
|
|
161
|
-
* Calculate the exponential backoff time with randomized jitter
|
|
162
|
-
* @param {int} numRetries Which retry number this one will be
|
|
163
|
-
* @param {int} baseInterval The base retry interval set in config
|
|
164
|
-
* @returns {int} The number of milliseconds after which to retry
|
|
165
|
-
*/
|
|
166
|
-
function getRetryTimeout(numRetries, baseInterval) {
|
|
167
|
-
|
|
168
|
-
var minRandomization = 1 - RETRY_RANDOMIZATION_FACTOR;
|
|
169
|
-
var maxRandomization = 1 + RETRY_RANDOMIZATION_FACTOR;
|
|
170
|
-
var randomization = (Math.random() * (maxRandomization - minRandomization)) + minRandomization;
|
|
171
|
-
var exponential = Math.pow(2, numRetries - 1);
|
|
172
|
-
return Math.ceil(exponential * baseInterval * randomization);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
158
|
// ------------------------------------------------------------------------------
|
|
176
159
|
// Public
|
|
177
160
|
// ------------------------------------------------------------------------------
|
|
@@ -241,7 +224,6 @@ APIRequest.prototype.getResponseStream = function() {
|
|
|
241
224
|
* @private
|
|
242
225
|
*/
|
|
243
226
|
APIRequest.prototype._handleResponse = function(err, response) {
|
|
244
|
-
|
|
245
227
|
// Clean sensitive headers here to prevent the user from accidentily using/logging them in prod
|
|
246
228
|
cleanSensitiveHeaders(this.request);
|
|
247
229
|
|
|
@@ -264,8 +246,12 @@ APIRequest.prototype._handleResponse = function(err, response) {
|
|
|
264
246
|
// Have the SDK emit the error response
|
|
265
247
|
this.eventBus.emit('response', err);
|
|
266
248
|
|
|
267
|
-
|
|
268
|
-
if (this.
|
|
249
|
+
var isJWT = false;
|
|
250
|
+
if (this.config.request.hasOwnProperty('form') && this.config.request.form.hasOwnProperty('grant_type') && this.config.request.form.grant_type === 'urn:ietf:params:oauth:grant-type:jwt-bearer') {
|
|
251
|
+
isJWT = true;
|
|
252
|
+
}
|
|
253
|
+
// If our APIRequest instance is retryable, attempt a retry. Otherwise, finish and propagate the error. Doesn't retry when the request is for JWT authentication, since that is handled in retryJWTGrant.
|
|
254
|
+
if (this.isRetryable && !isJWT) {
|
|
269
255
|
this._retry(err);
|
|
270
256
|
} else {
|
|
271
257
|
this._finish(err);
|
|
@@ -318,6 +304,8 @@ APIRequest.prototype._retry = function(err) {
|
|
|
318
304
|
this._finish(err);
|
|
319
305
|
return;
|
|
320
306
|
}
|
|
307
|
+
} else if (err.hasOwnProperty('response') && err.response.hasOwnProperty('headers') && err.response.headers.hasOwnProperty('retry-after')) {
|
|
308
|
+
retryTimeout = err.response.headers['retry-after'] * 1000;
|
|
321
309
|
} else {
|
|
322
310
|
retryTimeout = getRetryTimeout(this.numRetries, this.config.retryIntervalMS);
|
|
323
311
|
}
|
package/lib/box-client.js
CHANGED
|
@@ -492,6 +492,7 @@ BoxClient.prototype.revokeTokens = function(callback) {
|
|
|
492
492
|
* @param {string} [resource] The absolute URL of an API resource to scope the new token to
|
|
493
493
|
* @param {Object} [options] - Optional parameters
|
|
494
494
|
* @param {ActorParams} [options.actor] - Optional actor parameters for creating annotator tokens with Token Auth client
|
|
495
|
+
* @param {SharedLinkParams} [options.sharedLink] - Optional shared link parameters for creating tokens using shared links
|
|
495
496
|
* @param {Function} [callback] Called with the new token
|
|
496
497
|
* @returns {Promise<TokenInfo>} A promise resolving to the exchanged token info
|
|
497
498
|
*/
|
|
@@ -611,16 +612,27 @@ BoxClient.prototype.upload = function(path, params, formData, callback) {
|
|
|
611
612
|
/**
|
|
612
613
|
* Puts the client into batch mode, which will queue calls instead of
|
|
613
614
|
* immediately making the API request.
|
|
615
|
+
*
|
|
616
|
+
* DEPRECATED: Batch API is not supported and should not be used; make calls in parallel instead.
|
|
617
|
+
*
|
|
614
618
|
* @returns {BoxClient} Current client object
|
|
615
619
|
*/
|
|
616
|
-
BoxClient.prototype.batch = function() {
|
|
617
|
-
|
|
620
|
+
BoxClient.prototype.batch = util.deprecate(function() {
|
|
621
|
+
/* eslint-disable no-invalid-this */
|
|
618
622
|
this._batch = [];
|
|
619
623
|
return this;
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
BoxClient.prototype.batchExec = function(callback) {
|
|
624
|
+
/* eslint-enable no-invalid-this */
|
|
625
|
+
}, 'Batch API is not supported and should not be used; make calls in parallel instead.');
|
|
623
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Executes a batch of requests.
|
|
629
|
+
*
|
|
630
|
+
* DEPRECATED: Batch API is not supported and should not be used; make calls in parallel instead.
|
|
631
|
+
*
|
|
632
|
+
* @returns {Promise<Object>} Promise resolving to the collection of batch responses
|
|
633
|
+
*/
|
|
634
|
+
BoxClient.prototype.batchExec = util.deprecate(function(callback) {
|
|
635
|
+
/* eslint-disable no-invalid-this */
|
|
624
636
|
if (!this._batch) {
|
|
625
637
|
return Promise.reject(new Error('Must start a batch before executing'))
|
|
626
638
|
.asCallback(callback);
|
|
@@ -651,7 +663,8 @@ BoxClient.prototype.batchExec = function(callback) {
|
|
|
651
663
|
throw err;
|
|
652
664
|
})
|
|
653
665
|
.asCallback(callback);
|
|
654
|
-
|
|
666
|
+
/* eslint-enable no-invalid-this */
|
|
667
|
+
}, 'Batch API is not supported and should not be used; make calls in parallel instead.');
|
|
655
668
|
|
|
656
669
|
/**
|
|
657
670
|
* Build the 'BoxApi' Header used for authenticating access to a shared item
|
|
@@ -84,6 +84,8 @@ Enterprise.prototype.userRoles = Object.freeze({
|
|
|
84
84
|
* @param {Object} [options] - Optional parameters, can be left null in most cases
|
|
85
85
|
* @param {string} [options.filter_term] - Filter the results to only users starting with the filter_term in either the name or the login
|
|
86
86
|
* @param {int} [options.limit=100] - The number of records to return
|
|
87
|
+
* @param {boolean} [options.usemarker=false] - Whether or not to use marker-based pagination
|
|
88
|
+
* @param {string} [options.marker=''] - The marker for the page at which to start. Default is the first page
|
|
87
89
|
* @param {int} [options.offset=0] - The record at which to start
|
|
88
90
|
* @param {EnterpriseUserType} [options.user_type=managed] - The type of user to search for
|
|
89
91
|
* @param {Function} [callback] - Passed the list of users if successful, error otherwise
|
package/lib/managers/files.js
CHANGED
|
@@ -78,10 +78,11 @@ function createFileMetadataFormData(parentFolderID, filename, options) {
|
|
|
78
78
|
/**
|
|
79
79
|
* Returns the multipart form value for file upload content.
|
|
80
80
|
* @param {string|Buffer|Stream} content - the content of the file being uploaded
|
|
81
|
+
* @param {Object} options - options for the content
|
|
81
82
|
* @returns {Object} - the form value expected by the API for the 'content' key
|
|
82
83
|
* @private
|
|
83
84
|
*/
|
|
84
|
-
function createFileContentFormData(content) {
|
|
85
|
+
function createFileContentFormData(content, options) {
|
|
85
86
|
// The upload API appears to look for a form field that contains a filename
|
|
86
87
|
// property and assume that this form field contains the file content. Thus,
|
|
87
88
|
// the value of name does not actually matter (as long as it does not conflict
|
|
@@ -90,7 +91,7 @@ function createFileContentFormData(content) {
|
|
|
90
91
|
// filename specified in the metadata form field instead.
|
|
91
92
|
return {
|
|
92
93
|
value: content,
|
|
93
|
-
options: { filename: 'unused' }
|
|
94
|
+
options: Object.assign({ filename: 'unused' }, options)
|
|
94
95
|
};
|
|
95
96
|
}
|
|
96
97
|
|
|
@@ -601,6 +602,7 @@ Files.prototype.promoteVersion = function(fileID, versionID, callback) {
|
|
|
601
602
|
* @param {Object} [options] - Optional parameters
|
|
602
603
|
* @param {string} [options.content_created_at] - RFC 3339 timestamp when the file was created
|
|
603
604
|
* @param {string} [options.content_modified_at] - RFC 3339 timestamp when the file was last modified
|
|
605
|
+
* @param {int} [options.content_length] - Optional length of the content. Required if content is a read stream of any type other than fs stream.
|
|
604
606
|
* @param {Function} [callback] - called with data about the upload if successful, or an error if the
|
|
605
607
|
* upload failed
|
|
606
608
|
* @returns {Promise<Object>} A promise resolving to the uploaded file
|
|
@@ -613,10 +615,17 @@ Files.prototype.uploadFile = function(parentFolderID, filename, content, options
|
|
|
613
615
|
options = {};
|
|
614
616
|
}
|
|
615
617
|
|
|
618
|
+
var formOptions = {};
|
|
619
|
+
if (options && options.hasOwnProperty('content_length')) {
|
|
620
|
+
formOptions.knownLength = options.content_length;
|
|
621
|
+
// Delete content_length from options so it's not added to the attributes of the form
|
|
622
|
+
delete options.content_length;
|
|
623
|
+
}
|
|
624
|
+
|
|
616
625
|
var apiPath = urlPath(BASE_PATH, '/content'),
|
|
617
626
|
multipartFormData = {
|
|
618
627
|
attributes: createFileMetadataFormData(parentFolderID, filename, options),
|
|
619
|
-
content: createFileContentFormData(content)
|
|
628
|
+
content: createFileContentFormData(content, formOptions)
|
|
620
629
|
};
|
|
621
630
|
|
|
622
631
|
return this.client.wrapWithDefaultHandler(this.client.upload)(apiPath, null, multipartFormData, callback);
|
|
@@ -635,6 +644,7 @@ Files.prototype.uploadFile = function(parentFolderID, filename, content, options
|
|
|
635
644
|
* @param {Object} [options] - Optional parameters
|
|
636
645
|
* @param {string} [options.content_modified_at] - RFC 3339 timestamp when the file was last modified
|
|
637
646
|
* @param {string} [options.name] - A new name for the file
|
|
647
|
+
* @param {int} [options.content_length] - Optional length of the content. Required if content is a read stream of any type other than fs stream.
|
|
638
648
|
* @param {Function} [callback] - called with data about the upload if successful, or an error if the
|
|
639
649
|
* upload failed
|
|
640
650
|
* @returns {Promise<Object>} A promise resolving to the uploaded file
|
|
@@ -650,11 +660,18 @@ Files.prototype.uploadNewFileVersion = function(fileID, content, options, callba
|
|
|
650
660
|
var apiPath = urlPath(BASE_PATH, fileID, '/content'),
|
|
651
661
|
multipartFormData = {};
|
|
652
662
|
|
|
663
|
+
|
|
664
|
+
var formOptions = {};
|
|
653
665
|
if (options) {
|
|
666
|
+
if (options.hasOwnProperty('content_length')) {
|
|
667
|
+
formOptions.knownLength = options.content_length;
|
|
668
|
+
// Delete content_length from options so it's not added to the attributes of the form
|
|
669
|
+
delete options.content_length;
|
|
670
|
+
}
|
|
654
671
|
multipartFormData.attributes = JSON.stringify(options);
|
|
655
672
|
}
|
|
656
673
|
|
|
657
|
-
multipartFormData.content = createFileContentFormData(content);
|
|
674
|
+
multipartFormData.content = createFileContentFormData(content, formOptions);
|
|
658
675
|
|
|
659
676
|
return this.client.wrapWithDefaultHandler(this.client.upload)(apiPath, null, multipartFormData, callback);
|
|
660
677
|
};
|
package/lib/managers/folders.js
CHANGED
|
@@ -61,7 +61,7 @@ Folders.prototype.get = function(folderID, options, callback) {
|
|
|
61
61
|
* @param {string} folderID - Box ID of the folder being requested
|
|
62
62
|
* @param {Object} [options] - Additional options for the request. Can be left null in most cases.
|
|
63
63
|
* @param {Function} [callback] - Passed the folder information if it was acquired successfully
|
|
64
|
-
* @returns {Promise<Object>} A
|
|
64
|
+
* @returns {Promise<Object>} A promise resolving to the collection of the items in the folder
|
|
65
65
|
*/
|
|
66
66
|
Folders.prototype.getItems = function(folderID, options, callback) {
|
|
67
67
|
var params = {
|
package/lib/managers/metadata.js
CHANGED
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
// -----------------------------------------------------------------------------
|
|
29
29
|
// Requirements
|
|
30
30
|
// -----------------------------------------------------------------------------
|
|
31
|
-
var urlPath = require('../util/url-path')
|
|
31
|
+
var urlPath = require('../util/url-path'),
|
|
32
|
+
merge = require('merge-options');
|
|
32
33
|
|
|
33
34
|
// -----------------------------------------------------------------------------
|
|
34
35
|
// Private
|
|
@@ -38,7 +39,8 @@ var PROPERTIES_TEMPLATE = 'properties',
|
|
|
38
39
|
SCHEMA_SUBRESOURCE = 'schema',
|
|
39
40
|
ENTERPRISE_SCOPE = 'enterprise',
|
|
40
41
|
GLOBAL_SCOPE = 'global',
|
|
41
|
-
CASCADE_POLICIES_PATH = '/metadata_cascade_policies'
|
|
42
|
+
CASCADE_POLICIES_PATH = '/metadata_cascade_policies',
|
|
43
|
+
QUERY_PATH = '/metadata_queries/execute_read';
|
|
42
44
|
|
|
43
45
|
// -----------------------------------------------------------------------------
|
|
44
46
|
// Public
|
|
@@ -178,7 +180,7 @@ Metadata.prototype = {
|
|
|
178
180
|
* @param {Object[]} operations - The operations to perform
|
|
179
181
|
* @param {Function} [callback] - Passed the updated template if successful, error otherwise
|
|
180
182
|
* @returns {Promise<Object>} A promise resolving to the updated template
|
|
181
|
-
* @see {@link https://
|
|
183
|
+
* @see {@link https://developer.box.com/en/reference/put-metadata-templates-id-id-schema/}
|
|
182
184
|
*/
|
|
183
185
|
updateTemplate(scope, template, operations, callback) {
|
|
184
186
|
|
|
@@ -200,7 +202,7 @@ Metadata.prototype = {
|
|
|
200
202
|
* @param {string} template - The template to delete
|
|
201
203
|
* @param {Function} [callback] - Passed empty response body if successful, err otherwise
|
|
202
204
|
* @returns {Promise<void>} A promise resolving to nothing
|
|
203
|
-
* @see {@link https://
|
|
205
|
+
* @see {@link https://developer.box.com/en/reference/delete-metadata-templates-id-id-schema/}
|
|
204
206
|
*/
|
|
205
207
|
deleteTemplate(scope, template, callback) {
|
|
206
208
|
|
|
@@ -313,6 +315,31 @@ Metadata.prototype = {
|
|
|
313
315
|
};
|
|
314
316
|
|
|
315
317
|
return this.client.wrapWithDefaultHandler(this.client.post)(apiPath, params, callback);
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Query Box items by their metadata
|
|
322
|
+
*
|
|
323
|
+
* API Endpoint: '/metadata_queries/execute_read'
|
|
324
|
+
* Method: POST
|
|
325
|
+
*
|
|
326
|
+
* @param {string} from - The template used in the query. Must be in the form scope.templateKey
|
|
327
|
+
* @param {string} ancestorFolderId - The folder_id to which to restrain the query
|
|
328
|
+
* @param {Object} options - Query options
|
|
329
|
+
* @param {Function} [callback] - Passed a collection of items and their associated metadata
|
|
330
|
+
* @returns {Promise<void>} Promise resolving to a collection of items and their associated metadata
|
|
331
|
+
*/
|
|
332
|
+
query(from, ancestorFolderId, options, callback) {
|
|
333
|
+
var body = {
|
|
334
|
+
from,
|
|
335
|
+
ancestor_folder_id: ancestorFolderId
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
var params = {
|
|
339
|
+
body: merge(body, options)
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
return this.client.wrapWithDefaultHandler(this.client.post)(QUERY_PATH, params, callback);
|
|
316
343
|
}
|
|
317
344
|
};
|
|
318
345
|
|
package/lib/managers/webhooks.js
CHANGED
|
@@ -31,7 +31,7 @@ const MAX_MESSAGE_AGE = 10 * 60; // 10 minutes
|
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Compute the message signature
|
|
34
|
-
* @see {@Link https://
|
|
34
|
+
* @see {@Link https://developer.box.com/en/guides/webhooks/handle/setup-signatures/}
|
|
35
35
|
*
|
|
36
36
|
* @param {string} body - The request body of the webhook message
|
|
37
37
|
* @param {Object} headers - The request headers of the webhook message
|
|
@@ -63,7 +63,7 @@ function computeSignature(body, headers, signatureKey) {
|
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
65
|
* Validate the message signature
|
|
66
|
-
* @see {@Link https://
|
|
66
|
+
* @see {@Link https://developer.box.com/en/guides/webhooks/handle/verify-signatures/}
|
|
67
67
|
*
|
|
68
68
|
* @param {string} body - The request body of the webhook message
|
|
69
69
|
* @param {Object} headers - The request headers of the webhook message
|
package/lib/token-manager.js
CHANGED
|
@@ -18,6 +18,12 @@
|
|
|
18
18
|
* server requesting the tokens.
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Parameters for creating a token using a Box shared link via token exchange
|
|
23
|
+
* @typedef {Object} SharedLinkParams
|
|
24
|
+
* @property {string} url Shared link URL
|
|
25
|
+
*/
|
|
26
|
+
|
|
21
27
|
/**
|
|
22
28
|
* Parameters for creating an actor token via token exchange
|
|
23
29
|
* @typedef {Object} ActorParams
|
|
@@ -47,12 +53,12 @@
|
|
|
47
53
|
*/
|
|
48
54
|
function isJWTAuthErrorRetryable(err) {
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
+
if (err.authExpired && err.response.headers.date && (err.response.body.error_description.indexOf('exp') > -1 || err.response.body.error_description.indexOf('jti') > -1)) {
|
|
57
|
+
return true;
|
|
58
|
+
} else if (err.statusCode === 429 || err.statusCode >= 500) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
// ------------------------------------------------------------------------------
|
|
@@ -62,7 +68,8 @@ var errors = require('./util/errors'),
|
|
|
62
68
|
jwt = require('jsonwebtoken'),
|
|
63
69
|
uuid = require('uuid'),
|
|
64
70
|
httpStatusCodes = require('http-status'),
|
|
65
|
-
Promise = require('bluebird')
|
|
71
|
+
Promise = require('bluebird'),
|
|
72
|
+
getRetryTimeout = require('./util/exponential-backoff');
|
|
66
73
|
|
|
67
74
|
// ------------------------------------------------------------------------------
|
|
68
75
|
// Constants
|
|
@@ -94,6 +101,9 @@ var tokenPaths = {
|
|
|
94
101
|
REVOKE: '/revoke'
|
|
95
102
|
};
|
|
96
103
|
|
|
104
|
+
// Timer used to track elapsed time starting with executing an async request and ending with emitting the response.
|
|
105
|
+
var asyncRequestTimer;
|
|
106
|
+
|
|
97
107
|
// The XFF header label - Used to give the API better information for uploads, rate-limiting, etc.
|
|
98
108
|
const HEADER_XFF = 'X-Forwarded-For';
|
|
99
109
|
const ACCESS_TOKEN_TYPE = 'urn:ietf:params:oauth:token-type:access_token';
|
|
@@ -339,7 +349,6 @@ TokenManager.prototype = {
|
|
|
339
349
|
try {
|
|
340
350
|
assertion = jwt.sign(claims, keyParams, jwtOptions);
|
|
341
351
|
} catch (jwtErr) {
|
|
342
|
-
|
|
343
352
|
return Promise.reject(jwtErr);
|
|
344
353
|
}
|
|
345
354
|
|
|
@@ -347,29 +356,85 @@ TokenManager.prototype = {
|
|
|
347
356
|
grant_type: grantTypes.JWT,
|
|
348
357
|
assertion
|
|
349
358
|
};
|
|
359
|
+
// Start the request timer immediately before executing the async request
|
|
360
|
+
asyncRequestTimer = process.hrtime();
|
|
350
361
|
return this.getTokens(params, options)
|
|
351
|
-
.catch(err =>
|
|
352
|
-
|
|
353
|
-
// When a client's clock is out of sync with Box API servers, they'll get an error about the exp claim
|
|
354
|
-
// In these cases, we can attempt to retry the grant request with a new exp claim calculated frem the
|
|
355
|
-
// Date header sent by the server
|
|
356
|
-
if (isJWTAuthErrorRetryable(err)) {
|
|
357
|
-
|
|
358
|
-
var serverTime = Math.floor(Date.parse(err.response.headers.date) / 1000);
|
|
359
|
-
claims.exp = serverTime + this.config.appAuth.expirationTime;
|
|
360
|
-
jwtOptions.jwtid = uuid.v4();
|
|
362
|
+
.catch(err => this.retryJWTGrant(claims, jwtOptions, keyParams, params, options, err, 0));
|
|
363
|
+
},
|
|
361
364
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
365
|
+
/**
|
|
366
|
+
* Attempt a retry if possible and create a new JTI claim. If the request hasn't exceeded it's maximum number of retries,
|
|
367
|
+
* re-execute the request (after the retry interval). Otherwise, propagate a new error.
|
|
368
|
+
*
|
|
369
|
+
* @param {Object} claims - JTI claims object
|
|
370
|
+
* @param {Object} [jwtOptions] - JWT options for the signature
|
|
371
|
+
* @param {Object} keyParams - Key JWT parameters object that contains the private key and the passphrase
|
|
372
|
+
* @param {Object} params - Should contain all params expected by Box OAuth2 token endpoint
|
|
373
|
+
* @param {TokenRequestOptions} [options] - Sets optional behavior for the token grant
|
|
374
|
+
* @param {Error} error - Error from the previous JWT request
|
|
375
|
+
* @param {int} numRetries - Number of retries attempted
|
|
376
|
+
* @returns {Promise<TokenInfo>} Promise resolving to the token info
|
|
377
|
+
*/
|
|
378
|
+
// eslint-disable-next-line max-params
|
|
379
|
+
retryJWTGrant(claims, jwtOptions, keyParams, params, options, error, numRetries) {
|
|
380
|
+
if (numRetries < this.config.numMaxRetries && isJWTAuthErrorRetryable(error)) {
|
|
381
|
+
var retryTimeout;
|
|
382
|
+
numRetries += 1;
|
|
383
|
+
// If the retry strategy is defined, then use it to determine the time (in ms) until the next retry or to
|
|
384
|
+
// propagate an error to the user.
|
|
385
|
+
if (this.config.retryStrategy) {
|
|
386
|
+
// Get the total elapsed time so far since the request was executed
|
|
387
|
+
var totalElapsedTime = process.hrtime(asyncRequestTimer);
|
|
388
|
+
var totalElapsedTimeMS = (totalElapsedTime[0] * 1000) + (totalElapsedTime[1] / 1000000);
|
|
389
|
+
var retryOptions = {
|
|
390
|
+
error,
|
|
391
|
+
numRetryAttempts: numRetries,
|
|
392
|
+
numMaxRetries: this.config.numMaxRetries,
|
|
393
|
+
retryIntervalMS: this.config.retryIntervalMS,
|
|
394
|
+
totalElapsedTimeMS
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
retryTimeout = this.config.retryStrategy(retryOptions);
|
|
398
|
+
|
|
399
|
+
// If the retry strategy doesn't return a number/time in ms, then propagate the response error to the user.
|
|
400
|
+
// However, if the retry strategy returns its own error, this will be propagated to the user instead.
|
|
401
|
+
if (typeof retryTimeout !== 'number') {
|
|
402
|
+
if (retryTimeout instanceof Error) {
|
|
403
|
+
error = retryTimeout;
|
|
366
404
|
}
|
|
367
|
-
|
|
368
|
-
return this.getTokens(params, options);
|
|
405
|
+
throw error;
|
|
369
406
|
}
|
|
407
|
+
} else if (error.hasOwnProperty('response') && error.response.hasOwnProperty('headers') && error.response.headers.hasOwnProperty('retry-after')) {
|
|
408
|
+
retryTimeout = error.response.headers['retry-after'] * 1000;
|
|
409
|
+
} else {
|
|
410
|
+
retryTimeout = getRetryTimeout(numRetries, this.config.retryIntervalMS);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
var time = Math.floor(Date.now() / 1000);
|
|
414
|
+
if (error.response.headers.date) {
|
|
415
|
+
time = Math.floor(Date.parse(error.response.headers.date) / 1000);
|
|
416
|
+
}
|
|
417
|
+
// Add length of retry timeout to current expiration time to calculate the expiration time for the JTI claim.
|
|
418
|
+
claims.exp = time + this.config.appAuth.expirationTime + (retryTimeout / 1000);
|
|
419
|
+
jwtOptions.jwtid = uuid.v4();
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
params.assertion = jwt.sign(claims, keyParams, jwtOptions);
|
|
423
|
+
} catch (jwtErr) {
|
|
424
|
+
throw jwtErr;
|
|
425
|
+
}
|
|
370
426
|
|
|
371
|
-
|
|
427
|
+
return Promise.delay(retryTimeout).then(() => {
|
|
428
|
+
// Start the request timer immediately before executing the async request
|
|
429
|
+
asyncRequestTimer = process.hrtime();
|
|
430
|
+
return this.getTokens(params, options)
|
|
431
|
+
.catch(err => this.retryJWTGrant(claims, jwtOptions, keyParams, params, options, err, numRetries));
|
|
372
432
|
});
|
|
433
|
+
} else if (numRetries >= this.config.numMaxRetries) {
|
|
434
|
+
error.maxRetriesExceeded = true;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
throw error;
|
|
373
438
|
},
|
|
374
439
|
|
|
375
440
|
/**
|
|
@@ -382,6 +447,7 @@ TokenManager.prototype = {
|
|
|
382
447
|
* @param {Object} [options] - Optional parameters
|
|
383
448
|
* @param {TokenRequestOptions} [options.tokenRequestOptions] - Sets optional behavior for the token grant
|
|
384
449
|
* @param {ActorParams} [options.actor] - Optional actor parameters for creating annotator tokens
|
|
450
|
+
* @param {SharedLinkParams} [options.sharedLink] - Optional shared link parameters for creating tokens using shared links
|
|
385
451
|
* @returns {Promise<TokenInfo>} Promise resolving to the new token info
|
|
386
452
|
*/
|
|
387
453
|
exchangeToken(accessToken, scopes, resource, options) {
|
|
@@ -396,6 +462,10 @@ TokenManager.prototype = {
|
|
|
396
462
|
params.resource = resource;
|
|
397
463
|
}
|
|
398
464
|
|
|
465
|
+
if (options && options.sharedLink) {
|
|
466
|
+
params.box_shared_link = options.sharedLink.url;
|
|
467
|
+
}
|
|
468
|
+
|
|
399
469
|
if (options && options.actor) {
|
|
400
470
|
|
|
401
471
|
var payload = {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Calculate exponential backoff time
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
// ------------------------------------------------------------------------------
|
|
8
|
+
// Private
|
|
9
|
+
// ------------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
// Retry intervals are between 50% and 150% of the exponentially increasing base amount
|
|
12
|
+
const RETRY_RANDOMIZATION_FACTOR = 0.5;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Calculate the exponential backoff time with randomized jitter
|
|
16
|
+
* @param {int} numRetries Which retry number this one will be
|
|
17
|
+
* @param {int} baseInterval The base retry interval set in config
|
|
18
|
+
* @returns {int} The number of milliseconds after which to retry
|
|
19
|
+
*/
|
|
20
|
+
function getRetryTimeout(numRetries, baseInterval) {
|
|
21
|
+
|
|
22
|
+
var minRandomization = 1 - RETRY_RANDOMIZATION_FACTOR;
|
|
23
|
+
var maxRandomization = 1 + RETRY_RANDOMIZATION_FACTOR;
|
|
24
|
+
var randomization = (Math.random() * (maxRandomization - minRandomization)) + minRandomization;
|
|
25
|
+
var exponential = Math.pow(2, numRetries - 1);
|
|
26
|
+
return Math.ceil(exponential * baseInterval * randomization);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports = getRetryTimeout;
|
|
@@ -58,11 +58,11 @@ class PagingIterator {
|
|
|
58
58
|
* @returns {boolean} Whether the response is iterable
|
|
59
59
|
*/
|
|
60
60
|
static isIterable(response) {
|
|
61
|
-
var
|
|
61
|
+
var isGetOrPostRequest = (response.request && (response.request.method === 'GET' || response.request.method === 'POST')),
|
|
62
62
|
hasEntries = (response.body && Array.isArray(response.body.entries)),
|
|
63
63
|
notEventStream = (response.body && !response.body.next_stream_position);
|
|
64
64
|
|
|
65
|
-
return Boolean(
|
|
65
|
+
return Boolean(isGetOrPostRequest && hasEntries && notEventStream);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
@@ -80,7 +80,6 @@ class PagingIterator {
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
var data = response.body;
|
|
83
|
-
|
|
84
83
|
if (Number.isSafeInteger(data.offset)) {
|
|
85
84
|
this.nextField = PAGING_MODES.OFFSET;
|
|
86
85
|
this.nextValue = data.offset;
|
|
@@ -102,6 +101,13 @@ class PagingIterator {
|
|
|
102
101
|
headers: response.request.headers,
|
|
103
102
|
qs: querystring.parse(response.request.uri.query)
|
|
104
103
|
};
|
|
104
|
+
if (response.request.body) {
|
|
105
|
+
if (Object.prototype.toString.call(response.request.body) === '[object Object]') {
|
|
106
|
+
this.options.body = response.request.body;
|
|
107
|
+
} else {
|
|
108
|
+
this.options.body = JSON.parse(response.request.body);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
105
111
|
|
|
106
112
|
// querystring.parse() makes everything a string, ensure numeric params are the correct type
|
|
107
113
|
if (this.options.qs.limit) {
|
|
@@ -112,7 +118,12 @@ class PagingIterator {
|
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
delete this.options.headers.Authorization;
|
|
115
|
-
|
|
121
|
+
if (response.request.method === 'GET') {
|
|
122
|
+
this.fetch = client.get.bind(client, href);
|
|
123
|
+
}
|
|
124
|
+
if (response.request.method === 'POST') {
|
|
125
|
+
this.fetch = client.post.bind(client, href);
|
|
126
|
+
}
|
|
116
127
|
this.buffer = response.body.entries;
|
|
117
128
|
this.queue = new PromiseQueue(1, Infinity);
|
|
118
129
|
this._updatePaging(response);
|
|
@@ -146,8 +157,13 @@ class PagingIterator {
|
|
|
146
157
|
this.done = true;
|
|
147
158
|
}
|
|
148
159
|
}
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
if (response.request.method === 'GET') {
|
|
161
|
+
this.options.qs[this.nextField] = this.nextValue;
|
|
162
|
+
} else if (response.request.method === 'POST') {
|
|
163
|
+
this.options.body[this.nextField] = this.nextValue;
|
|
164
|
+
let bodyString = JSON.stringify(this.options.body);
|
|
165
|
+
this.options.headers['content-length'] = bodyString.length;
|
|
166
|
+
}
|
|
151
167
|
}
|
|
152
168
|
|
|
153
169
|
/**
|
|
@@ -215,6 +231,14 @@ class PagingIterator {
|
|
|
215
231
|
|
|
216
232
|
return this.queue.add(this._getData.bind(this));
|
|
217
233
|
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Fetch the next marker
|
|
237
|
+
* @returns {string|int} String that is the next marker or int that is the next offset
|
|
238
|
+
*/
|
|
239
|
+
getNextMarker() {
|
|
240
|
+
return this.nextValue;
|
|
241
|
+
}
|
|
218
242
|
}
|
|
219
243
|
|
|
220
244
|
module.exports = PagingIterator;
|
package/lib/util/url-path.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// Private
|
|
9
9
|
// ------------------------------------------------------------------------------
|
|
10
10
|
|
|
11
|
+
// Pattern to check for relative paths
|
|
12
|
+
var PATTERN = /\/\.+/;
|
|
11
13
|
/**
|
|
12
14
|
* remove leading & trailing slashes from some string. This is useful for
|
|
13
15
|
* removing slashes from the path segments that are actually a part of the
|
|
@@ -39,7 +41,14 @@ function trimSlashes(segment) {
|
|
|
39
41
|
*/
|
|
40
42
|
module.exports = function urlPath(/* arguments*/) {
|
|
41
43
|
var args = Array.prototype.slice.call(arguments);
|
|
42
|
-
var path = args.map(x => String(x))
|
|
44
|
+
var path = args.map(x => String(x))
|
|
45
|
+
.map(x => {
|
|
46
|
+
var trimmedX = trimSlashes(x);
|
|
47
|
+
if (PATTERN.test(trimmedX)) {
|
|
48
|
+
throw new Error(`An invalid path parameter exists in ${trimmedX}. Relative path parameters cannot be passed.`);
|
|
49
|
+
}
|
|
50
|
+
return trimmedX;
|
|
51
|
+
})
|
|
43
52
|
.map(x => encodeURIComponent(x))
|
|
44
53
|
.join('/');
|
|
45
54
|
return `/${path}`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "box-node-sdk",
|
|
3
3
|
"author": "Box <oss@box.com>",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.33.0",
|
|
5
5
|
"description": "Official SDK for Box Plaform APIs",
|
|
6
6
|
"license": "Apache-2.0",
|
|
7
7
|
"repository": {
|
|
@@ -32,33 +32,35 @@
|
|
|
32
32
|
"major": "node Makefile.js major"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"bluebird": "^3.
|
|
36
|
-
"http-status": "^1.1
|
|
37
|
-
"jsonwebtoken": "^8.
|
|
35
|
+
"bluebird": "^3.7.1",
|
|
36
|
+
"http-status": "^1.4.1",
|
|
37
|
+
"jsonwebtoken": "^8.5.1",
|
|
38
38
|
"merge-options": "^1.0.1",
|
|
39
39
|
"promise-queue": "^2.2.3",
|
|
40
|
-
"request": "^2.
|
|
40
|
+
"request": "^2.88.0",
|
|
41
41
|
"url-template": "^2.0.8",
|
|
42
|
-
"uuid": "^3.
|
|
42
|
+
"uuid": "^3.3.3"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"chai": "^4.
|
|
46
|
-
"coveralls": "^3.0.
|
|
47
|
-
"eslint": "^4.
|
|
48
|
-
"eslint-plugin-node": "^6.0.
|
|
49
|
-
"eslint-plugin-promise": "^3.
|
|
50
|
-
"eslint-plugin-unicorn": "^4.0.
|
|
45
|
+
"chai": "^4.2.0",
|
|
46
|
+
"coveralls": "^3.0.8",
|
|
47
|
+
"eslint": "^4.19.1",
|
|
48
|
+
"eslint-plugin-node": "^6.0.1",
|
|
49
|
+
"eslint-plugin-promise": "^3.8.0",
|
|
50
|
+
"eslint-plugin-unicorn": "^4.0.3",
|
|
51
51
|
"istanbul": "^0.4.3",
|
|
52
|
-
"jsdoc": "^3.
|
|
52
|
+
"jsdoc": "^3.6.3",
|
|
53
53
|
"jsonlint2": "^1.7.1",
|
|
54
|
-
"leche": "^2.
|
|
55
|
-
"mocha": "^5.0
|
|
54
|
+
"leche": "^2.3.0",
|
|
55
|
+
"mocha": "^5.2.0",
|
|
56
56
|
"mockery": "^2.1.0",
|
|
57
|
-
"nock": "^9.
|
|
58
|
-
"
|
|
59
|
-
"
|
|
57
|
+
"nock": "^9.6.1",
|
|
58
|
+
"np": "^5.1.3",
|
|
59
|
+
"npm-upgrade": "^2.0.3",
|
|
60
|
+
"nyc": "^11.9.0",
|
|
61
|
+
"shelljs": "^0.8.3",
|
|
60
62
|
"shelljs-nodecli": "^0.1.1",
|
|
61
|
-
"sinon": "^7.
|
|
63
|
+
"sinon": "^7.5.0"
|
|
62
64
|
},
|
|
63
65
|
"files": [
|
|
64
66
|
"config",
|
package/lib/.DS_Store
DELETED
|
Binary file
|