@unboundcx/sdk 4.13.94 → 4.13.96

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.
@@ -409,6 +409,64 @@ export class TaskService {
409
409
  return result;
410
410
  }
411
411
 
412
+ /**
413
+ * Unassign a task back to pending (worker unlinked). Used by CC bot
414
+ * drain for short-lived connected tasks — not park.
415
+ *
416
+ * @param {Object} options
417
+ * @param {string} options.taskId
418
+ * @returns {Promise<Object>}
419
+ */
420
+ async unassign(options = {}) {
421
+ const { taskId, userId } = options;
422
+
423
+ this.sdk.validateParams(
424
+ { taskId, userId },
425
+ {
426
+ taskId: { type: 'string', required: true },
427
+ userId: { type: 'string', required: false },
428
+ },
429
+ );
430
+
431
+ const params = { body: { taskId } };
432
+ if (userId) params.body.userId = userId;
433
+
434
+ return internalRequest(
435
+ this.sdk,
436
+ '/taskRouter/tasks/unassign',
437
+ 'PUT',
438
+ params,
439
+ );
440
+ }
441
+
442
+ /**
443
+ * Staff-only internal note on a task (webchat/SMS/voice feed, or
444
+ * timeline). Never sent to the customer.
445
+ *
446
+ * @param {Object} options
447
+ * @param {string} options.taskId
448
+ * @param {string} options.message
449
+ * @returns {Promise<Object>}
450
+ */
451
+ async note(options = {}) {
452
+ const { taskId, message } = options;
453
+
454
+ this.sdk.validateParams(
455
+ { taskId, message },
456
+ {
457
+ taskId: { type: 'string', required: true },
458
+ message: { type: 'string', required: true },
459
+ },
460
+ );
461
+
462
+ return internalRequest(
463
+ this.sdk,
464
+ `/taskRouter/tasks/${taskId}/notes`,
465
+ 'POST',
466
+ { body: { message } },
467
+ );
468
+ }
469
+
412
470
  /**
413
471
  * Change task priority
414
472
  * Modify the priority of a task to increase or decrease its routing priority.
@@ -730,12 +788,13 @@ export class TaskService {
730
788
  * console.log(result.taskId); // "task456"
731
789
  */
732
790
  async complete(options = {}) {
733
- const { taskId } = options;
791
+ const { taskId, completedReason } = options;
734
792
 
735
793
  this.sdk.validateParams(
736
- { taskId },
794
+ { taskId, completedReason },
737
795
  {
738
796
  taskId: { type: 'string', required: true },
797
+ completedReason: { type: 'string', required: false },
739
798
  },
740
799
  );
741
800
 
@@ -745,6 +804,10 @@ export class TaskService {
745
804
  },
746
805
  };
747
806
 
807
+ if (completedReason) {
808
+ params.body.completedReason = completedReason;
809
+ }
810
+
748
811
  const result = await internalRequest(this.sdk,
749
812
  '/taskRouter/tasks/complete',
750
813
  'PUT',
@@ -14,16 +14,6 @@ import { internalRequest } from '../../base.js';
14
14
  // method forces HTTP (esign.public precedent, base.js `forceFetch`) since
15
15
  // these are one-shot fetches from a customer page or a custom UI, never
16
16
  // NATS-transport traffic.
17
- //
18
- // Every call also passes `credentials: 'omit'` (base.js `_httpRequest`
19
- // opt-in), matching app1-webchat-embed's own webchatApi.js fetch wrapper
20
- // (which already sets `credentials:'omit'` for the same reason). This
21
- // surface is meant for a custom UI on a third-party origin calling the API
22
- // directly (see `grant()` below), not just the embed iframe -- and
23
- // app1-api's CORS only sends `access-control-allow-credentials` for
24
- // trusted app1 base domains, so a browser rejects the response for a
25
- // `credentials:'include'` request from any other origin even though auth
26
- // here is Authorization-header/body-token based, not cookie based.
27
17
  function authHeaders(token) {
28
18
  return token ? { headers: { Authorization: `Bearer ${token}` } } : {};
29
19
  }
@@ -74,7 +64,6 @@ class WebchatVisitorSessionService {
74
64
  trackingId,
75
65
  initialMessage,
76
66
  },
77
- credentials: 'omit',
78
67
  },
