@unboundcx/sdk 4.13.26 → 4.13.28

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/index.js CHANGED
@@ -304,6 +304,7 @@ export {
304
304
  WebchatWidgetsService,
305
305
  WebchatWidgetKeysService,
306
306
  } from './services/webchat.js';
307
+ export { WebchatVisitorService } from './services/webchat/VisitorService.js';
307
308
  export { ExternalOAuthService } from './services/externalOAuth.js';
308
309
  export { GoogleCalendarService } from './services/googleCalendar.js';
309
310
  export { DriveService } from './services/drive.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unboundcx/sdk",
3
- "version": "4.13.26",
3
+ "version": "4.13.28",
4
4
  "description": "Official JavaScript SDK for the Unbound API - A comprehensive toolkit for integrating with Unbound's communication, AI, and data management services",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,359 @@
1
+ import { internalRequest } from '../../base.js';
2
+
3
+ // Public, unauthenticated visitor surface -- `sdk.webchat.visitor`.
4
+ //
5
+ // Mirrors the app1-webchat-embed app's own request shapes
6
+ // (app1-webchat-embed/src/lib/utils/webchatApi.js) exactly, so this is a
7
+ // drop-in for that fetch wrapper, not a parallel client with different
8
+ // semantics: same paths under bare /webchat/:widgetId/*, same `embedGrant`
9
+ // body field on session create, same Authorization-header-or-body/query
10
+ // `token` fallback the API controllers accept.
11
+ //
12
+ // No agent auth: the sdk instance backing this only needs `namespace` (or
13
+ // a custom `baseURL`) set at construction -- never `sdk.token`. Every
14
+ // method forces HTTP (esign.public precedent, base.js `forceFetch`) since
15
+ // these are one-shot fetches from a customer page or a custom UI, never
16
+ // NATS-transport traffic.
17
+ function authHeaders(token) {
18
+ return token ? { headers: { Authorization: `Bearer ${token}` } } : {};
19
+ }
20
+
21
+ class WebchatVisitorSessionService {
22
+ constructor(sdk) {
23
+ this.sdk = sdk;
24
+ }
25
+
26
+ /**
27
+ * Start a new conversation. Requires an `embedGrant` minted by
28
+ * `GET /webchat/:widgetId/grant` (or by loader.js for the JS-snippet
29
+ * path) -- a custom UI on an allowlisted origin calls `grant()` first.
30
+ * @param {Object} params
31
+ * @param {string} params.widgetId
32
+ * @param {string} params.embedGrant
33
+ * @param {Object} [params.visitorData] - `{...fields, hash}`; `hash` is the
34
+ * HMAC-SHA256 (identitySecret) over the sorted `key=value&...` fields
35
+ * string, matching verifyVisitorIdentity.js's canonicalization exactly.
36
+ * Omit `hash` (or the whole object) to send fully unverified metadata.
37
+ * @param {string} [params.pageUrl]
38
+ * @returns {Promise<{engagementSessionId:string, token:string, resumeToken:string, status:'created'}>}
39
+ */
40
+ async create({ widgetId, embedGrant, visitorData, pageUrl } = {}) {
41
+ this.sdk.validateParams(
42
+ { widgetId, embedGrant },
43
+ {
44
+ widgetId: { type: 'string', required: true },
45
+ embedGrant: { type: 'string', required: true },
46
+ },
47
+ );
48
+ return internalRequest(
49
+ this.sdk,
50
+ `/webchat/${widgetId}/session`,
51
+ 'POST',
52
+ { body: { embedGrant, visitorData, pageUrl } },
53
+ true,
54
+ );
55
+ }
56
+
57
+ /**
58
+ * Resume an existing conversation from a previously-issued resume token
59
+ * (same endpoint as `create`; the resumeToken body field selects the
60
+ * resume path server-side).
61
+ * @param {Object} params
62
+ * @param {string} params.widgetId
63
+ * @param {string} params.resumeToken
64
+ * @returns {Promise<{engagementSessionId:string, token:string, resumeToken:string, status:'resumed'}>}
65
+ */
66
+ async resume({ widgetId, resumeToken } = {}) {
67
+ this.sdk.validateParams(
68
+ { widgetId, resumeToken },
69
+ {
70
+ widgetId: { type: 'string', required: true },
71
+ resumeToken: { type: 'string', required: true },
72
+ },
73
+ );
74
+ return internalRequest(
75
+ this.sdk,
76
+ `/webchat/${widgetId}/session`,
77
+ 'POST',
78
+ { body: { resumeToken } },
79
+ true,
80
+ );
81
+ }
82
+
83
+ /**
84
+ * Visitor-initiated completion (idle-timeout path closes the same way,
85
+ * server-side).
86
+ * @param {Object} params
87
+ * @param {string} params.widgetId
88
+ * @param {string} params.token - Session JWT from create/resume.
89
+ * @returns {Promise<{status:'completed'}>}
90
+ */
91
+ async end({ widgetId, token } = {}) {
92
+ this.sdk.validateParams(
93
+ { widgetId, token },
94
+ {
95
+ widgetId: { type: 'string', required: true },
96
+ token: { type: 'string', required: true },
97
+ },
98
+ );
99
+ return internalRequest(
100
+ this.sdk,
101
+ `/webchat/${widgetId}/session/end`,
102
+ 'POST',
103
+ { ...authHeaders(token) },
104
+ true,
105
+ );
106
+ }
107
+ }
108
+
109
+ class WebchatVisitorMessagesService {
110
+ constructor(sdk) {
111
+ this.sdk = sdk;
112
+ }
113
+
114
+ /**
115
+ * History for the session's own conversation (session JWT scopes
116
+ * engagementSessionId server-side -- never pass one here).
117
+ * @param {Object} params
118
+ * @param {string} params.widgetId
119
+ * @param {string} params.token
120
+ * @param {string} [params.before] - Cursor: messages created before this.
121
+ * @param {number} [params.limit]
122
+ * @returns {Promise<{messages:Object[]}>}
123
+ */
124
+ async list({ widgetId, token, before, limit } = {}) {
125
+ this.sdk.validateParams(
126
+ { widgetId, token },
127
+ {
128
+ widgetId: { type: 'string', required: true },
129
+ token: { type: 'string', required: true },
130
+ },
131
+ );
132
+ return internalRequest(
133
+ this.sdk,
134
+ `/webchat/${widgetId}/messages`,
135
+ 'GET',
136
+ {
137
+ query: {
138
+ ...(before !== undefined && before !== null ? { before } : {}),
139
+ ...(limit !== undefined && limit !== null ? { limit } : {}),
140
+ },
141
+ ...authHeaders(token),
142
+ },
143
+ true,
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Send a visitor message (direction is always 'visitor' server-side).
149
+ * @param {Object} params
150
+ * @param {string} params.widgetId
151
+ * @param {string} params.token
152
+ * @param {string} params.message
153
+ * @param {Object[]} [params.media]
154
+ * @returns {Promise<{message:Object}>}
155
+ */
156
+ async send({ widgetId, token, message, media } = {}) {
157
+ this.sdk.validateParams(
158
+ { widgetId, token },
159
+ {
160
+ widgetId: { type: 'string', required: true },
161
+ token: { type: 'string', required: true },
162
+ },
163
+ );
164
+ return internalRequest(
165
+ this.sdk,
166
+ `/webchat/${widgetId}/messages`,
167
+ 'POST',
168
+ { body: { message, media }, ...authHeaders(token) },
169
+ true,
170
+ );
171
+ }
172
+ }
173
+
174
+ class WebchatVisitorFilesService {
175
+ constructor(sdk) {
176
+ this.sdk = sdk;
177
+ }
178
+
179
+ /**
180
+ * Upload a visitor file (streaming multer engine server-side; 403 when
181
+ * the widget has file upload disabled). Browser-only (needs `FormData`)
182
+ * -- unlike the embed app's own XHR upload, this has no progress event;
183
+ * a custom UI that needs a progress bar should keep using XHR directly
184
+ * against the same endpoint (see app1-webchat-embed/webchatApi.js) until
185
+ * the SDK grows an upload-progress hook.
186
+ * @param {Object} params
187
+ * @param {string} params.widgetId
188
+ * @param {string} params.token
189
+ * @param {File|Blob} params.file
190
+ * @returns {Promise<{file:Object, message:Object}>}
191
+ */
192
+ async upload({ widgetId, token, file } = {}) {
193
+ this.sdk.validateParams(
194
+ { widgetId, token },
195
+ {
196
+ widgetId: { type: 'string', required: true },
197
+ token: { type: 'string', required: true },
198
+ },
199
+ );
200
+ if (typeof FormData === 'undefined' || !file) {
201
+ throw new Error(
202
+ 'webchat.visitor.files.upload :: a browser File/Blob and FormData support are required',
203
+ );
204
+ }
205
+ const body = new FormData();
206
+ body.append('file', file);
207
+ return internalRequest(
208
+ this.sdk,
209
+ `/webchat/${widgetId}/files`,
210
+ 'POST',
211
+ { body, ...authHeaders(token) },
212
+ true,
213
+ );
214
+ }
215
+
216
+ /**
217
+ * Direct download URL for a previously-uploaded file (token as a query
218
+ * param -- iframe `<a>`/`<img>` tags can't set an Authorization header).
219
+ * Not a request -- returns the URL string to link/navigate to.
220
+ * @param {Object} params
221
+ * @param {string} params.widgetId
222
+ * @param {string} params.fileId
223
+ * @param {string} params.token
224
+ * @returns {string}
225
+ */
226
+ downloadUrl({ widgetId, fileId, token } = {}) {
227
+ this.sdk.validateParams(
228
+ { widgetId, fileId, token },
229
+ {
230
+ widgetId: { type: 'string', required: true },
231
+ fileId: { type: 'string', required: true },
232
+ token: { type: 'string', required: true },
233
+ },
234
+ );
235
+ return `${this.sdk.fullUrl || this.sdk.baseURL}/webchat/${widgetId}/files/${fileId}?token=${encodeURIComponent(token)}`;
236
+ }
237
+ }
238
+
239
+ export class WebchatVisitorService {
240
+ constructor(sdk) {
241
+ this.sdk = sdk;
242
+ this.session = new WebchatVisitorSessionService(sdk);
243
+ this.messages = new WebchatVisitorMessagesService(sdk);
244
+ this.files = new WebchatVisitorFilesService(sdk);
245
+ }
246
+
247
+ /**
248
+ * Mint a short-TTL embedGrant for this page's real origin (server
249
+ * validates Origin/Referer against the widget's domainAllowlist) -- a
250
+ * custom UI calls this before `session.create`, same as loader.js does
251
+ * at iframe-open time.
252
+ * @param {string} widgetId
253
+ * @returns {Promise<{grant:string, exp:number}>}
254
+ */
255
+ async grant(widgetId) {
256
+ this.sdk.validateParams(
257
+ { widgetId },
258
+ { widgetId: { type: 'string', required: true } },
259
+ );
260
+ return internalRequest(this.sdk, `/webchat/${widgetId}/grant`, 'GET', {}, true);
261
+ }
262
+
263
+ /**
264
+ * Hours/offline state (server-evaluated, `no-store`).
265
+ * @param {string} widgetId
266
+ * @returns {Promise<{open:boolean, offlineBehavior:string, offlineFormConfig?:Object}>}
267
+ */
268
+ async status(widgetId) {
269
+ this.sdk.validateParams(
270
+ { widgetId },
271
+ { widgetId: { type: 'string', required: true } },
272
+ );
273
+ return internalRequest(this.sdk, `/webchat/${widgetId}/status`, 'GET', {}, true);
274
+ }
275
+
276
+ /**
277
+ * Opt in (or out) of a transcript email for this conversation. Only
278
+ * stores the choice -- the email is sent at conversation close.
279
+ * @param {Object} params
280
+ * @param {string} params.widgetId
281
+ * @param {string} params.token
282
+ * @param {boolean} params.optIn
283
+ * @param {string} [params.email] - Required when `optIn` is true.
284
+ * @returns {Promise<{optIn:boolean, email:?string}>}
285
+ */
286
+ async transcript({ widgetId, token, optIn, email } = {}) {
287
+ this.sdk.validateParams(
288
+ { widgetId, token, optIn },
289
+ {
290
+ widgetId: { type: 'string', required: true },
291
+ token: { type: 'string', required: true },
292
+ optIn: { type: 'boolean', required: true },
293
+ },
294
+ );
295
+ return internalRequest(
296
+ this.sdk,
297
+ `/webchat/${widgetId}/transcript`,
298
+ 'POST',
299
+ { body: { optIn, email }, ...authHeaders(token) },
300
+ true,
301
+ );
302
+ }
303
+
304
+ /**
305
+ * Post-session-start HMAC identity verification (plan §5/P7 identify
306
+ * pin). Same canonicalization as `session.create`'s `visitorData.hash`:
307
+ * HMAC-SHA256(sorted `key=value&...` over every field except `hash`,
308
+ * identitySecret). Verified fields merge into the conversation's
309
+ * visitorData and (best-effort) link peopleId; an unsigned/mismatched
310
+ * hash is rejected outright, nothing stored.
311
+ * @param {Object} params
312
+ * @param {string} params.widgetId
313
+ * @param {string} params.token - Session JWT.
314
+ * @param {string} [params.userId]
315
+ * @param {string} [params.email]
316
+ * @param {string} [params.name]
317
+ * @param {string} [params.phone]
318
+ * @param {Object} [params.custom]
319
+ * @param {string} params.hash
320
+ * @returns {Promise<Object>}
321
+ */
322
+ async identify({ widgetId, token, hash, ...fields } = {}) {
323
+ this.sdk.validateParams(
324
+ { widgetId, token, hash },
325
+ {
326
+ widgetId: { type: 'string', required: true },
327
+ token: { type: 'string', required: true },
328
+ hash: { type: 'string', required: true },
329
+ },
330
+ );
331
+ return internalRequest(
332
+ this.sdk,
333
+ `/webchat/${widgetId}/identify`,
334
+ 'POST',
335
+ { body: { ...fields, hash }, ...authHeaders(token) },
336
+ true,
337
+ );
338
+ }
339
+
340
+ /**
341
+ * Emit a typing signal over an already-connected `/webchat` namespace
342
+ * socket (`webchatSocket.js`/`connectWebchatSocket` precedent -- handshake
343
+ * `auth:{namespace:'webchat', widgetId, token}`). The SDK owns no socket
344
+ * transport (liveQuery.js precedent): pass an already-connected
345
+ * socket.io-client instance in, this is a thin `emit` wrapper, not a
346
+ * connection manager.
347
+ * @param {Object} params
348
+ * @param {Object} params.socket - Connected socket.io-client instance.
349
+ * @param {boolean} [params.isTyping=true]
350
+ */
351
+ typing({ socket, isTyping = true } = {}) {
352
+ if (!socket || typeof socket.emit !== 'function') {
353
+ throw new Error(
354
+ 'webchat.visitor.typing :: a connected socket.io-client instance is required (pass { socket })',
355
+ );
356
+ }
357
+ socket.emit('webchat.typing', { isTyping });
358
+ }
359
+ }
@@ -1,8 +1,10 @@
1
1
  import { internalRequest } from '../base.js';
2
+ import { WebchatVisitorService } from './webchat/VisitorService.js';
2
3
 
3
4
  // WebChat P0: agent-facing widget CRUD. Endpoints live under
4
5
  // /webchat/widgets/ (checkApiAuth) -- deliberately not bare /webchat/, which
5
- // is reserved for the future unauthenticated visitor surface (plan §5).
6
+ // is the (now-implemented, P7) unauthenticated visitor surface -- see
7
+ // `sdk.webchat.visitor` (./webchat/VisitorService.js) (plan §5/§10 Q7).
6
8
  export class WebchatWidgetKeysService {
7
9
  constructor(sdk) {
8
10
  this.sdk = sdk;
@@ -236,6 +238,51 @@ export class WebchatConversationsService {
236
238
  this.sdk = sdk;
237
239
  this.messages = new WebchatConversationMessagesService(sdk);
238
240
  }
241
+
242
+ /**
243
+ * Send a typing indicator into a webchat conversation as an agent.
244
+ * @param {string} widgetId
245
+ * @param {string} engagementSessionId
246
+ * @param {boolean} isTyping
247
+ * @returns {Promise<Object>}
248
+ */
249
+ async typing(widgetId, engagementSessionId, isTyping) {
250
+ this.sdk.validateParams(
251
+ { widgetId, engagementSessionId },
252
+ {
253
+ widgetId: { type: 'string', required: true },
254
+ engagementSessionId: { type: 'string', required: true },
255
+ },
256
+ );
257
+
258
+ const params = { body: { isTyping: isTyping !== false } };
259
+
260
+ return internalRequest(
261
+ this.sdk,
262
+ `/webchat/widgets/${widgetId}/conversations/${engagementSessionId}/typing`,
263
+ 'POST',
264
+ params,
265
+ );
266
+ }
267
+
268
+ /**
269
+ * Look up a webchat conversation by engagementSessionId -- resolves the
270
+ * widgetId + verified-visitor badge data for the agent thread.
271
+ * @param {string} engagementSessionId
272
+ * @returns {Promise<Object>}
273
+ */
274
+ async get(engagementSessionId) {
275
+ this.sdk.validateParams(
276
+ { engagementSessionId },
277
+ { engagementSessionId: { type: 'string', required: true } },
278
+ );
279
+
280
+ return internalRequest(
281
+ this.sdk,
282
+ `/webchat/widgets/conversations/${engagementSessionId}`,
283
+ 'GET',
284
+ );
285
+ }
239
286
  }
240
287
 
241
288
  export class WebchatService {
@@ -243,5 +290,9 @@ export class WebchatService {
243
290
  this.sdk = sdk;
244
291
  this.widgets = new WebchatWidgetsService(sdk);
245
292
  this.conversations = new WebchatConversationsService(sdk);
293
+ // Public visitor surface (plan §10 Q7, esign.public pattern) -- no
294
+ // agent token required; a custom-UI integration constructs its own sdk
295
+ // instance with just `{namespace}` and uses only this namespace.
296
+ this.visitor = new WebchatVisitorService(sdk);
246
297
  }
247
298
  }