@unboundcx/sdk 4.13.27 → 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.27",
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;
@@ -288,5 +290,9 @@ export class WebchatService {
288
290
  this.sdk = sdk;
289
291
  this.widgets = new WebchatWidgetsService(sdk);
290
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);
291
297
  }
292
298
  }