79
68
  true,
80
69
  );
@@ -100,7 +89,7 @@ class WebchatVisitorSessionService {
100
89
  this.sdk,
101
90
  `/webchat/${widgetId}/visitor-profile`,
102
91
  'POST',
103
- { body: { embedGrant, trackingId }, credentials: 'omit' },
92
+ { body: { embedGrant, trackingId } },
104
93
  true,
105
94
  );
106
95
  }
@@ -126,7 +115,7 @@ class WebchatVisitorSessionService {
126
115
  this.sdk,
127
116
  `/webchat/${widgetId}/session`,
128
117
  'POST',
129
- { body: { resumeToken }, credentials: 'omit' },
118
+ { body: { resumeToken } },
130
119
  true,
131
120
  );
132
121
  }
@@ -151,7 +140,7 @@ class WebchatVisitorSessionService {
151
140
  this.sdk,
152
141
  `/webchat/${widgetId}/session/end`,
153
142
  'POST',
154
- { ...authHeaders(token), credentials: 'omit' },
143
+ { ...authHeaders(token) },
155
144
  true,
156
145
  );
157
146
  }
@@ -190,7 +179,6 @@ class WebchatVisitorMessagesService {
190
179
  ...(limit !== undefined && limit !== null ? { limit } : {}),
191
180
  },
192
181
  ...authHeaders(token),
193
- credentials: 'omit',
194
182
  },
195
183
  true,
196
184
  );
@@ -217,7 +205,7 @@ class WebchatVisitorMessagesService {
217
205
  this.sdk,
218
206
  `/webchat/${widgetId}/messages`,
219
207
  'POST',
220
- { body: { message, media }, ...authHeaders(token), credentials: 'omit' },
208
+ { body: { message, media }, ...authHeaders(token) },
221
209
  true,
222
210
  );
223
211
  }
@@ -260,7 +248,7 @@ class WebchatVisitorFilesService {
260
248
  this.sdk,
261
249
  `/webchat/${widgetId}/files`,
262
250
  'POST',
263
- { body, ...authHeaders(token), credentials: 'omit' },
251
+ { body, ...authHeaders(token) },
264
252
  true,
265
253
  );
266
254
  }
@@ -309,13 +297,7 @@ export class WebchatVisitorService {
309
297
  { widgetId },
310
298
  { widgetId: { type: 'string', required: true } },
311
299
  );
312
- return internalRequest(
313
- this.sdk,
314
- `/webchat/${widgetId}/grant`,
315
- 'GET',
316
- { credentials: 'omit' },
317
- true,
318
- );
300
+ return internalRequest(this.sdk, `/webchat/${widgetId}/grant`, 'GET', {}, true);
319
301
  }
320
302
 
