@qualified-salesforce/web 0.0.1-oidc-bootstrap → 0.1.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.
Files changed (97) hide show
  1. package/CAPABILITY-REFERENCE.md +44 -0
  2. package/CHANGELOG.md +75 -0
  3. package/constants.generated.js +30 -2
  4. package/css/widget/multimodal_v2/index.split-css.panda.css +265 -47
  5. package/package.json +17 -3
  6. package/packs/widget/api/docking/viewport_lock.js +93 -2
  7. package/packs/widget/esm/index.d.ts +134 -0
  8. package/packs/widget/esm/index.js +18 -2
  9. package/packs/widget/esm/qualified.css +265 -47
  10. package/src/graphql-types.js +74 -1
  11. package/src/url.js +4 -1
  12. package/vendor/actioncable/connection.js +40 -3
  13. package/vendor/actioncable/connection_monitor.js +8 -0
  14. package/vendor/actioncable/consumer.js +7 -1
  15. package/vendor/actioncable/index.js +5 -2
  16. package/vendor/actioncable/sse_socket.js +262 -0
  17. package/widget/analytics.js +8 -0
  18. package/widget/api_handler/base_api_handler.js +24 -0
  19. package/widget/booker_outcome_tracker.js +58 -20
  20. package/widget/capabilities.js +190 -0
  21. package/widget/channel.js +38 -6
  22. package/widget/constants.generated.js +2 -1
  23. package/widget/multimodal_v2/components/AppLayout.js +1 -1
  24. package/widget/multimodal_v2/components/app/Foreground.js +10 -9
  25. package/widget/multimodal_v2/components/app/Input.js +8 -3
  26. package/widget/multimodal_v2/components/app/LightboxExperience.js +17 -3
  27. package/widget/multimodal_v2/components/app/Main.js +87 -31
  28. package/widget/multimodal_v2/components/app/MessengerButton.js +18 -1
  29. package/widget/multimodal_v2/components/chat/ChatInputForm.js +8 -6
  30. package/widget/multimodal_v2/components/chat/timeline/CalendarEventPickRenderer.js +2 -2
  31. package/widget/multimodal_v2/components/chat/timeline/PresentDeckRenderer.js +346 -170
  32. package/widget/multimodal_v2/components/chat/timeline/SystemRenderer.js +4 -7
  33. package/widget/multimodal_v2/components/decks/index.js +506 -52
  34. package/widget/multimodal_v2/components/helpers/content_click_payloads.js +67 -1
  35. package/widget/multimodal_v2/components/lightbox_gather/index.js +4 -4
  36. package/widget/multimodal_v2/components/meetings/MeetingBookerEntry.js +8 -1
  37. package/widget/multimodal_v2/components/meetings/MeetingOfferEmailStep.js +1 -1
  38. package/widget/multimodal_v2/components/meetings/MeetingSummaryRail.js +1 -1
  39. package/widget/multimodal_v2/components/meetings/MeetingTimeSelector.js +1 -1
  40. package/widget/multimodal_v2/components/multimodal/Controls.js +11 -3
  41. package/widget/multimodal_v2/components/multimodal/useMultimodal.js +19 -7
  42. package/widget/multimodal_v2/components/presentations/index.js +61 -0
  43. package/widget/multimodal_v2/constants.js +1 -1
  44. package/widget/multimodal_v2/images/icons.js +17 -1
  45. package/widget/multimodal_v2/index.panda.js +2 -2
  46. package/widget/multimodal_v2/locales/cs.js +8 -1
  47. package/widget/multimodal_v2/locales/da.js +8 -1
  48. package/widget/multimodal_v2/locales/de.js +8 -1
  49. package/widget/multimodal_v2/locales/en.js +8 -1
  50. package/widget/multimodal_v2/locales/en_AU.js +8 -1
  51. package/widget/multimodal_v2/locales/en_GB.js +8 -1
  52. package/widget/multimodal_v2/locales/es.js +8 -1
  53. package/widget/multimodal_v2/locales/es_LA.js +8 -1
  54. package/widget/multimodal_v2/locales/fi.js +8 -1
  55. package/widget/multimodal_v2/locales/fr.js +8 -1
  56. package/widget/multimodal_v2/locales/fr_CA.js +8 -1
  57. package/widget/multimodal_v2/locales/id.js +8 -1
  58. package/widget/multimodal_v2/locales/it.js +8 -1
  59. package/widget/multimodal_v2/locales/ja.js +8 -1
  60. package/widget/multimodal_v2/locales/ko.js +8 -1
  61. package/widget/multimodal_v2/locales/nb.js +8 -1
  62. package/widget/multimodal_v2/locales/nl.js +8 -1
  63. package/widget/multimodal_v2/locales/pl.js +8 -1
  64. package/widget/multimodal_v2/locales/pt_BR.js +8 -1
  65. package/widget/multimodal_v2/locales/ru.js +8 -1
  66. package/widget/multimodal_v2/locales/sv.js +8 -1
  67. package/widget/multimodal_v2/locales/th.js +8 -1
  68. package/widget/multimodal_v2/locales/tr.js +8 -1
  69. package/widget/multimodal_v2/locales/vi.js +8 -1
  70. package/widget/multimodal_v2/locales/zh_CN.js +8 -1
  71. package/widget/multimodal_v2/locales/zh_TW.js +8 -1
  72. package/widget/multimodal_v2/multimodal.js +5 -1
  73. package/widget/multimodal_v2/multimodal_channel.js +13 -8
  74. package/widget/multimodal_v2/signals/messages_signal.js +4 -0
  75. package/widget/multimodal_v2/stores/ai_conversation_store.js +6 -2
  76. package/widget/multimodal_v2/stores/artifact_store.js +352 -27
  77. package/widget/multimodal_v2/stores/call_store.js +40 -1
  78. package/widget/multimodal_v2/stores/call_ui_adapter.js +1 -1
  79. package/widget/multimodal_v2/stores/decks_store.js +405 -39
  80. package/widget/multimodal_v2/stores/meeting_store.js +15 -10
  81. package/widget/multimodal_v2/stores/presentation_ga_adapter.js +64 -0
  82. package/widget/multimodal_v2/stores/presentation_telemetry_store.js +556 -0
  83. package/widget/multimodal_v2/stores/presentations_store.js +1 -0
  84. package/widget/multimodal_v2/stores/root_store.js +157 -268
  85. package/widget/multimodal_v2/stores/screen_share_store.js +41 -21
  86. package/widget/multimodal_v2/stores/translation_store.js +9 -0
  87. package/widget/multimodal_v2/stores/ui_store.js +7 -7
  88. package/widget/multimodal_v2/stores/website_navigation_store.js +3 -1
  89. package/widget/multimodal_v2/text_turn_response_tracker.js +419 -0
  90. package/widget/multimodal_v2/types.js +21 -1
  91. package/widget/multimodal_v2/utils/locale.js +2 -1
  92. package/widget/multimodal_v2/utils/signals.js +35 -4
  93. package/widget/pardot_visitor.js +7 -1
  94. package/widget/server-logger.js +97 -2
  95. package/widget/tracker.js +69 -21
  96. package/widget/widget/page_mirror_factory.js +9 -1
  97. package/widget/widget/widget.js +29 -4
