@unboundcx/sdk 2.8.6 → 2.8.8

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.
@@ -213,8 +213,25 @@ export class StorageService {
213
213
  formFields,
214
214
  endpoint = '/storage/upload',
215
215
  method = 'POST',
216
+ onProgress = null,
217
+ skipClamscan = false,
216
218
  ) {
217
219
  const isNode = typeof window === 'undefined';
220
+
221
+ // In browser with progress callback: Use XMLHttpRequest
222
+ if (!isNode && onProgress && typeof onProgress === 'function') {
223
+ return this._performUploadWithProgress(
224
+ file,
225
+ fileName,
226
+ formFields,
227
+ endpoint,
228
+ method,
229
+ onProgress,
230
+ skipClamscan,
231
+ );
232
+ }
233
+
234
+ // Default behavior: Use fetch via sdk._fetch
218
235
  let formData, headers;
219
236
 
220
237
  if (isNode) {
@@ -227,6 +244,15 @@ export class StorageService {
227
244
  headers = result.headers;
228
245
  }
229
246
 
247
+ if (process?.env?.AUTH_V3_TOKEN_TYPE_OVERRIDE) {
248
+ headers['x-token-type-override'] =
249
+ process.env.AUTH_V3_TOKEN_TYPE_OVERRIDE;
250
+ }
251
+
252
+ if (skipClamscan && process?.env?.CLAMSCAN_OVERRIDE_KEY) {
253
+ headers['x-clamscan-override-key'] = process.env.CLAMSCAN_OVERRIDE_KEY;
254
+ }
255
+
230
256
  const params = {
231
257
  body: formData,
232
258
  headers,
@@ -235,6 +261,145 @@ export class StorageService {
235
261
  return await this.sdk._fetch(endpoint, method, params, true);
236
262
  }
237
263
 
264
+ // Upload with progress tracking using XMLHttpRequest
265
+ async _performUploadWithProgress(
266
+ file,
267
+ fileName,
268
+ formFields,
269
+ endpoint,
270
+ method,
271
+ onProgress,
272
+ skipClamscan = false,
273
+ ) {
274
+ const { formData } = this._createBrowserFormData(
275
+ file,
276
+ fileName,
277
+ formFields,
278
+ );
279
+
280
+ return new Promise((resolve, reject) => {
281
+ const xhr = new XMLHttpRequest();
282
+
283
+ const startTime = Date.now();
284
+
285
+ // Progress tracking
286
+ xhr.upload.onprogress = (event) => {
287
+ if (event.lengthComputable) {
288
+ const percentComplete = (event.loaded / event.total) * 100;
289
+ const elapsed = (Date.now() - startTime) / 1000; // seconds
290
+ const speed = event.loaded / elapsed; // bytes per second
291
+
292
+ onProgress({
293
+ loaded: event.loaded,
294
+ total: event.total,
295
+ percentage: percentComplete,
296
+ speed: speed, // bytes/sec
297
+ });
298
+ }
299
+ };
300
+
301
+ xhr.onload = () => {
302
+ if (xhr.status >= 200 && xhr.status < 300) {
303
+ try {
304
+ const response = JSON.parse(xhr.responseText);
305
+ resolve(response);
306
+ } catch (e) {
307
+ reject(new Error('Invalid JSON response'));
308
+ }
309
+ } else {
310
+ try {
311
+ const errorResponse = JSON.parse(xhr.responseText);
312
+ reject(errorResponse);
313
+ } catch (e) {
314
+ reject(new Error(`Upload failed with status ${xhr.status}`));
315
+ }
316
+ }
317
+ };
318
+
319
+ xhr.onerror = () => reject(new Error('Network error during upload'));
320
+ xhr.onabort = () => reject(new Error('Upload aborted'));
321
+
322
+ // Build URL with auth headers
323
+ const url = `${this.sdk.fullUrl}${endpoint}`;
324
+ xhr.open(method, url, true);
325
+
326
+ // IMPORTANT: Include credentials (cookies) for authentication
327
+ xhr.withCredentials = true;
328
+
329
+ // Add auth headers
330
+ if (this.sdk.token) {
331
+ xhr.setRequestHeader('Authorization', `Bearer ${this.sdk.token}`);
332
+ }
333
+ if (this.sdk.fwRequestId) {
334
+ xhr.setRequestHeader('x-request-id-fw', this.sdk.fwRequestId);
335
+ }
336
+ if (this.sdk.callId) {
337
+ xhr.setRequestHeader('x-call-id', this.sdk.callId);
338
+ }
339
+
340
+ // Add environment variable override headers
341
+ if (process?.env?.AUTH_V3_TOKEN_TYPE_OVERRIDE) {
342
+ xhr.setRequestHeader(
343
+ 'x-token-type-override',
344
+ process.env.AUTH_V3_TOKEN_TYPE_OVERRIDE,
345
+ );
346
+ }
347
+ if (skipClamscan && process?.env?.CLAMSCAN_OVERRIDE_KEY) {
348
+ xhr.setRequestHeader(
349
+ 'x-clamscan-override-key',
350
+ process.env.CLAMSCAN_OVERRIDE_KEY,
351
+ );
352
+ }
353
+
354
+ xhr.send(formData);
355
+ });
356
+ }
357
+ /*
358
+
359
+
360
+ Response:
361
+ {
362
+ "uploaded": [
363
+ {
364
+ "id": "017d0120251229hvdxjod4486582468133095",
365
+ "fileName": "sip-messages-20251223T195443.txt",
366
+ "fileSize": 18979,
367
+ "url": "https://masterc.api.dev-d01.app1svc.com/storage/017d0120251229hvdxjod4486582468133095.txt",
368
+ "mimeType": "text/plain",
369
+ "s3Regions": [
370
+ "d01",
371
+ "d03"
372
+ ],
373
+ "isPublic": false
374
+ }
375
+ ],
376
+ "viruses": [],
377
+ "errors": []
378
+ }
379
+
380
+ */
381
+ /**
382
+ * Upload a file to storage with optional format conversion
383
+ * @param {Object} config - Configuration object
384
+ * @param {Object} config.file - File content (Buffer or File) - REQUIRED
385
+ * @param {string} [config.classification='generic'] - File classification (e.g., 'fax', 'files', 'generic')
386
+ * @param {string} [config.folder] - Folder path for organizing files
387
+ * @param {string} [config.fileName] - Original file name
388
+ * @param {boolean} [config.isPublic=false] - Whether file is publicly accessible
389
+ * @param {string} [config.country='US'] - Country code for region selection
390
+ * @param {string} [config.expireAfter] - Expiration time
391
+ * @param {string} [config.relatedId] - Related object ID
392
+ * @param {boolean} [config.createAccessKey=false] - Generate an access key for the file
393
+ * @param {number} [config.accessKeyExpiresIn] - Access key expiration in seconds
394
+ * @param {string} [config.convertTo] - Convert uploaded file to this format before storing. Supported: 'pdf', 'tiff'. Input must be PDF, DOC, or DOCX.
395
+ * @param {Object} [config.convertOptions] - Options for file conversion (used with convertTo)
396
+ * @param {('fine'|'normal')} [config.convertOptions.resolution='fine'] - Fax resolution: 'fine' (204x196) or 'normal' (204x98)
397
+ * @param {('letter'|'a4')} [config.convertOptions.paperSize='letter'] - Paper size for conversion
398
+ * @param {('g4'|'g3')} [config.convertOptions.compression='g4'] - TIFF compression: 'g4' (default) or 'g3' for older fax machines
399
+ * @param {Function} [config.onProgress] - Progress callback for browser uploads
400
+ * @param {Object} [config._options] - Internal options
401
+ * @returns {Promise<Object>} Upload result with uploaded files, viruses, and errors
402
+ */
238
403
  async upload({
239
404
  classification = 'generic',
240
405
  folder,
@@ -246,6 +411,10 @@ export class StorageService {
246
411
  relatedId,
247
412
  createAccessKey = false,
248
413
  accessKeyExpiresIn,
414
+ convertTo,
415
+ convertOptions,
416
+ onProgress,
417
+ _options,
249
418
  }) {
250
419
  this.sdk.validateParams(
251
420
  {
@@ -259,6 +428,8 @@ export class StorageService {
259
428
  relatedId,
260
429
  createAccessKey,
261
430
  accessKeyExpiresIn,
431
+ convertTo,
432
+ convertOptions,
262
433
  },
263
434
  {
264
435
  classification: { type: 'string', required: false },
@@ -270,7 +441,9 @@ export class StorageService {
270
441
  expireAfter: { type: 'string', required: false },
271
442
  relatedId: { type: 'string', required: false },
272
443
  createAccessKey: { type: 'boolean', required: false },
273
- accessKeyExpiresIn: { type: 'string', required: false },
444
+ accessKeyExpiresIn: { type: 'number', required: false },
445
+ convertTo: { type: 'string', required: false },
446
+ convertOptions: { type: 'object', required: false },
274
447
  },
275
448
  );
276
449
 
@@ -287,12 +460,24 @@ export class StorageService {
287
460
  formFields.push(['createAccessKey', createAccessKey.toString()]);
288
461
  if (accessKeyExpiresIn)
289
462
  formFields.push(['accessKeyExpiresIn', accessKeyExpiresIn]);
463
+ if (convertTo) formFields.push(['convertTo', convertTo]);
464
+ if (convertOptions)
465
+ formFields.push(['convertOptions', JSON.stringify(convertOptions)]);
290
466
 
291
- return this._performUpload(file, fileName, formFields);
467
+ return this._performUpload(
468
+ file,
469
+ fileName,
470
+ formFields,
471
+ '/storage/upload',
472
+ 'POST',
473
+ onProgress,
474
+ _options?.skipScan,
475
+ );
292
476
  }
293
477
 
294
478
  async uploadFiles(files, options = {}) {
295
- const { classification, expireAfter, isPublic, metadata } = options;
479
+ const { classification, expireAfter, isPublic, metadata, _options } =
480
+ options;
296
481
 
297
482
  // Validate files parameter
298
483
  if (!files) {
@@ -392,6 +577,10 @@ export class StorageService {
392
577
  if (metadata) formData.append('metadata', JSON.stringify(metadata));
393
578
  }
394
579
 
580
+ if (_options?.skipScan && process?.env?.CLAMSCAN_OVERRIDE_KEY) {
581
+ headers['x-clamscan-override-key'] = process.env.CLAMSCAN_OVERRIDE_KEY;
582
+ }
583
+
395
584
  const params = {
396
585
  body: formData,
397
586
  headers,
@@ -467,13 +656,19 @@ export class StorageService {
467
656
  return result;
468
657
  }
469
658
 
470
- async uploadProfileImage({ file, classification = 'user_images', fileName }) {
659
+ async uploadProfileImage({
660
+ file,
661
+ classification = 'user_images',
662
+ fileName,
663
+ userId,
664
+ }) {
471
665
  this.sdk.validateParams(
472
- { file, classification },
666
+ { file, classification, userId },
473
667
  {
474
668
  file: { type: 'object', required: true },
475
669
  classification: { type: 'string', required: true },
476
670
  fileName: { type: 'string', required: false },
671
+ userId: { type: 'string', required: false },
477
672
  },
478
673
  );
479
674
 
@@ -488,6 +683,7 @@ export class StorageService {
488
683
  // Build form fields exactly like the regular upload but only include classification
489
684
  const formFields = [];
490
685
  formFields.push(['classification', classification]);
686
+ formFields.push(['userId', userId]);
491
687
 
492
688
  // Use the correct profile image endpoint with proper FormData
493
689
  return this._performUpload(
@@ -535,12 +731,14 @@ export class StorageService {
535
731
  }
536
732
 
537
733
  async listFiles(options = {}) {
538
- const { classification, limit, offset, orderBy, orderDirection } = options;
734
+ const { classification, folder, limit, offset, orderBy, orderDirection } =
735
+ options;
539
736
 
540
737
  // Validate optional parameters
541
738
  const validationSchema = {};
542
739
  if ('classification' in options)
543
740
  validationSchema.classification = { type: 'string' };
741
+ if ('folder' in options) validationSchema.folder = { type: 'string' };
544
742
  if ('limit' in options) validationSchema.limit = { type: 'number' };
545
743
  if ('offset' in options) validationSchema.offset = { type: 'number' };
546
744
  if ('orderBy' in options) validationSchema.orderBy = { type: 'string' };
@@ -724,6 +922,109 @@ export class StorageService {
724
922
  return result;
725
923
  }
726
924
 
925
+ /**
926
+ * Convert an existing stored file to a different format and save it as a new storage record.
927
+ * The original file remains unchanged. Supported source formats: PDF, DOC, DOCX.
928
+ * Supported output formats: 'pdf', 'tiff'.
929
+ *
930
+ * Fields not provided in the request (classification, folder, relatedId, isPublic)
931
+ * are inherited from the source file. If expireAfter is not provided, the expireAt
932
+ * timestamp is copied directly from the source file.
933
+ *
934
+ * @param {string} storageId - ID of the existing storage file to convert (required)
935
+ * @param {Object} config - Conversion options
936
+ * @param {string} config.convertTo - Target format: 'pdf' or 'tiff' (required)
937
+ * @param {Object} [config.convertOptions] - Options controlling the conversion output
938
+ * @param {('fine'|'normal')} [config.convertOptions.resolution='fine'] - Fax resolution: 'fine' (204x196 DPI) or 'normal' (204x98 DPI)
939
+ * @param {('letter'|'a4')} [config.convertOptions.paperSize='letter'] - Paper size for conversion
940
+ * @param {('g4'|'g3')} [config.convertOptions.compression='g4'] - TIFF compression: 'g4' (modern, default) or 'g3' (legacy fax machines)
941
+ * @param {string} [config.classification] - Storage classification for the new file. Defaults to source file's classification.
942
+ * @param {string} [config.folder] - Folder path for the new file. Defaults to source file's folder.
943
+ * @param {string} [config.relatedId] - Related object ID for the new file. Defaults to source file's relatedId.
944
+ * @param {boolean} [config.isPublic] - Whether the new file is publicly accessible. Defaults to source file's isPublic.
945
+ * @param {string} [config.expireAfter] - Expiration duration for the new file (e.g., '7d', '1h'). If omitted, copies expireAt from source.
946
+ * @param {boolean} [config.createAccessKey=false] - Generate a temporary access key for the new file
947
+ * @param {string} [config.accessKeyExpiresIn] - Access key expiration duration (e.g., '7d')
948
+ * @returns {Promise<Object>} The newly created storage record for the converted file
949
+ * @returns {string} result.id - Storage ID of the converted file
950
+ * @returns {string} result.sourceStorageId - Storage ID of the original source file
951
+ * @returns {string} result.fileName - File name of the converted file
952
+ * @returns {number} result.fileSize - Size in bytes of the converted file
953
+ * @returns {string} result.url - URL to access the converted file
954
+ * @returns {string} result.mimeType - MIME type of the converted file
955
+ * @returns {string[]} result.s3Regions - Regions where the converted file is stored
956
+ * @returns {boolean} result.isPublic - Whether the converted file is public
957
+ * @returns {string} [result.expireAt] - Expiration timestamp if set
958
+ * @returns {string} [result.accessKey] - Access key if createAccessKey was true
959
+ *
960
+ * @example
961
+ * // Convert a stored PDF to TIFF for fax transmission
962
+ * const result = await sdk.storage.convertFile('017d01...', {
963
+ * convertTo: 'tiff',
964
+ * convertOptions: {
965
+ * resolution: 'fine',
966
+ * paperSize: 'letter',
967
+ * compression: 'g4',
968
+ * },
969
+ * });
970
+ * console.log(result.id); // new storage ID for the TIFF
971
+ * console.log(result.sourceStorageId); // original PDF storage ID
972
+ *
973
+ * @example
974
+ * // Convert a DOCX to PDF, overriding the classification and folder
975
+ * const result = await sdk.storage.convertFile('017d02...', {
976
+ * convertTo: 'pdf',
977
+ * classification: 'fax',
978
+ * folder: 'outbound/2026',
979
+ * });
980
+ */
981
+ async convertFile(
982
+ storageId,
983
+ {
984
+ convertTo,
985
+ convertOptions,
986
+ classification,
987
+ folder,
988
+ relatedId,
989
+ isPublic,
990
+ expireAfter,
991
+ createAccessKey,
992
+ accessKeyExpiresIn,
993
+ } = {},
994
+ ) {
995
+ this.sdk.validateParams(
996
+ { storageId, convertTo },
997
+ {
998
+ storageId: { type: 'string', required: true },
999
+ convertTo: { type: 'string', required: true },
1000
+ convertOptions: { type: 'object', required: false },
1001
+ classification: { type: 'string', required: false },
1002
+ folder: { type: 'string', required: false },
1003
+ relatedId: { type: 'string', required: false },
1004
+ isPublic: { type: 'boolean', required: false },
1005
+ expireAfter: { type: 'string', required: false },
1006
+ createAccessKey: { type: 'boolean', required: false },
1007
+ accessKeyExpiresIn: { type: 'string', required: false },
1008
+ },
1009
+ );
1010
+
1011
+ const body = { convertTo };
1012
+ if (convertOptions !== undefined)
1013
+ body.convertOptions = convertOptions;
1014
+ if (classification !== undefined) body.classification = classification;
1015
+ if (folder !== undefined) body.folder = folder;
1016
+ if (relatedId !== undefined) body.relatedId = relatedId;
1017
+ if (isPublic !== undefined) body.isPublic = isPublic;
1018
+ if (expireAfter !== undefined) body.expireAfter = expireAfter;
1019
+ if (createAccessKey !== undefined) body.createAccessKey = createAccessKey;
1020
+ if (accessKeyExpiresIn !== undefined)
1021
+ body.accessKeyExpiresIn = accessKeyExpiresIn;
1022
+
1023
+ const params = { body };
1024
+
1025
+ return await this.sdk._fetch(`/storage/${storageId}/convert`, 'POST', params);
1026
+ }
1027
+
727
1028
  /**
728
1029
  * Update file contents and metadata
729
1030
  * @param {string} storageId - Storage file ID (required)
@@ -749,6 +1050,7 @@ export class StorageService {
749
1050
  country,
750
1051
  expireAfter,
751
1052
  relatedId,
1053
+ _options,
752
1054
  },
753
1055
  ) {
754
1056
  this.sdk.validateParams(
@@ -784,6 +1086,8 @@ export class StorageService {
784
1086
  formFields,
785
1087
  `/storage/${storageId}`,
786
1088
  'PUT',
1089
+ null,
1090
+ _options?.skipScan,
787
1091
  );
788
1092
  } else {
789
1093
  // If only updating metadata, use JSON request
@@ -0,0 +1,111 @@
1
+ export class MetricsService {
2
+ constructor(sdk) {
3
+ this.sdk = sdk;
4
+ }
5
+
6
+ /**
7
+ * Get current task router metrics
8
+ * Retrieves real-time metrics for task router queues, tasks, and workers.
9
+ * This provides insights into queue performance, wait times, task counts, and worker activity.
10
+ *
11
+ * @param {Object} params - Metric parameters
12
+ * @param {string} [params.period] - Time period for metrics calculation. Options: '5min', '15min', '30min', '1hour', '24hour'
13
+ * @param {string} [params.queueId] - Specific queue ID to filter metrics. If not provided, returns metrics for all queues
14
+ * @param {string} [params.metricType] - Type of metrics to retrieve: 'queue', 'task', 'worker', or 'all' (default: 'all')
15
+ * @param {number} [params.limit=100] - Maximum number of metric records to return (default: 100)
16
+ * @returns {Promise<Object>} Object containing the requested metrics
17
+ * @returns {Object} result.metrics - The metrics data organized by type
18
+ * @returns {Object} [result.metrics.queue] - Queue-level metrics (if metricType is 'queue' or 'all')
19
+ * @returns {number} result.metrics.queue.tasksWaiting - Number of tasks currently waiting in queue
20
+ * @returns {number} result.metrics.queue.tasksAssigned - Number of tasks currently assigned to workers
21
+ * @returns {number} result.metrics.queue.tasksConnected - Number of tasks currently connected/active
22
+ * @returns {number} result.metrics.queue.avgWaitTime - Average wait time in seconds for tasks in this period
23
+ * @returns {number} result.metrics.queue.longestWaitTime - Longest current wait time in seconds
24
+ * @returns {Object} [result.metrics.task] - Task-level metrics (if metricType is 'task' or 'all')
25
+ * @returns {number} result.metrics.task.created - Number of tasks created in this period
26
+ * @returns {number} result.metrics.task.completed - Number of tasks completed in this period
27
+ * @returns {number} result.metrics.task.abandoned - Number of tasks abandoned in this period
28
+ * @returns {Object} [result.metrics.worker] - Worker-level metrics (if metricType is 'worker' or 'all')
29
+ * @returns {number} result.metrics.worker.available - Number of workers currently available
30
+ * @returns {number} result.metrics.worker.busy - Number of workers currently busy with tasks
31
+ * @returns {number} result.metrics.worker.offline - Number of workers currently offline
32
+ * @returns {string} result.period - The time period used for calculations
33
+ * @returns {string} [result.queueId] - The queue ID if filtered to a specific queue
34
+ *
35
+ * @example
36
+ * // Get all current metrics for all queues
37
+ * const metrics = await sdk.taskRouter.metrics.getCurrent({
38
+ * period: '15min',
39
+ * metricType: 'all'
40
+ * });
41
+ * console.log(metrics.metrics.queue.tasksWaiting); // 5
42
+ * console.log(metrics.metrics.worker.available); // 12
43
+ *
44
+ * @example
45
+ * // Get queue-specific metrics for last 5 minutes
46
+ * const queueMetrics = await sdk.taskRouter.metrics.getCurrent({
47
+ * period: '5min',
48
+ * queueId: 'queue456',
49
+ * metricType: 'queue',
50
+ * limit: 50
51
+ * });
52
+ * console.log(queueMetrics.metrics.queue.avgWaitTime); // 45.3
53
+ *
54
+ * @example
55
+ * // Get worker metrics for last hour
56
+ * const workerMetrics = await sdk.taskRouter.metrics.getCurrent({
57
+ * period: '1hour',
58
+ * metricType: 'worker'
59
+ * });
60
+ * console.log(workerMetrics.metrics.worker.available); // 8
61
+ * console.log(workerMetrics.metrics.worker.busy); // 4
62
+ *
63
+ * @example
64
+ * // Get task completion metrics for last 24 hours
65
+ * const taskMetrics = await sdk.taskRouter.metrics.getCurrent({
66
+ * period: '24hour',
67
+ * metricType: 'task',
68
+ * limit: 200
69
+ * });
70
+ * console.log(taskMetrics.metrics.task.created); // 150
71
+ * console.log(taskMetrics.metrics.task.completed); // 142
72
+ */
73
+ async getCurrent(accountId, params = {}) {
74
+ const { period, queueId, metricType, limit = 100 } = params;
75
+
76
+ this.sdk.validateParams(
77
+ { period, queueId, metricType, limit },
78
+ {
79
+ period: { type: 'string', required: false },
80
+ queueId: { type: 'string', required: false },
81
+ metricType: { type: 'string', required: false },
82
+ limit: { type: 'number', required: false },
83
+ },
84
+ );
85
+
86
+ const requestParams = {
87
+ body: {
88
+ limit,
89
+ },
90
+ };
91
+
92
+ if (period !== undefined) {
93
+ requestParams.body.period = period;
94
+ }
95
+
96
+ if (queueId !== undefined) {
97
+ requestParams.body.queueId = queueId;
98
+ }
99
+
100
+ if (metricType !== undefined) {
101
+ requestParams.body.metricType = metricType;
102
+ }
103
+
104
+ const result = await this.sdk._fetch(
105
+ '/taskRouter/metrics/current',
106
+ 'GET',
107
+ requestParams,
108
+ );
109
+ return result;
110
+ }
111
+ }
@@ -0,0 +1,12 @@
1
+ import { WorkerService } from './WorkerService.js';
2
+ import { TaskService } from './TaskService.js';
3
+ import { MetricsService } from './MetricsService.js';
4
+
5
+ export class TaskRouterService {
6
+ constructor(sdk) {
7
+ this.sdk = sdk;
8
+ this.worker = new WorkerService(sdk);
9
+ this.task = new TaskService(sdk);
10
+ this.metrics = new MetricsService(sdk);
11
+ }
12
+ }