321
303
  /**
@@ -328,13 +310,7 @@ export class WebchatVisitorService {
328
310
  { widgetId },
329
311
  { widgetId: { type: 'string', required: true } },
330
312
  );
331
- return internalRequest(
332
- this.sdk,
333
- `/webchat/${widgetId}/status`,
334
- 'GET',
335
- { credentials: 'omit' },
336
- true,
337
- );
313
+ return internalRequest(this.sdk, `/webchat/${widgetId}/status`, 'GET', {}, true);
338
314
  }
339
315
 
340
316
  /**
@@ -360,7 +336,7 @@ export class WebchatVisitorService {
360
336
  this.sdk,
361
337
  `/webchat/${widgetId}/transcript`,
362
338
  'POST',
363
- { body: { optIn, email }, ...authHeaders(token), credentials: 'omit' },
339
+ { body: { optIn, email }, ...authHeaders(token) },
364
340
  true,
365
341
  );
366
342
  }
@@ -396,7 +372,7 @@ export class WebchatVisitorService {
396
372
  this.sdk,
397
373
  `/webchat/${widgetId}/identify`,
398
374
  'POST',
399
- { body: { ...fields, hash }, ...authHeaders(token), credentials: 'omit' },
375
+ { body: { ...fields, hash }, ...authHeaders(token) },
400
376
  true,
401
377
  );
402
378
  }
@@ -281,4 +281,98 @@ export class WebchatService {
281
281
  // instance with just `{namespace}` and uses only this namespace.
282
282
  this.visitor = new WebchatVisitorService(sdk);
283
283
  }
284
+
285
+ /**
286
+ * Send an agent webchat message on a task. The server resolves the
287
+ * widget + engagement from the task — callers must not UOQL
288
+ * webchatConversations for widgetId.
289
+ *
290
+ * @param {Object} options
291
+ * @param {string} options.taskId
292
+ * @param {string} [options.message]
293
+ * @param {Object} [options.media]
294
+ * @param {Object|Array} [options.card]
295
+ * @returns {Promise<Object>}
296
+ */
297
+ async sendOnTask(options = {}) {
298
+ const { taskId, message, media, card } = options;
299
+
300
+ this.sdk.validateParams(
301
+ { taskId, message, media, card },
302
+ {
303
+ taskId: { type: 'string', required: true },
304
+ message: { type: 'string', required: false },
305
+ media: { type: 'object', required: false },
306
+ card: { type: 'object', required: false },
307
+ },
308
+ );
309
+
310
+ const body = {};
311
+ if (message !== undefined) body.message = message;
312
+ if (media !== undefined) body.media = media;
313
+ if (card !== undefined) body.card = card;
314
+
315
+ return internalRequest(
316
+ this.sdk,
317
+ `/webchat/tasks/${taskId}/messages`,
318
+ 'POST',
319
+ { body },
320
+ );
321
+ }
322
+
323
+ /**
324
+ * Agent/bot typing indicator on a task's webchat. Server resolves the
325
+ * engagement from the task — callers must not UOQL widgetId.
326
+ *
327
+ * @param {Object} options
328
+ * @param {string} options.taskId
329
+ * @param {boolean} options.isTyping
330
+ * @returns {Promise<Object>}
331
+ */
332
+ async typingOnTask(options = {}) {
333
+ const { taskId, isTyping } = options;
334
+
335
+ this.sdk.validateParams(
336
+ { taskId, isTyping },
337
+ {
338
+ taskId: { type: 'string', required: true },
339
+ isTyping: { type: 'boolean', required: true },
340
+ },
341
+ );
342
+
343
+ return internalRequest(
344
+ this.sdk,
345
+ `/webchat/tasks/${taskId}/typing`,
346
+ 'POST',
347
+ { body: { isTyping } },
348
+ );
349
+ }
350
+
351
+ /**
352
+ * Staff-only system note on a task's webchat feed (visibility=internal).
353
+ * Not sent to the visitor. Server resolves engagement from the task.
354
+ *
355
+ * @param {Object} options
356
+ * @param {string} options.taskId
357
+ * @param {string} options.message
358
+ * @returns {Promise<Object>}
359
+ */
360
+ async noteOnTask(options = {}) {
361
+ const { taskId, message } = options;
362
+
363
+ this.sdk.validateParams(
364
+ { taskId, message },
365
+ {
366
+ taskId: { type: 'string', required: true },
367
+ message: { type: 'string', required: true },
368
+ },
369
+ );
370
+
371
+ return internalRequest(
372
+ this.sdk,
373
+ `/webchat/tasks/${taskId}/notes`,
374
+ 'POST',
375
+ { body: { message } },
376
+ );
377
+ }
284
378
  }
