helpdesk-app-framework-sdk 1.0.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/lib/client.js ADDED
@@ -0,0 +1,422 @@
1
+ /**
2
+ * BoldDesk App Framework - Client SDK
3
+ * Core client for cross-frame communication and data access
4
+ */
5
+
6
+ import { queryParameters, when, isObject, isString, isArray, isFunction, debounce, deepClone } from './utils'
7
+ import ContextAPI from './apis/context'
8
+ import MetadataAPI from './apis/metadata'
9
+ import { InstanceAPI } from './apis/instance'
10
+ import RequestAPI from './apis/request'
11
+ import NativePromise from 'native-promise-only'
12
+
13
+ const Promise = window.Promise || NativePromise
14
+
15
+ // Constants
16
+ const IFRAME_SESSION_TIMEOUT = 5 * 60 * 1000
17
+ const PROMISE_TIMEOUT = 10000
18
+ const PROMISE_TIMEOUT_LONG = 5 * 60 * 1000
19
+
20
+ class Client {
21
+ constructor (options = {}) {
22
+ this.origin = options.origin
23
+ this.appGuid = options.appGuid
24
+
25
+ // Initialize state
26
+ this._isReady = false
27
+ this._messageQueue = []
28
+ this._pendingRequests = {}
29
+ this._eventListeners = {}
30
+ this._requestId = 0
31
+ this._currentInstanceId = null
32
+
33
+ // Initialize APIs
34
+ this.context = new ContextAPI()
35
+ this.metadata = new MetadataAPI()
36
+ this.instance = new InstanceAPI(this)
37
+ this.request = new RequestAPI(this)
38
+
39
+ // Bind message handler
40
+ this._handleMessage = this._handleMessage.bind(this)
41
+ window.addEventListener('message', this._handleMessage)
42
+
43
+ // Register with host
44
+ this._registerWithHost()
45
+ }
46
+
47
+ /**
48
+ * Register app with BoldDesk host
49
+ * @private
50
+ */
51
+ _registerWithHost () {
52
+ this.postMessage('app.register', {
53
+ appGuid: this.appGuid,
54
+ version: '1.0.0'
55
+ })
56
+ }
57
+
58
+ /**
59
+ * Post message to host frame
60
+ * @param {string} action - Action name
61
+ * @param {*} data - Data payload
62
+ * @param {Function} callback - Optional callback
63
+ * @private
64
+ */
65
+ postMessage (action, data, callback) {
66
+ const requestId = ++this._requestId
67
+
68
+ if (callback && isFunction(callback)) {
69
+ this._pendingRequests[requestId] = callback
70
+ }
71
+
72
+ const message = {
73
+ type: 'baf.request',
74
+ action,
75
+ data,
76
+ requestId,
77
+ appGuid: this.appGuid,
78
+ timestamp: Date.now()
79
+ }
80
+
81
+ if (this._isReady) {
82
+ window.parent.postMessage(message, this.origin)
83
+ } else {
84
+ this._messageQueue.push(message)
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Handle incoming messages from host
90
+ * @private
91
+ */
92
+ _handleMessage (event) {
93
+ if (event.origin !== this.origin) return
94
+
95
+ const message = event.data
96
+ if (!message || message.type !== 'baf.response') return
97
+
98
+ // Handle app registration
99
+ if (message.action === 'app.registered') {
100
+ this._isReady = true
101
+ this._processMessageQueue()
102
+ this._handleAppRegistered(message.data)
103
+ return
104
+ }
105
+
106
+ // Handle pending request responses
107
+ if (message.requestId && this._pendingRequests[message.requestId]) {
108
+ const callback = this._pendingRequests[message.requestId]
109
+ delete this._pendingRequests[message.requestId]
110
+ callback(message.data)
111
+ return
112
+ }
113
+
114
+ // Handle events
115
+ if (message.action === 'event') {
116
+ this._handleEvent(message.eventName, message.data)
117
+ return
118
+ }
119
+
120
+ // Handle instance messages
121
+ if (message.action === 'instance.message') {
122
+ this.instance._triggerMessage(message.channel, message.payload)
123
+ return
124
+ }
125
+
126
+ // Handle instance registration
127
+ if (message.action === 'instance.registered') {
128
+ this.instance._registerInstance(message.data)
129
+ return
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Handle app registration from host
135
+ * @private
136
+ */
137
+ _handleAppRegistered (data) {
138
+ if (data.context) {
139
+ this.context._updateData(data.context)
140
+ }
141
+
142
+ if (data.metadata) {
143
+ this.metadata._updateData(data.metadata)
144
+ }
145
+
146
+ if (data.instanceId) {
147
+ this._currentInstanceId = data.instanceId
148
+ }
149
+
150
+ // Emit app.registered event
151
+ this._emitEvent('app.registered', {
152
+ context: this.context.getAll(),
153
+ metadata: this.metadata.getAll()
154
+ })
155
+ }
156
+
157
+ /**
158
+ * Process queued messages
159
+ * @private
160
+ */
161
+ _processMessageQueue () {
162
+ while (this._messageQueue.length > 0) {
163
+ const message = this._messageQueue.shift()
164
+ window.parent.postMessage(message, this.origin)
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Get data from platform context
170
+ * Retrieves data like ticket info, user info, etc.
171
+ *
172
+ * @param {string|Array} path - Data path or array of paths
173
+ * @returns {Promise} Resolves with requested data
174
+ *
175
+ * @example
176
+ * // Get single value
177
+ * sdk.get('ticket.subject').then(data => {
178
+ * console.log(data['ticket.subject']);
179
+ * });
180
+ *
181
+ * @example
182
+ * // Get multiple values
183
+ * sdk.get(['ticket.subject', 'ticket.requester.email']).then(data => {
184
+ * console.log(data['ticket.subject']);
185
+ * console.log(data['ticket.requester.email']);
186
+ * });
187
+ */
188
+ get (path) {
189
+ return new Promise((resolve, reject) => {
190
+ if (!path) {
191
+ return reject(new Error('Path is required for get() method'))
192
+ }
193
+
194
+ try {
195
+ const paths = isArray(path) ? path : [path]
196
+ this.postMessage('data.get', { paths }, (response) => {
197
+ if (response.error) {
198
+ reject(new Error(response.error))
199
+ } else {
200
+ resolve(response.data || {})
201
+ }
202
+ })
203
+ } catch (error) {
204
+ reject(error)
205
+ }
206
+ })
207
+ }
208
+
209
+ /**
210
+ * Set data in platform context
211
+ * Updates or stores data like ticket properties, custom fields, etc.
212
+ *
213
+ * @param {string} path - Data path to update
214
+ * @param {*} value - Value to set
215
+ * @returns {Promise} Resolves when data is set
216
+ *
217
+ * @example
218
+ * sdk.set('ticket.subject', 'Issue Resolved').then(() => {
219
+ * console.log('Subject updated');
220
+ * });
221
+ */
222
+ set (path, value) {
223
+ return new Promise((resolve, reject) => {
224
+ if (!path) {
225
+ return reject(new Error('Path is required for set() method'))
226
+ }
227
+
228
+ try {
229
+ this.postMessage('data.set', { path, value }, (response) => {
230
+ if (response.error) {
231
+ reject(new Error(response.error))
232
+ } else {
233
+ resolve()
234
+ }
235
+ })
236
+ } catch (error) {
237
+ reject(error)
238
+ }
239
+ })
240
+ }
241
+
242
+ /**
243
+ * Register event listener
244
+ * Listen for platform events like data changes, UI events, etc.
245
+ *
246
+ * @param {string} eventName - Event name to listen for
247
+ * @param {Function} callback - Handler function
248
+ * @returns {void}
249
+ *
250
+ * @example
251
+ * sdk.on('ticket.updated', (ticket) => {
252
+ * console.log('Ticket updated:', ticket);
253
+ * });
254
+ */
255
+ on (eventName, callback) {
256
+ if (!isString(eventName) || !isFunction(callback)) {
257
+ throw new Error('on() requires eventName (string) and callback (function)')
258
+ }
259
+
260
+ if (!this._eventListeners[eventName]) {
261
+ this._eventListeners[eventName] = []
262
+ this.postMessage('event.register', { eventName })
263
+ }
264
+
265
+ this._eventListeners[eventName].push(callback)
266
+ }
267
+
268
+ /**
269
+ * Unregister event listener
270
+ * Stop listening for platform events
271
+ *
272
+ * @param {string} eventName - Event name
273
+ * @param {Function} callback - Optional specific handler to remove
274
+ * @returns {void}
275
+ *
276
+ * @example
277
+ * sdk.off('ticket.updated');
278
+ */
279
+ off (eventName, callback) {
280
+ if (!this._eventListeners[eventName]) return
281
+
282
+ if (callback && isFunction(callback)) {
283
+ const index = this._eventListeners[eventName].indexOf(callback)
284
+ if (index > -1) {
285
+ this._eventListeners[eventName].splice(index, 1)
286
+ }
287
+ } else {
288
+ this._eventListeners[eventName] = []
289
+ }
290
+
291
+ // Unregister from host if no more listeners
292
+ if (this._eventListeners[eventName].length === 0) {
293
+ this.postMessage('event.unregister', { eventName })
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Invoke a platform action or method
299
+ * Execute platform-exposed methods like opening modals, etc.
300
+ *
301
+ * @param {string} methodName - Method name to invoke
302
+ * @param {*} payload - Optional data for the method
303
+ * @returns {Promise} Resolves with method result
304
+ *
305
+ * @example
306
+ * sdk.invoke('modal.open', {
307
+ * title: 'Contact Details',
308
+ * content: 'View full contact information'
309
+ * });
310
+ */
311
+ invoke (methodName, payload) {
312
+ return new Promise((resolve, reject) => {
313
+ if (!isString(methodName)) {
314
+ return reject(new Error('Method name is required'))
315
+ }
316
+
317
+ try {
318
+ this.postMessage('action.invoke', { methodName, payload }, (response) => {
319
+ if (response.error) {
320
+ reject(new Error(response.error))
321
+ } else {
322
+ resolve(response.result)
323
+ }
324
+ })
325
+ } catch (error) {
326
+ reject(error)
327
+ }
328
+ })
329
+ }
330
+
331
+ /**
332
+ * Trigger a custom event
333
+ * Emit custom events to the platform or other app components
334
+ *
335
+ * @param {string} eventName - Event name
336
+ * @param {*} data - Event data
337
+ * @returns {void}
338
+ *
339
+ * @example
340
+ * sdk.trigger('custom.notification', {
341
+ * message: 'Data saved successfully'
342
+ * });
343
+ */
344
+ trigger (eventName, data) {
345
+ if (!isString(eventName)) {
346
+ throw new Error('Event name must be a string')
347
+ }
348
+
349
+ this.postMessage('event.trigger', { eventName, data })
350
+
351
+ // Also emit locally
352
+ this._emitEvent(eventName, data)
353
+ }
354
+
355
+ /**
356
+ * Check if capability or value exists
357
+ * Verify availability of features or data paths
358
+ *
359
+ * @param {string} name - Capability or value name
360
+ * @returns {Promise} Resolves with boolean
361
+ *
362
+ * @example
363
+ * sdk.has('ticket.custom_field_123').then(exists => {
364
+ * if (exists) {
365
+ * console.log('Custom field is available');
366
+ * }
367
+ * });
368
+ */
369
+ has (name) {
370
+ return new Promise((resolve, reject) => {
371
+ if (!isString(name)) {
372
+ return reject(new Error('Name must be a string'))
373
+ }
374
+
375
+ try {
376
+ this.postMessage('capability.check', { name }, (response) => {
377
+ resolve(response.exists === true)
378
+ })
379
+ } catch (error) {
380
+ reject(error)
381
+ }
382
+ })
383
+ }
384
+
385
+ /**
386
+ * Internal: Emit event to local listeners
387
+ * @private
388
+ */
389
+ _emitEvent (eventName, data) {
390
+ if (this._eventListeners[eventName]) {
391
+ this._eventListeners[eventName].forEach(callback => {
392
+ try {
393
+ callback(data)
394
+ } catch (error) {
395
+ console.error(`Error in event listener for ${eventName}:`, error)
396
+ }
397
+ })
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Internal: Handle incoming event from host
403
+ * @private
404
+ */
405
+ _handleEvent (eventName, data) {
406
+ this._emitEvent(eventName, data)
407
+ }
408
+
409
+ /**
410
+ * Destroy client and clean up
411
+ * @returns {void}
412
+ */
413
+ destroy () {
414
+ window.removeEventListener('message', this._handleMessage)
415
+ this._eventListeners = {}
416
+ this._pendingRequests = {}
417
+ this._messageQueue = []
418
+ this._isReady = false
419
+ }
420
+ }
421
+
422
+ export default Client
package/lib/index.js ADDED
@@ -0,0 +1,99 @@
1
+ /**
2
+ * BoldDesk App Framework SDK
3
+ * Main entry point for the BAF SDK library
4
+ */
5
+
6
+ import Client from './client'
7
+ import { queryParameters } from './utils'
8
+
9
+ /**
10
+ * BAFClient - BoldDesk App Framework Client
11
+ *
12
+ * Main API for initializing and using the BoldDesk App Framework SDK
13
+ * in embedded app iframes.
14
+ *
15
+ * @example
16
+ * // Initialize the client
17
+ * const client = BAFClient.init((context) => {
18
+ * console.log('App initialized');
19
+ * console.log('Current module:', context.module);
20
+ * console.log('User:', context.user.name);
21
+ * });
22
+ *
23
+ * // Use the client APIs
24
+ * client.get('ticket.subject').then(data => {
25
+ * console.log(data['ticket.subject']);
26
+ * });
27
+ */
28
+
29
+ const BAFClient = {
30
+ /**
31
+ * Initialize BAFClient and establish connection with BoldDesk host
32
+ *
33
+ * Call this function in your app's HTML to initialize the SDK.
34
+ * The client will automatically detect the app instance and establish
35
+ * two-way communication with the BoldDesk platform.
36
+ *
37
+ * @param {Function} callback - Optional callback function called when app is registered
38
+ * Receives context object with app information
39
+ * @param {Object} loc - Optional window location object (for testing)
40
+ * @returns {Client|boolean} Client instance or false if not in iframe
41
+ *
42
+ * @example
43
+ * // Basic usage
44
+ * const client = BAFClient.init();
45
+ *
46
+ * @example
47
+ * // With callback
48
+ * const client = BAFClient.init((context) => {
49
+ * const currentUser = context.user;
50
+ * console.log('Hello ' + currentUser.name);
51
+ * });
52
+ */
53
+ init: function (callback, loc) {
54
+ loc = loc || window.location
55
+
56
+ // Parse query/hash parameters
57
+ const queryParams = queryParameters(loc.search)
58
+ const hashParams = queryParameters(loc.hash)
59
+
60
+ // Required parameters for iframe app
61
+ const origin = queryParams.origin || hashParams.origin
62
+ const appGuid = queryParams.app_guid || hashParams.app_guid
63
+
64
+ // Validate required parameters
65
+ if (!origin || !appGuid) {
66
+ console.error(
67
+ 'BAFClient.init(): Missing required parameters. ' +
68
+ 'App must be loaded with "origin" and "app_guid" parameters.'
69
+ )
70
+ return false
71
+ }
72
+
73
+ // Create client instance
74
+ const client = new Client({ origin, appGuid })
75
+
76
+ // Register callback for app registration event
77
+ if (typeof callback === 'function') {
78
+ client.on('app.registered', function (context) {
79
+ callback.call(client, context)
80
+ })
81
+ }
82
+
83
+ return client
84
+ }
85
+ }
86
+
87
+ export default BAFClient
88
+
89
+ // Also export Client for advanced usage
90
+ export { default as Client } from './client'
91
+
92
+ // Export utility functions
93
+ export * from './utils'
94
+
95
+ // Export APIs for direct access
96
+ export { default as ContextAPI } from './apis/context'
97
+ export { default as MetadataAPI } from './apis/metadata'
98
+ export { InstanceAPI } from './apis/instance'
99
+ export { default as RequestAPI } from './apis/request'
package/lib/utils.js ADDED
@@ -0,0 +1,202 @@
1
+ /**
2
+ * BoldDesk App Framework - Utility Functions
3
+ * Provides helper functions for common operations
4
+ */
5
+
6
+ /**
7
+ * Query string parser
8
+ * @param {string} queryString - Query string to parse (with or without ?)
9
+ * @returns {Object} Parsed parameters
10
+ */
11
+ export function queryParameters (queryString) {
12
+ const params = {}
13
+ if (!queryString) return params
14
+
15
+ const cleaned = queryString.replace(/^\?|^#/, '')
16
+ if (!cleaned) return params
17
+
18
+ cleaned.split('&').forEach((pair) => {
19
+ const [key, value] = pair.split('=')
20
+ if (key) {
21
+ params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : true
22
+ }
23
+ })
24
+
25
+ return params
26
+ }
27
+
28
+ /**
29
+ * Promise utility - when all conditions are met
30
+ * @param {Array} promises - Array of promises to wait for
31
+ * @returns {Promise}
32
+ */
33
+ export function when (promises) {
34
+ return Promise.all(promises)
35
+ }
36
+
37
+ /**
38
+ * Type checking utilities
39
+ */
40
+ export const isObject = (obj) => Object.prototype.toString.call(obj) === '[object Object]'
41
+ export const isString = (str) => typeof str === 'string'
42
+ export const isArray = (arr) => Array.isArray(arr)
43
+ export const isFunction = (fn) => typeof fn === 'function'
44
+ export const isUndefined = (val) => typeof val === 'undefined'
45
+ export const isNull = (val) => val === null
46
+
47
+ /**
48
+ * Debounce function - delays execution
49
+ * @param {Function} fn - Function to debounce
50
+ * @param {number} delay - Delay in milliseconds
51
+ * @returns {Function} Debounced function
52
+ */
53
+ export function debounce (fn, delay) {
54
+ let timeoutId
55
+ return function (...args) {
56
+ clearTimeout(timeoutId)
57
+ timeoutId = setTimeout(() => fn.apply(this, args), delay)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Throttle function - rate limits execution
63
+ * @param {Function} fn - Function to throttle
64
+ * @param {number} limit - Time limit in milliseconds
65
+ * @returns {Function} Throttled function
66
+ */
67
+ export function throttle (fn, limit) {
68
+ let inThrottle
69
+ return function (...args) {
70
+ if (!inThrottle) {
71
+ fn.apply(this, args)
72
+ inThrottle = true
73
+ setTimeout(() => (inThrottle = false), limit)
74
+ }
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Deep clone an object
80
+ * @param {*} obj - Object to clone
81
+ * @returns {*} Cloned object
82
+ */
83
+ export function deepClone (obj) {
84
+ if (obj === null || typeof obj !== 'object') return obj
85
+ if (obj instanceof Date) return new Date(obj.getTime())
86
+ if (obj instanceof Array) return obj.map(item => deepClone(item))
87
+ if (obj instanceof Object) {
88
+ const cloned = {}
89
+ for (const key in obj) {
90
+ if (obj.hasOwnProperty(key)) {
91
+ cloned[key] = deepClone(obj[key])
92
+ }
93
+ }
94
+ return cloned
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Merge objects deeply
100
+ * @param {Object} target - Target object
101
+ * @param {Object} source - Source object
102
+ * @returns {Object} Merged object
103
+ */
104
+ export function deepMerge (target, source) {
105
+ const result = { ...target }
106
+ for (const key in source) {
107
+ if (source.hasOwnProperty(key)) {
108
+ if (isObject(source[key]) && isObject(result[key])) {
109
+ result[key] = deepMerge(result[key], source[key])
110
+ } else {
111
+ result[key] = deepClone(source[key])
112
+ }
113
+ }
114
+ }
115
+ return result
116
+ }
117
+
118
+ /**
119
+ * UUID v4 generator
120
+ * @returns {string} Generated UUID
121
+ */
122
+ export function generateUUID () {
123
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
124
+ const r = (Math.random() * 16) | 0
125
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
126
+ return v.toString(16)
127
+ })
128
+ }
129
+
130
+ /**
131
+ * Validate manifest fields
132
+ * @param {Object} manifest - Manifest object to validate
133
+ * @returns {Object} Validation result { valid: boolean, errors: Array }
134
+ */
135
+ export function validateManifest (manifest) {
136
+ const errors = []
137
+
138
+ if (!manifest.name) errors.push('Manifest field "name" is required')
139
+ if (!manifest.version) errors.push('Manifest field "version" is required')
140
+ if (!manifest.frameworkVersion) errors.push('Manifest field "frameworkVersion" is required')
141
+ if (!manifest.product) errors.push('Manifest field "product" is required')
142
+
143
+ if (manifest.developer) {
144
+ const dev = manifest.developer
145
+ if (!dev.name) errors.push('Developer field "name" is required')
146
+ if (!dev.contactEmail) errors.push('Developer field "contactEmail" is required')
147
+ if (!dev.supportEmail) errors.push('Developer field "supportEmail" is required')
148
+ } else {
149
+ errors.push('Manifest field "developer" is required')
150
+ }
151
+
152
+ if (!Array.isArray(manifest.trustedDomains)) {
153
+ errors.push('Manifest field "trustedDomains" must be an array')
154
+ }
155
+
156
+ return {
157
+ valid: errors.length === 0,
158
+ errors
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Parse widget locations to ensure valid format
164
+ * @param {Array} locations - Widget locations from manifest
165
+ * @returns {Array} Parsed locations
166
+ */
167
+ export function parseWidgetLocations (locations) {
168
+ const validLocations = [
169
+ 'desk.menu.left',
170
+ 'desk.ticket.view.rightpanel',
171
+ 'desk.contact.view.rightpanel',
172
+ 'desk.contactgroup.view.rightpanel',
173
+ 'desk.chat.view.rightpanel',
174
+ 'desk.cti.widget'
175
+ ]
176
+
177
+ return locations.filter(loc => validLocations.includes(loc))
178
+ }
179
+
180
+ /**
181
+ * Validate URL format
182
+ * @param {string} url - URL to validate
183
+ * @returns {boolean} Is valid URL
184
+ */
185
+ export function isValidUrl (url) {
186
+ try {
187
+ new URL(url)
188
+ return true
189
+ } catch (e) {
190
+ return false
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Format error message for logging
196
+ * @param {string} context - Context of error
197
+ * @param {Error} error - Error object
198
+ * @returns {string} Formatted error message
199
+ */
200
+ export function formatError (context, error) {
201
+ return `[${context}] ${error.message || error}`
202
+ }