@@ -1,7 +1,10 @@
1
+ import { __webpack_require__ } from "../../../js/runtime.js";
1
2
  import { signals_core_module_d, signals_core_module_w } from "../../../js/multimodal_v2.js";
2
3
  import { getCache } from "./multimodal_cache.js";
3
4
  import { ObjectsMap } from "./objects_map.js";
5
+ import "../../utils/native_globals.js";
4
6
 
7
+ /* provided dependency */ var signals_setTimeout = __webpack_require__(68034).pS;
5
8
 
6
9
 
7
10
 
@@ -45,6 +48,31 @@ function $attr(signal, key) {
45
48
  return (_ref = (_signal_value = signal.value) == null ? void 0 : _signal_value[key]) != null ? _ref : null;
46
49
  });
47
50
  }
51
+ /**
52
+ * Backoff between extra attempts at a failed dynamic import.
53
+ *
54
+ * Small on purpose. A chunk whose hash was rotated away by a deploy 404s on
55
+ * every attempt, so only a transient fetch benefits and a longer budget just
56
+ * holds the caller — an overlay waiting to open — for no gain.
57
+ */ const IMPORT_RETRY_DELAYS_MS = [
58
+ 250,
59
+ 1000
60
+ ];
61
+ /**
62
+ * Runs a dynamic import, retrying a failure on the backoff above.
63
+ *
64
+ * Exported for chunks loaded outside a `$fromImport` store. Wrap the import
65
+ * itself, not work that follows it: a retry re-runs whatever it is given, and
66
+ * only the fetch is worth a second attempt.
67
+ */ const importWithRetry = (importFn, attempt)=>{
68
+ if (attempt === void 0) attempt = 0;
69
+ return importFn().catch((error)=>{
70
+ if (attempt >= IMPORT_RETRY_DELAYS_MS.length) throw error;
71
+ return new Promise((resolve, reject)=>{
72
+ signals_setTimeout(()=>importWithRetry(importFn, attempt + 1).then(resolve, reject), IMPORT_RETRY_DELAYS_MS[attempt]);
73
+ });
74
+ });
75
+ };
48
76
  function $fromImport(importFn) {
49
77
  for(var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++){
50
78
  args[_key - 1] = arguments[_key];
@@ -60,12 +88,15 @@ function $fromImport(importFn) {
60
88
  if (loadPromise) {
61
89
  return loadPromise;
62
90
  }
63
- // Start loading
64
- loadPromise = importFn().then((module)=>{
65
- // Use default export
91
+ loadPromise = importWithRetry(importFn).then((module)=>{
66
92
  const Class = module.default;
67
93
  sig.value = new Class(...args);
68
94
  return sig.value;
95
+ }) // Clear on failure so a later caller starts over instead of re-awaiting
96
+ // this rejection for the rest of the session.
97
+ .catch((error)=>{
98
+ loadPromise = null;
99
+ throw error;
69
100
  });
70
101
  return loadPromise;
71
102
  };
@@ -94,4 +125,4 @@ var signals_$all = /* unused export */ undefined;
94
125
  var signals_$attr = /* unused export */ undefined;
95
126
  var signals_$empty = /* unused export */ undefined;
96
127
  var signals_$present = /* unused export */ undefined;
97
- export { $any, $exists, $fromImport, $not, isSignalike, signals_$absent as $absent, signals_$all as $all, signals_$attr as $attr, signals_$empty as $empty, signals_$present as $present };
128
+ export { $any, $exists, $fromImport, $not, importWithRetry, isSignalike, signals_$absent as $absent, signals_$all as $all, signals_$attr as $attr, signals_$empty as $empty, signals_$present as $present };
@@ -1,6 +1,7 @@
1
1
  import { __webpack_require__ } from "../js/runtime.js";
2
2
  import { api } from "../js/4101.js";
3
3
  import { delay as timing_delay } from "../src/timing.js";
4
+ import { isCapabilityAllowed } from "./capabilities.js";
4
5
  import "./telemetry/index.js";
5
6
  import "./utils/native_globals.js";
6
7
  import { telemetry, getTelemetryCtx } from "./telemetry/telemetry.js";
@@ -9,6 +10,7 @@ import { telemetry, getTelemetryCtx } from "./telemetry/telemetry.js";
9
10
 
10
11
 
11
12
 
13
+
12
14
  class PardotVisitor {
13
15
  async getIdentities() {
14
16
  const identities = {};
@@ -87,7 +89,11 @@ class PardotVisitor {
87
89
  }
88
90
  getProspectEmail() {
89
91
  const { trackerDomain, accountId, dynamicContentId } = this.config;
90
- if (!dynamicContentId) {
92
+ // QAI-5864: never inject the third-party Pardot `dc.js` script when the consumer hasn't
93
+ // allowed the "pardot" capability. This is the one "must never load" third-party resource
94
+ // from the GitHub security review (finding 1). Allow-all for the hosted widget (no boundary
95
+ // installed); default-deny once an npm consumer enrols an allowlist via setup().
96
+ if (!isCapabilityAllowed("pardot") || !dynamicContentId) {
91
97
  const promise = Promise.reject();
92
98
  promise.cancel = ()=>{};
93
99
  return promise;
@@ -1,15 +1,22 @@
1
1
  import { __webpack_require__ } from "../js/runtime.js";
2
2
  import { _class_private_field_loose_base, _class_private_field_loose_key, _extends } from "../js/4101.js";
3
3
  import { getSentryClient } from "./widget/sentry.js";
4
+ import "./utils/native_globals.js";
4
5
 
5
6
  const isNil = __webpack_require__("11655");
6
7
  var isNil_default = /*#__PURE__*/__webpack_require__.n(isNil);
8
+ /* provided dependency */ var server_logger_setTimeout = __webpack_require__(68034).pS;
9
+ /* provided dependency */ var server_logger_clearTimeout = __webpack_require__(68034).Kj;
10
+ /* provided dependency */ var __nativeWindowAddEventListener = __webpack_require__(68034).zh;
11
+ /* provided dependency */ var __nativeDocumentAddEventListener = __webpack_require__(68034).Qj;
7
12
 
8
13
 
9
14
 
10
15
 
11
16
 
12
- var _config = /*#__PURE__*/ _class_private_field_loose_key("_config"), _widgetId = /*#__PURE__*/ _class_private_field_loose_key("_widgetId"), _visitorId = /*#__PURE__*/ _class_private_field_loose_key("_visitorId");
17
+ const VISITOR_EVENTS_BATCH_SIZE_LIMIT = 20;
18
+ const VISITOR_EVENTS_BATCH_FLUSH_INTERVAL_MS = 3000;
19
+ var _config = /*#__PURE__*/ _class_private_field_loose_key("_config"), _widgetId = /*#__PURE__*/ _class_private_field_loose_key("_widgetId"), _visitorId = /*#__PURE__*/ _class_private_field_loose_key("_visitorId"), _batchQueue = /*#__PURE__*/ _class_private_field_loose_key("_batchQueue"), _flushTimerId = /*#__PURE__*/ _class_private_field_loose_key("_flushTimerId");
13
20
  class ServerLogger {
14
21
  /**
15
22
  * Updates the widget configuration.
@@ -66,9 +73,19 @@ class ServerLogger {
66
73
  * serverLog({ message: "Page closing", fireAndForget: true });
67
74
  */ serverLog(param) {
68
75
  let { type = "log", message: event, extra = {}, fireAndForget = false } = param;
76
+ var _class_private_field_loose_base__config_featureFlags;
69
77
  if (!_class_private_field_loose_base(this, _config)[_config].host || !_class_private_field_loose_base(this, _config)[_config].token) {
70
78
  return;
71
79
  }
80
+ if ((_class_private_field_loose_base__config_featureFlags = _class_private_field_loose_base(this, _config)[_config].featureFlags) == null ? void 0 : _class_private_field_loose_base__config_featureFlags.widgetVisitorEventsBatching) {
81
+ this.enqueueEvent({
82
+ event,
83
+ type,
84
+ extra,
85
+ fireAndForget
86
+ });
87
+ return;
88
+ }
72
89
  this.sendEvent("visitor_events", {
73
90
  params: {
74
91
  wu: _class_private_field_loose_base(this, _widgetId)[_widgetId],
@@ -99,6 +116,54 @@ class ServerLogger {
99
116
  })
100
117
  });
101
118
  }
119
+ enqueueEvent(item) {
120
+ _class_private_field_loose_base(this, _batchQueue)[_batchQueue].push(item);
121
+ if (_class_private_field_loose_base(this, _batchQueue)[_batchQueue].length >= VISITOR_EVENTS_BATCH_SIZE_LIMIT) {
122
+ this.flushBatch();
123
+ return;
124
+ }
125
+ this.scheduleFlush();
126
+ }
127
+ scheduleFlush() {
128
+ if (_class_private_field_loose_base(this, _flushTimerId)[_flushTimerId] !== null) {
129
+ return;
130
+ }
131
+ _class_private_field_loose_base(this, _flushTimerId)[_flushTimerId] = server_logger_setTimeout(()=>this.flushBatch(), VISITOR_EVENTS_BATCH_FLUSH_INTERVAL_MS);
132
+ }
133
+ flushBatch(param) {
134
+ let { fireAndForget = false } = param === void 0 ? {} : param;
135
+ if (_class_private_field_loose_base(this, _flushTimerId)[_flushTimerId] !== null) {
136
+ server_logger_clearTimeout(_class_private_field_loose_base(this, _flushTimerId)[_flushTimerId]);
137
+ _class_private_field_loose_base(this, _flushTimerId)[_flushTimerId] = null;
138
+ }
139
+ if (_class_private_field_loose_base(this, _batchQueue)[_batchQueue].length === 0) {
140
+ return;
141
+ }
142
+ const events = _class_private_field_loose_base(this, _batchQueue)[_batchQueue];
143
+ _class_private_field_loose_base(this, _batchQueue)[_batchQueue] = [];
144
+ // A batch is one request: if any queued event asked to be fire-and-forgotten
145
+ // (e.g. an abandonment logged on its own pagehide listener), the whole flush
146
+ // is, since we can't selectively suppress errors for part of one response.
147
+ const shouldFireAndForget = fireAndForget || events.some((event)=>event.fireAndForget);
148
+ this.sendEvent("visitor_events/batch", {
149
+ params: {
150
+ wu: _class_private_field_loose_base(this, _widgetId)[_widgetId],
151
+ uuid: _class_private_field_loose_base(this, _visitorId)[_visitorId]
152
+ },
153
+ body: {
154
+ events: events.map((param)=>{
155
+ let { event, type, extra } = param;
156
+ return {
157
+ event,
158
+ type,
159
+ extra
160
+ };
161
+ })
162
+ }
163
+ }, {
164
+ fireAndForget: shouldFireAndForget
165
+ });
166
+ }
102
167
  async sendEvent(path, param, param1) {
103
168
  let { params, body } = param;
104
169
  let { fireAndForget = false } = param1 === void 0 ? {} : param1;
@@ -145,12 +210,42 @@ class ServerLogger {
145
210
  writable: true,
146
211
  value: void 0
147
212
  });
213
+ Object.defineProperty(this, _batchQueue, {
214
+ writable: true,
215
+ value: void 0
216
+ });
217
+ Object.defineProperty(this, _flushTimerId, {
218
+ writable: true,
219
+ value: void 0
220
+ });
221
+ _class_private_field_loose_base(this, _batchQueue)[_batchQueue] = [];
222
+ _class_private_field_loose_base(this, _flushTimerId)[_flushTimerId] = null;
223
+ this.handlePagehide = ()=>{
224
+ this.flushBatch({
225
+ fireAndForget: true
226
+ });
227
+ };
228
+ this.handleVisibilityChange = ()=>{
229
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") {
230
+ this.flushBatch({
231
+ fireAndForget: true
232
+ });
233
+ }
234
+ };
148
235
  _class_private_field_loose_base(this, _config)[_config] = config;
149
236
  _class_private_field_loose_base(this, _widgetId)[_widgetId] = undefined;
150
237
  _class_private_field_loose_base(this, _visitorId)[_visitorId] = undefined;
238
+ if (typeof window !== "undefined") {
239
+ __nativeWindowAddEventListener("pagehide", this.handlePagehide);
240
+ }
241
+ if (typeof document !== "undefined") {
242
+ __nativeDocumentAddEventListener("visibilitychange", this.handleVisibilityChange);
243
+ }
151
244
  }
152
245
  }
153
246
  const serverLogger = new ServerLogger({});
154
247
 
155
248
  var server_logger_ServerLogger = /* unused export */ undefined;
156
- export { serverLogger, server_logger_ServerLogger as ServerLogger };
249
+ var server_logger_VISITOR_EVENTS_BATCH_FLUSH_INTERVAL_MS = /* unused export */ undefined;
250
+ var server_logger_VISITOR_EVENTS_BATCH_SIZE_LIMIT = /* unused export */ undefined;
251
+ export { serverLogger, server_logger_ServerLogger as ServerLogger, server_logger_VISITOR_EVENTS_BATCH_FLUSH_INTERVAL_MS as VISITOR_EVENTS_BATCH_FLUSH_INTERVAL_MS, server_logger_VISITOR_EVENTS_BATCH_SIZE_LIMIT as VISITOR_EVENTS_BATCH_SIZE_LIMIT };
package/widget/tracker.js CHANGED
@@ -7,6 +7,7 @@ import { BUTTON_LIKE_SELECTOR, isWithinQualifiedElement } from "../src/smart_web
7
7
  import { fromBase64 } from "../src/string.js";
8
8
  import { every, after } from "../src/timing.js";
9
9
  import { default as WidgetObject } from "./widget_object.js";
10
+ import { isCapabilityAllowed } from "./capabilities.js";
10
11
  import { captureWidgetExceptions } from "./capture_widget_exceptions.js";
11
12
  import { isSafariUserAgent } from "./constants.js";
12
13
  import { default as data360_visitor } from "./data360_visitor.js";
@@ -53,6 +54,7 @@ var throttle_default = /*#__PURE__*/__webpack_require__.n(throttle);
53
54
 
54
55
 
55
56
 
57
+
56
58
 
57
59
 
58
60
  const events = [
@@ -111,7 +113,12 @@ class Tracker extends WidgetObject {
111
113
  if (this.isDisposed) {
112
114
  return;
113
115
  }
114
- if (this.config.hasMarketo) {
116
+ // QAI-5896: marketing-automation visitor-identity capture (Marketo/HubSpot/Data360/Eloqua)
117
+ // reads a local marketing cookie/global and forwards the token to Qualified. The `has*` flags
118
+ // are stripped by `filterRuntimeConfig` when `marketingIdentity` is denied; this `isCapability
119
+ // Allowed` guard is the client-side backstop, so the capture never runs even if a flag reaches
120
+ // the widget by an unfiltered path. Allow-all for the hosted widget (no boundary installed).
121
+ if (this.config.hasMarketo && isCapabilityAllowed("marketingIdentity")) {
115
122
  await telemetry.startActiveSpanAsync("Init marketo", {}, this.telemetryCtx, async (span)=>{
116
123
  const marketoToken = await new marketo_visitor().getToken();
117
124
  if (marketoToken !== undefined) {
@@ -127,7 +134,7 @@ class Tracker extends WidgetObject {
127
134
  if (this.isDisposed) {
128
135
  return;
129
136
  }
130
- if (this.config.hasHubSpot) {
137
+ if (this.config.hasHubSpot && isCapabilityAllowed("marketingIdentity")) {
131
138
  await telemetry.startActiveSpanAsync("Init hubspot", {}, this.telemetryCtx, async (span)=>{
132
139
  const hubspotToken = await new hubspot_visitor().getToken(this.widget.config.hubspotTokenRetrievalTimeout);
133
140
  if (hubspotToken !== undefined) {
@@ -143,7 +150,7 @@ class Tracker extends WidgetObject {
143
150
  if (this.isDisposed) {
144
151
  return;
145
152
  }
146
- if (this.config.hasData360) {
153
+ if (this.config.hasData360 && isCapabilityAllowed("marketingIdentity")) {
147
154
  await telemetry.startActiveSpanAsync("Init data360", {}, this.telemetryCtx, async (span)=>{
148
155
  const deviceId = await new data360_visitor().getDeviceId();
149
156
  if (deviceId !== undefined) {
@@ -159,7 +166,7 @@ class Tracker extends WidgetObject {
159
166
  if (this.isDisposed) {
160
167
  return;
161
168
  }
162
- if (this.config.hasEloqua) {
169
+ if (this.config.hasEloqua && isCapabilityAllowed("marketingIdentity")) {
163
170
  await telemetry.startActiveSpanAsync("Init eloqua", {}, this.telemetryCtx, async (span)=>{
164
171
  const eloquaToken = new eloqua_visitor().getToken();
165
172
  if (eloquaToken !== undefined) {
@@ -176,7 +183,12 @@ class Tracker extends WidgetObject {
176
183
  return;
177
184
  }
178
185
  if (this.visitor.smartFormData) {
179
- if (this.config.hasSmartForms) {
186
+ // QAI-5896: the smart-form replay egresses captured form-field values, so it belongs behind
187
+ // formCapture too — a client backstop to the config-strip of `hasSmartForms`, for the case a
188
+ // flag reaches the widget by an unfiltered path. `clearSmartFormData()` still runs when denied
189
+ // so a stashed submit can't leak on a later navigation.
190
+ const formCaptureAllowed = isCapabilityAllowed("formCapture");
191
+ if (formCaptureAllowed && this.config.hasSmartForms) {
180
192
  this.experienceAdapter.showLightboxLoading();
181
193
  // Pre-offer SLI: a submit saved by `saveSmartFormData` before this page
182
194
  // context existed, replayed at init. Unlike the api_handler path this IS
@@ -192,12 +204,14 @@ class Tracker extends WidgetObject {
192
204
  armFormCtaBookerIntent("form:smart_form", this.visitor.smartFormData.botApiName);
193
205
  }
194
206
  }
195
- this.setInitializationParams({
196
- smart_form: {
197
- field_values: this.visitor.smartFormData.fieldValues,
198
- bot_api_name: this.visitor.smartFormData.botApiName
199
- }
200
- });
207
+ if (formCaptureAllowed) {
208
+ this.setInitializationParams({
209
+ smart_form: {
210
+ field_values: this.visitor.smartFormData.fieldValues,
211
+ bot_api_name: this.visitor.smartFormData.botApiName
212
+ }
213
+ });
214
+ }
201
215
  this.visitor.clearSmartFormData();
202
216
  }
203
217
  if (!this.widget.isMeetingLink) {
@@ -534,11 +548,22 @@ class Tracker extends WidgetObject {
534
548
  if (!this.config.hasSmartForms) return;
535
549
  const validBotApiName = botApiName || ((_this_widget_visitor_smartFormData = this.widget.visitor.smartFormData) == null ? void 0 : _this_widget_visitor_smartFormData.botApiName);
536
550
  const validFieldValues = ((_this_widget_visitor_formData = this.widget.visitor.formData) == null ? void 0 : _this_widget_visitor_formData.fieldValues) || ((_this_widget_visitor_smartFormData1 = this.widget.visitor.smartFormData) == null ? void 0 : _this_widget_visitor_smartFormData1.fieldValues);
551
+ // Minted and registered here, immediately before the request that carries it,
552
+ // so every registered id is guaranteed to actually round-trip (CORE-3488).
553
+ // Registering any earlier (e.g. at the caller's showLightboxLoading site)
554
+ // would leak the id if this method bails out above on `!hasSmartForms`.
555
+ const formTransactionId = esm_browser_v4();
556
+ try {
557
+ this.widget.iframeManager.registerLoadingRequest(formTransactionId);
558
+ } catch (unused) {
559
+ // V2 path: IframeManager not registered, safe to ignore
560
+ }
537
561
  this.channel.perform("show_form_experience", {
538
562
  bot_api_name: validBotApiName,
539
563
  field_values: validFieldValues,
540
564
  form_data: this.widget.visitor.formData,
541
- form_destinations: formDestinations
565
+ form_destinations: formDestinations,
566
+ form_transaction_id: formTransactionId
542
567
  });
543
568
  this.widget.visitor.clearSmartFormData();
544
569
  this.widget.visitor.clearFormData();
@@ -585,6 +610,15 @@ class Tracker extends WidgetObject {
585
610
  armFormCtaBookerIntent("form:" + formType);
586
611
  }
587
612
  this.experienceAdapter.showLightboxLoading();
613
+ // Minted and registered here, immediately before the request that carries
614
+ // it, so every registered id is guaranteed to actually round-trip
615
+ // (CORE-3488).
616
+ const formTransactionId = esm_browser_v4();
617
+ try {
618
+ this.widget.iframeManager.registerLoadingRequest(formTransactionId);
619
+ } catch (unused) {
620
+ // V2 path: IframeManager not registered, safe to ignore
621
+ }
588
622
  this.channel.perform("show_experience_for_form", {
589
623
  form_type: formType,
590
624
  form_data: {
@@ -592,7 +626,8 @@ class Tracker extends WidgetObject {
592
626
  form_id: formData.formId,
593
627
  form_email: formData.formEmail,
594
628
  form_action_url: (_formData_formActionUrl1 = formData.formActionUrl) == null ? void 0 : _formData_formActionUrl1.replace(/^https?:\/\//, "")
595
- }
629
+ },
630
+ form_transaction_id: formTransactionId
596
631
  });
597
632
  }
598
633
  this.widget.debugState.logEvent("tracker:showExperienceForForm", "success", {});
@@ -640,7 +675,6 @@ class Tracker extends WidgetObject {
640
675
  });
641
676
  }, // TODO: remove once meeting link has its own pack
642
677
  this.recordMeetingLinkLoad = (token)=>{
643
- if (token === void 0) token = undefined;
644
678
  const data = Object.assign({}, this.initializationParams, {
645
679
  wu: this.widget.widgetId,
646
680
  url: document.location.href,
@@ -779,14 +813,28 @@ class Tracker extends WidgetObject {
779
813
  this.visitor.setQualifiedRedirect(true);
780
814
  this.isBotRedirecting = true;
781
815
  }, // @ts-expect-error property decorator typing
782
- this.onEndLoadingState = ()=>{
783
- var // Clear any active loading overlay in the v2 adapter path.
784
- // In v1, the iframe handles this via postMessage; v2 needs an explicit signal write.
785
- // Guard: experienceAdapter may not be initialized yet if the channel fires before Widget.init() completes.
786
- _this_experienceAdapter;
787
- (_this_experienceAdapter = this.experienceAdapter) == null ? void 0 : _this_experienceAdapter.closeLightbox();
816
+ this.onEndLoadingState = (payload)=>{
817
+ // Withhold the overlay teardown -- but not the fallback dispatch below -- when
818
+ // this answers for one outstanding request while another is still waiting
819
+ // (CORE-3488). Only the v1 iframe path tracks outstanding ids; v2 has nothing
820
+ // to withhold against, so it always tears down.
821
+ let shouldTeardown = true;
822
+ try {
823
+ shouldTeardown = this.widget.iframeManager.releaseLoadingRequest(payload == null ? void 0 : payload.formTransactionId);
824
+ } catch (unused) {
825
+ // V2 path: IframeManager not registered, safe to ignore
826
+ }
827
+ if (shouldTeardown) {
828
+ var // Clear any active loading overlay in the v2 adapter path.
829
+ // In v1, the iframe handles this via postMessage; v2 needs an explicit signal write.
830
+ // Guard: experienceAdapter may not be initialized yet if the channel fires before Widget.init() completes.
831
+ _this_experienceAdapter;
832
+ (_this_experienceAdapter = this.experienceAdapter) == null ? void 0 : _this_experienceAdapter.closeLightbox();
833
+ }
788
834
  // This fallback is called if we should trigger the default behavior of the last button click
789
- // i.e if the button click was hijacked but the visitor was disqualified
835
+ // i.e if the button click was hijacked but the visitor was disqualified. Not gated on
836
+ // shouldTeardown: it answers a different question (was this click's own bot a dead end?)
837
+ // than the outstanding-request gate above (is some OTHER request still waiting?).
790
838
  this.lastButtonTriggerEvent && this.dispatchFallbackEvent(this.lastButtonTriggerEvent);
791
839
  }, this.getRedirectCount = ()=>{
792
840
  var _performance_getEntriesByType;
@@ -1,11 +1,19 @@
1
+ import { isCapabilityAllowed } from "../capabilities.js";
1
2
  import { default as PageMirror } from "../page_mirror.js";
2
3
  import { default as WidgetObject } from "../widget_object.js";
3
4
 
4
5
 
5
6
 
7
+
6
8
  class PageMirrorFactory extends WidgetObject {
7
9
  create() {
8
- if (!this.widget.config.isComplete || this.widget.config.disableLiveView) {
10
+ // QAI-5864: skip DOM serialization/mirroring unless the consumer allowed the "liveView"
11
+ // capability. Live View has no positive config enabler (it is on unless `disableLiveView` is
12
+ // set) and its stream arrives on a separate cable subscription that bypasses the message,
13
+ // command, and config enforcement points — so this client-side guard is the only thing that
14
+ // stops the mirror for an enrolled npm consumer. Allow-all for the hosted widget (no boundary
15
+ // installed); default-deny once an npm consumer enrols via setup().
16
+ if (!this.widget.config.isComplete || this.widget.config.disableLiveView || !isCapabilityAllowed("liveView")) {
9
17
  return this.container.resolve("emptyPageMirror");
10
18
  } else {
11
19
  return this.container.resolve("pageMirror", this.widget.config.denylistedVisitorStreamDomains || []);
@@ -6,6 +6,7 @@ import { isDomainMatch, isIframed } from "../../src/iframes.js";
6
6
  import { delay as timing_delay } from "../../src/timing.js";
7
7
  import { normalizeURL, hasURLMatch } from "../../src/url-utils.js";
8
8
  import { default as ApiHandler } from "../api_handler/api_handler.js";
9
+ import { isCapabilityAllowed } from "../capabilities.js";
9
10
  import { captureWidgetExceptions } from "../capture_widget_exceptions.js";
10
11
  import { default as channel } from "../channel.js";
11
12
  import { default as Container } from "../container.js";
@@ -91,6 +92,7 @@ var cloneDeep_default = /*#__PURE__*/__webpack_require__.n(cloneDeep);
91
92
 
92
93
 
93
94
 
95
+
94
96
 
95
97
 
96
98
  const LOOPBACK_HOSTS = new Set([
@@ -114,7 +116,10 @@ function isLoopbackHost(host) {
114
116
  return host.includes("localhost");
115
117
  }
116
118
  }
117
- class IframedFormExperienceAdapter {
119
+ // Presentation layer for a surface that never shows the messenger: every verb is
120
+ // a no-op and every counter is at rest. `MeetingLinkWidget` uses it; the base
121
+ // `V1Adapter` would dereference a missing iframe manager on the first verb.
122
+ class NoopExperienceAdapter {
118
123
  dispose() {}
119
124
  showLightboxLoading() {}
120
125
  showMessengerLoading() {}
@@ -348,6 +353,10 @@ class Widget extends Injectable(Disposable(Object)) {
348
353
  }
349
354
  this.config = _extends({}, this.config, config);
350
355
  this.config.requiresCookieConsent = this.config.hasCookieConsent !== undefined;
356
+ // The earlier setConfig() call (above, before this async gap) captured a config
357
+ // snapshot that predates this merge, so featureFlags (only present on the merged
358
+ // config, not the bootstrap-time envConfig) never reached serverLogger without this.
359
+ serverLogger.setConfig(this.config);
351
360
  if (this.isIframed && !this.bootstrapsWhenIframed()) {
352
361
  this.messengerUptime.dispose();
353
362
  const iframeApiHandler = this.apiHandler;
@@ -516,7 +525,10 @@ class Widget extends Injectable(Disposable(Object)) {
516
525
  error
517
526
  });
518
527
  }
519
- if (this.tracker.config.hasWebsiteEmbeddedForms) {
528
+ // QAI-5864: registering the `<qualified-form>` embedded-form element wires its own submit
529
+ // listener that egresses via `channel.send` (bypassing the command-layer gate), so guard the
530
+ // registration on the "formCapture" capability too. Allow-all for the hosted widget.
531
+ if (isCapabilityAllowed("formCapture") && this.tracker.config.hasWebsiteEmbeddedForms) {
520
532
  this.initQualifiedForms();
521
533
  }
522
534
  this.exposeDebugging();
@@ -802,7 +814,13 @@ class Widget extends Injectable(Disposable(Object)) {
802
814
  // TODO: remove once meeting link has its own pack
803
815
  get meetingLinkToken() {
804
816
  if (!this.isMeetingLink) return;
805
- const pathParts = window.location.pathname.replace(/^\//, "").split("-");
817
+ // Strip trailing slashes as well as the leading one. Customer CMSes often
818
+ // emit the link as `https://schedule.qualified.com/mA7n1gd/`; Rails ignores
819
+ // the trailing slash when routing the page, but `pathname` keeps it, so the
820
+ // token sent with `record_meeting_link_load` became `mA7n1gd/`. The server
821
+ // cannot resolve that and broadcasts an empty conversation, leaving the
822
+ // visitor on the spinner until the watchdog fires (CORE-3725).
823
+ const pathParts = window.location.pathname.replace(/^\/+|\/+$/g, "").split("-");
806
824
  return pathParts[pathParts.length - 1];
807
825
  }
808
826
  get connectionParams() {
@@ -910,6 +928,12 @@ class Widget extends Injectable(Disposable(Object)) {
910
928
  }
911
929
  initFormExperience() {
912
930
  var _this_config_featureFlags;
931
+ // QAI-5864: don't arm the auto-armed third-party form listeners (Marketo / HubSpot) unless the
932
+ // consumer allowed the "formCapture" capability. These listeners are not driven by a public
933
+ // command verb, so the command-layer gate never sees them — only the `hasSmartForms` config key
934
+ // (which the runtime-config filter strips when denied) and this guard protect them. Allow-all
935
+ // for the hosted widget (no boundary installed); default-deny once an npm consumer enrols.
936
+ if (!isCapabilityAllowed("formCapture")) return;
913
937
  if (!this.tracker.config.hasSmartForms) {
914
938
  this.debugState.logEvent("widget:hasSmartForms", "error");
915
939
  return;
@@ -1694,5 +1718,6 @@ __decorate([
1694
1718
  ], Widget.prototype, "trackVisitedContentActivity", null);
1695
1719
 
1696
1720
  var widget_FormStorageKey = /* unused export */ undefined;
1697
- export { widget_FormStorageKey as FormStorageKey };
1721
+ var widget_NoopExperienceAdapter = /* unused export */ undefined;
1722
+ export { widget_FormStorageKey as FormStorageKey, widget_NoopExperienceAdapter as NoopExperienceAdapter };
1698
1723
  export default Widget;