@@ -1,35 +0,0 @@
1
- import { internalRequest } from '../../base.js';
2
-
3
- // Forms v2 P8 (plans/forms-v2-plan.md D13, forms-v2-precheck.md §9) --
4
- // agent-authenticated Health tab data (`sdk.forms.health`): submissions
5
- // tiles, a 30-day submissions-per-day sparkline, per-field fill rate, and
6
- // drift alerts (a field with 0 non-blank values in the last 7 days after
7
- // previously being filled >=50% of the time). Backed by the nightly
8
- // `formFieldStats` rollup -- server route:
9
- // `app1-api/src/services/forms/controllers/getFormHealth.js`.
10
- export class FormsHealthService {
11
- constructor(sdk) {
12
- this.sdk = sdk;
13
- }
14
-
15
- /**
16
- * @param {string} formId
17
- * @returns {Promise<{
18
- * tiles: {submissions:number, submissionsChangePct:number|null,
19
- * spamCount:number, spamRatePct:number, companyLinkedPct:number,
20
- * companyCreated:number, actionsFailed:number},
21
- * sparkline: {day:string, submitted:number}[],
22
- * fields: {fieldKey:string, label:string, fillRate:number|null,
23
- * priorFillRate:number|null, isDrifting:boolean}[],
24
- * driftAlerts: object[],
25
- * windowDays: number,
26
- * }>}
27
- */
28
- async get(formId) {
29
- return internalRequest(
30
- this.sdk,
31
- `/forms/${encodeURIComponent(formId)}/health`,
32
- 'GET',
33
- );
34
- }
35
- }
@@ -1,110 +0,0 @@
1
- import { internalRequest } from '../../base.js';
2
-
3
- // Public, unauthenticated forms surface -- `sdk.forms.public`. Mirrors
4
- // WebchatVisitorService.js: the sdk instance backing this only needs
5
- // `namespace` (or a custom `baseURL`) set at construction, never
6
- // `sdk.token`. POSTs `forceFetch` (HTTP-only, no NATS transport) since
7
- // these are one-shot fetches from a customer page / marketing site, same
8
- // as every other visitor-facing call.
9
- //
10
- // Every call passes `credentials: 'omit'` (base.js `_httpRequest` opt-in) --
11
- // this surface is hit from third-party origins (a marketing site embedding
12
- // a form, not an app1 base domain), and app1-api's CORS
13
- // (`src/index.js` allowlist) only sends `access-control-allow-credentials`
14
- // for trusted app1 base domains. A browser rejects the whole response for
15
- // a `credentials:'include'` request when that header is missing, even
16
- // though these endpoints don't use cookies for auth -- so third-party
17
- // callers must opt out explicitly.
18
- //
19
- // Wire shape matches `POST /f/:publicKey` (app1-api webhooks service,
20
- // forms-v2-precheck.md §4.1): plain form fields go at the top level of the
21
- // body (fieldKey -> value, arrays allowed), and everything the server
22
- // treats as a *control* field (not a form value) is sent with a leading
23
- // underscore, e.g. `_idempotencyKey`/`_captchaToken` below --
24
- // formSubmitPublicKey.js splits on that same convention. `context` is NOT a
25
- // control field: app1-api's captureContext.js builds the submission's
26
- // context column by scanning the body's TOP-LEVEL keys against an
27
- // allowlist (utm_*, gclid, fbclid, referrer, landingUrl, pageUrl,
28
- // userAgent) and explicitly skips any `_`-prefixed key as a control
29
- // field -- so `context` values are spread onto the top level alongside
30
- // `fields`, never nested, or the server drops them silently.
31
- export class FormsPublicService {
32
- constructor(sdk) {
33
- this.sdk = sdk;
34
- }
35
-
36
- /**
37
- * Submit a public form by its publicKey (D1 -- every form has its own
38
- * publicKey; no tracking-code/_token needed -- publicKey + origin/
39
- * domainAllowlist is the only auth for a public form submit).
40
- * @param {string} publicKey
41
- * @param {Object} fields - fieldKey -> value (arrays kept as arrays).
42
- * @param {Object} [options]
43
- * @param {Object} [options.context] - Client-observed context (utm/referrer/
44
- * landingUrl/etc, D12) to merge with what the server infers from the
45
- * request itself -- spread onto the top-level body (never nested);
46
- * the server's captureContext.js allowlist only reads top-level keys.
47
- * A key here that collides with a `fields` key loses to `fields`.
48
- * @param {string} [options.captchaToken] - Turnstile/etc response token
49
- * (D10); sent as `_captchaToken`. Ignored server-side until a captcha
50
- * provider is configured for the form/account.
51
- * @param {string} [options.idempotencyKey] - Per-render dedupe token
52
- * (D10); sent as `_idempotencyKey`.
53
- * @returns {Promise<{ok:boolean, message?:string}>}
54
- */
55
- async submit(publicKey, fields, { context, captchaToken, idempotencyKey } = {}) {
56
- this.sdk.validateParams(
57
- { publicKey },
58
- { publicKey: { type: 'string', required: true } },
59
- );
60
- const body = { ...(context || {}), ...(fields || {}) };
61
- if (idempotencyKey) body._idempotencyKey = idempotencyKey;
62
- if (captchaToken) body._captchaToken = captchaToken;
63
-
64
- return internalRequest(
65
- this.sdk,
66
- `/f/${publicKey}`,
67
- 'POST',
68
- { body, credentials: 'omit' },
69
- true,
70
- );
71
- }
72
-
73
- /**
74
- * Upload a file for a `file` inputType field (D21), ahead of the real
75
- * `submit()` call. Mirrors `webchat.visitor.files.upload()`'s shape:
76
- * browser-only (needs `FormData`), no progress event. The returned
77
- * `token` must be echoed back inside `submit()`'s `fields[fieldKey]` as
78
- * `JSON.stringify({fileId, token})` -- the server (formFileAttach.js)
79
- * verifies it names this exact (formId, fieldKey, fileId) triple before
80
- * attaching the file to the submission.
81
- * @param {string} publicKey
82
- * @param {string} fieldKey - Must match a `file` inputType field on the form.
83
- * @param {File|Blob} file
84
- * @returns {Promise<{fileId:string, token:string, fileName:string, fileType:string, fileSize:number}>}
85
- */
86
- async upload(publicKey, fieldKey, file) {
87
- this.sdk.validateParams(
88
- { publicKey, fieldKey },
89
- {
90
- publicKey: { type: 'string', required: true },
91
- fieldKey: { type: 'string', required: true },
92
- },
93
- );
94
- if (typeof FormData === 'undefined' || !file) {
95
- throw new Error(
96
- 'forms.public.upload :: a browser File/Blob and FormData support are required',
97
- );
98
- }
99
- const body = new FormData();
100
- body.append('fieldKey', fieldKey);
101
- body.append('file', file);
102
- return internalRequest(
103
- this.sdk,
104
- `/f/${publicKey}/upload`,
105
- 'POST',
106
- { body, credentials: 'omit' },
107
- true,
108
- );
109
- }
110
- }
@@ -1,160 +0,0 @@
1
- # Forms v2 SDK module (`sdk.forms`)
2
-
3
- Internal reference for `services/forms.js` + `services/forms/*.js` as they exist in code
4
- today on branch `f-forms-v2` (sdk version `4.13.92`). Not the plan — every claim below is
5
- cited to a source line. See `/workspace/code/app1/plans/forms-v2-plan.md` /
6
- `forms-v2-progress.md` / `forms-v2-precheck.md` for design history; §"Deviations from
7
- plan" at the bottom lists where progress-doc claims and code disagree.
8
-
9
- ## Files
10
-
11
- | File | Exports |
12
- |---|---|
13
- | `services/forms.js` | `FormsService` (registers as `sdk.forms`) |
14
- | `services/forms/PublicService.js` | `FormsPublicService` → `sdk.forms.public` |
15
- | `services/forms/SubmissionsService.js` | `FormsSubmissionsService` → `sdk.forms.submissions` |
16
- | `services/forms/SettingsService.js` | `FormsSettingsService` → `sdk.forms.settings` |
17
- | `services/forms/HealthService.js` | `FormsHealthService` → `sdk.forms.health` |
18
-
19
- Registered in `index.js:29` (import), `index.js:129` (`this.forms = new FormsService(this)`),
20
- exported at `index.js:331-333`.
21
-
22
- ## Methods
23
-
24
- | Method | Signature | Returns | Purpose |
25
- |---|---|---|---|
26
- | `sdk.forms.public.submit` | `submit(publicKey: string, fields: Object, { context, captchaToken, idempotencyKey }: Object = {})` | `Promise<{ok: boolean, message?: string}>` | Submit a public form by `publicKey`; `POST /f/:publicKey` |
27
- | `sdk.forms.public.upload` | `upload(publicKey: string, fieldKey: string, file: File\|Blob)` | `Promise<{fileId, token, fileName, fileType, fileSize}>` | Browser-only file upload ahead of `submit()`, for a `file`-type field; `POST /f/:publicKey/upload` |
28
- | `sdk.forms.submissions.reprocess` | `reprocess(id: string)` | `Promise<Object>` | Re-run the pipeline for a stored submission; `POST /object/formSubmissions/:id/reprocess` |
29
- | `sdk.forms.submissions.markNotSpam` | `markNotSpam(id: string)` | `Promise<Object>` | Clear a submission's spam status; `POST /object/formSubmissions/:id/mark-not-spam` |
30
- | `sdk.forms.submissions.resolveReview` | `resolveReview(id: string, choice: string)` | `Promise<Object>` | Resolve an `identityConflict`/`review`-flagged submission; `POST /object/formSubmissions/:id/resolve-review`, body `{choice}` |
31
- | `sdk.forms.settings.get` | `get()` | `Promise<{defaultRegion?, turnstileSiteKey?, turnstileSecretRef?}>` | Read the account-level `formsAccountSettings` singleton; `GET /forms/settings` |
32
- | `sdk.forms.settings.set` | `set(patch: {defaultRegion?, turnstileSiteKey?, turnstileSecret?})` | `Promise<Object>` | Merge-patch the singleton; `turnstileSecret` is plaintext in, never echoed back; `PUT /forms/settings` |
33
- | `sdk.forms.settings.setForm` | `setForm(formId: string, patch: {captchaProvider?, turnstileSiteKey?, turnstileSecret?})` | `Promise<Object>` | Per-form Turnstile override; `PUT /forms/:formId/settings` |
34
- | `sdk.forms.health.get` | `get(formId: string)` | `Promise<{tiles, sparkline, fields, driftAlerts, windowDays}>` | Health tab data (tiles, 30-day sparkline, per-field fill rate, drift alerts); `GET /forms/:formId/health` |
35
- | `sdk.forms.regeneratePublicKey` | `regeneratePublicKey(formId: string)` | `Promise<{publicKey: string}>` | Mint a new `publicKey`, invalidating every embed using the old one (destructive); `POST /forms/:formId/regenerate-key` |
36
- | `sdk.forms.previewToken` | `previewToken(formId: string)` | `Promise<{token: string, expiresIn: number}>` | Mint a signed 15-min draft-preview token; `POST /forms/:formId/preview-token` |
37
-
38
- Sources: `PublicService.js:46-100`, `SubmissionsService.js:28-79`, `SettingsService.js:28-72`,
39
- `HealthService.js:28-34`, `forms.js:20-63`.
40
-
41
- ## Auth model
42
-
43
- | Group | Methods | Session/JWT required? | Mechanism |
44
- |---|---|---|---|
45
- | Public | `public.submit`, `public.upload` | No | `internalRequest(..., forceFetch=true)` — `PublicService.js:46-62,77-100` explicitly pass `true` as the 5th arg. No `sdk.token` involved; auth is `publicKey` + server-side origin/domainAllowlist check, not a session (comment `PublicService.js:1-8`, mirrors `WebchatVisitorService`/`VisitorService` pattern). |
46
- | Agent-authenticated | `submissions.*`, `settings.*`, `health.get`, `regeneratePublicKey`, `previewToken` | Yes | `internalRequest(sdk, endpoint, method, {...})` with no `forceFetch`/`httpOnly` flag — routed through the SDK's normal transport (socket/HTTP) using `sdk.token`, same as any other authenticated service call (e.g. `WebchatWidgetsService.get()`). |
47
-
48
- `internalRequest`'s `forceFetch` param (`base.js:22-38`) skips optional transports and goes
49
- HTTP-only — used for the two public methods because they're one-shot calls from a visitor
50
- page with no socket session, same convention as file upload/download elsewhere in the SDK.
51
-
52
- Both `public.submit` and `public.upload` also pass `credentials: 'omit'` in the request
53
- params (`PublicService.js:46-62,77-100`; consumed by `base.js` `_httpRequest`, which
54
- otherwise defaults browser fetches to `credentials:'include'`). This surface is called
55
- from third-party origins (a marketing site, not an app1 base domain), and app1-api's CORS
56
- only sends `access-control-allow-credentials` for trusted app1 base domains — a browser
57
- rejects the entire response for a `credentials:'include'` fetch when that header is
58
- missing, even though these endpoints don't use cookies for auth. `sdk.webchat.visitor.*`
59
- (`services/webchat/VisitorService.js`) has the same third-party-origin exposure and sets
60
- the same option for the same reason.
61
-
62
- **Known SDK-surface gap (not a code bug, documented in-file):** `submissions.*` and
63
- `settings.get/set` call server routes that, per their own doc comments, were not yet built
64
- as of P2 (`SubmissionsService.js:7-16`, `SettingsService.js:7-17` — "GAP (forms-v2 P2) ...
65
- the server-side routes these call do NOT exist yet"). `forms-v2-progress.md` marks P5 done
66
- and lists "forms account settings API" and "reprocess/mark-not-spam/review actions" as
67
- shipped, so these comments are likely stale relative to the API side — the SDK client code
68
- itself was not re-verified against `app1-api` route files in this pass (out of scope: this
69
- README covers the SDK repo only). Flagged in Deviations below.
70
-
71
- ## Example — browser handler mode (public form submission)
72
-
73
- Realistic embed-adjacent usage: a page with its own HTML/`<form>` calling the SDK directly
74
- (the `forms.public` surface backs both the generated embed script and any hand-rolled
75
- integration like this).
76
-
77
- ```js
78
- import { BaseSDK, internalRequest } from '@unboundcx/sdk/base.js';
79
- import { FormsPublicService } from '@unboundcx/sdk/services/forms/PublicService.js';
80
-
81
- // Minimal public-only instance — namespace or a custom baseURL, NEVER sdk.token.
82
- // Importing only base.js + PublicService.js avoids pulling in the full SDK's
83
- // service tree (some of which have bundler-unfriendly static imports that broke
84
- // marketing_unbound_cx's browser build — see CHANGELOG 4.13.90).
85
- const sdk = new BaseSDK({ namespace: 'masterc' });
86
- const forms = new FormsPublicService(sdk);
87
-
88
- document.querySelector('#lead-form').addEventListener('submit', async (e) => {
89
- e.preventDefault();
90
- const data = new FormData(e.target);
91
-
92
- await forms.submit(
93
- 'PUBLIC_KEY_HERE',
94
- {
95
- email: data.get('email'),
96
- name: data.get('name'),
97
- interests: data.getAll('interests'), // arrays kept as arrays
98
- },
99
- {
100
- context: {
101
- utm_source: new URLSearchParams(location.search).get('utm_source'),
102
- referrer: document.referrer,
103
- landingUrl: location.href,
104
- },
105
- idempotencyKey: crypto.randomUUID(),
106
- },
107
- );
108
- });
109
- ```
110
-
111
- `context` is spread onto the top level of the request body alongside `fields` (not nested
112
- under a `_context` key — that was a bug fixed in 4.13.89, `PublicService.js:10-21` +
113
- CHANGELOG). Control fields (`_idempotencyKey`, `_captchaToken`) are sent with a leading
114
- underscore so the server can distinguish them from real form values (`PublicService.js:51-53`).
115
-
116
- ## Example — server-side submit (Node)
117
-
118
- ```js
119
- import UnboundSDK from '@unboundcx/sdk';
120
-
121
- // GOTCHA: in Node, UnboundSDK's constructor does NOT use a literal `baseURL` you pass
122
- // unless you pass it explicitly as `options.baseURL` — and even then only via the
123
- // object-form constructor. If you only pass `namespace` (the common case), Node builds
124
- // the URL as `https://<namespace||'login'>.${process.env.API_BASE_URL || 'api.unbound.cx'}`
125
- // (base.js:79-82). So:
126
- // - No API_BASE_URL env var set -> requests go to https://masterc.api.unbound.cx (WRONG
127
- // for dev — hits the real production-shaped host, not dev-d01).
128
- // - API_BASE_URL is treated as a DOMAIN SUFFIX appended after the namespace, not a full
129
- // URL override — set it to e.g. `dev-d01.app1svc.com`, not `https://api.dev-d01...`.
130
- process.env.API_BASE_URL = 'dev-d01.app1svc.com';
131
-
132
- const sdk = new UnboundSDK({ namespace: 'masterc', token: process.env.AGENT_JWT });
133
-
134
- const health = await sdk.forms.health.get('formIdHere');
135
- console.log(health.tiles);
136
-
137
- // Public submit also works from Node (no browser needed) since forms.public.submit()
138
- // only requires FormData for upload(), not submit():
139
- await sdk.forms.public.submit('PUBLIC_KEY_HERE', { email: 'test@example.com' });
140
- ```
141
-
142
- Verified against `base.js:56-82` (`_initializeEnvironment`, Node branch) and
143
- `base.js:107-134` (`setNamespace`, Node branch uses the same `!this._constructorBaseURL`
144
- guard) — this gotcha is still true for this SDK's client construction as of `4.13.92`.
145
- Matches the repo memory note "SDK Node baseURL gotcha."
146
-
147
- ## Deviations from plan / progress docs
148
-
149
- 1. **Health service exists but isn't in the precheck's SDK contract.** `forms-v2-precheck.md`
150
- §5 only specifies `public`, `submissions`, `settings` on `FormsService`; `health` (P8,
151
- `HealthService.js`) and the two gap-closure methods `regeneratePublicKey`/`previewToken`
152
- (added directly on `FormsService`, not a sub-service) were added later and aren't in the
153
- precheck's code sample — expected drift for a precheck doc frozen at P0, not a bug.
154
- 2. **`submissions.*` / `settings.get`/`settings.set` carry "route may not exist yet" comments
155
- still in the source** (`SubmissionsService.js:7-16`, `SettingsService.js:7-17`), even
156
- though `forms-v2-progress.md` marks P5 ("Captcha gate ... forms account settings API,
157
- submission reprocess/mark-not-spam/review actions") as done. This README does not resolve
158
- the discrepancy — it was not in scope to audit `app1-api`'s route files — but flags it as
159
- a live contradiction between the sdk source comments and the progress ledger. Reported
160
- back to Cameron per task instructions.