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 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
@@ -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
- // If our APIRequest instance is retryable, attempt a retry. Otherwise, finish and propagate the error.
268
- if (this.isRetryable) {
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
@@ -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
  };
@@ -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 prmoise resolving to the collection of the items in the folder
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 = {
@@ -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://docs.box.com/reference#update-metadata-schema}
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://docs.box.com/reference#delete-metadata-schema}
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
 
@@ -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://docs.box.com/reference#signatures}
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://docs.box.com/reference#signatures}
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
@@ -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
- return err.authExpired
51
- && err.response.headers.date
52
- && (
53
- err.response.body.error_description.indexOf('exp') > -1
54
- || err.response.body.error_description.indexOf('jti') > -1
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
- try {
363
- params.assertion = jwt.sign(claims, keyParams, jwtOptions);
364
- } catch (jwtErr) {
365
- throw jwtErr;
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
- throw err;
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 isGetRequest = (response.request && response.request.method === 'GET'),
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(isGetRequest && hasEntries && notEventStream);
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
- this.fetch = client.get.bind(client, href);
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
- this.options.qs[this.nextField] = this.nextValue;
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;
@@ -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)).map(x => trimSlashes(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.29.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.5.0",
36
- "http-status": "^1.1.0",
37
- "jsonwebtoken": "^8.2.1",
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.87.0",
40
+ "request": "^2.88.0",
41
41
  "url-template": "^2.0.8",
42
- "uuid": "^3.0.0"
42
+ "uuid": "^3.3.3"
43
43
  },
44
44
  "devDependencies": {
45
- "chai": "^4.1.1",
46
- "coveralls": "^3.0.2",
47
- "eslint": "^4.13.0",
48
- "eslint-plugin-node": "^6.0.0",
49
- "eslint-plugin-promise": "^3.6.0",
50
- "eslint-plugin-unicorn": "^4.0.2",
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.5.5",
52
+ "jsdoc": "^3.6.3",
53
53
  "jsonlint2": "^1.7.1",
54
- "leche": "^2.2.2",
55
- "mocha": "^5.0.1",
54
+ "leche": "^2.3.0",
55
+ "mocha": "^5.2.0",
56
56
  "mockery": "^2.1.0",
57
- "nock": "^9.0.13",
58
- "nyc": "^11.4.1",
59
- "shelljs": "^0.8.1",
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.2.4"
63
+ "sinon": "^7.5.0"
62
64
  },
63
65
  "files": [
64
66
  "config",
package/lib/.DS_Store DELETED
Binary file