@ada-support/embed2 1.13.6 → 1.14.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Embed 2
2
2
 
3
- Embed 2 allows clients to setup Ada Web Chat in their web application. For further information on how to use Embed, check out our [documentation](https://developers.ada.cx/reference/add-chat-to-website).
3
+ Embed 2 allows clients to setup Ada Web Chat in their web application. For further information on how to use Embed, check out our [documentation](https://docs.ada.cx/chat/web/getting-started).
4
4
 
5
5
  ## 1. Installation
6
6
 
@@ -34,4 +34,4 @@ adaEmbed.start({
34
34
  });
35
35
  ```
36
36
 
37
- In the example above the `handle` key is a part of `AdaSettings`. To learn more about other settings, consult the [API reference](https://developers.ada.cx/reference/embed2-reference).
37
+ In the example above the `handle` key is a part of `AdaSettings`. To learn more about other settings, consult the [API reference](https://docs.ada.cx/chat/web/sdk-api-reference).
@@ -0,0 +1,5 @@
1
+ import type { PayloadByEvent } from "common/lib/channel";
2
+ /** Strips the internal `proactive_conversation` tracker for getMetaFields(). */
3
+ export declare function toHostFacingMetaFields(metaFields: Record<string, unknown> | undefined): PayloadByEvent["GET_META_FIELDS_RESPONSE"];
4
+ /** Merges a proactive_conversation key onto existing metaFields. */
5
+ export declare function withProactiveKey(metaFields: Record<string, unknown> | undefined, key: string): Record<string, unknown>;
@@ -10,6 +10,7 @@ export declare function createEmbed(adaSettings: StartOptions): {
10
10
  readonly adaSettings: StartOptions;
11
11
  readonly getInfo: () => Promise<WindowInfo>;
12
12
  readonly setMetaFields: (options: MetaFieldPayload) => Promise<void>;
13
+ readonly getMetaFields: () => Promise<MetaFieldPayload>;
13
14
  readonly setSensitiveMetaFields: (options: MetaFieldPayload) => Promise<void>;
14
15
  readonly stop: () => Promise<void>;
15
16
  readonly deleteHistory: () => Promise<void>;
@@ -59,6 +59,8 @@ export declare const GET_INFO = "GET_INFO";
59
59
  export declare const GET_INFO_RESPONSE = "GET_INFO_RESPONSE";
60
60
  export declare const SET_META_FIELDS = "SET_META_FIELDS";
61
61
  export declare const SET_META_FIELDS_RESPONSE = "SET_META_FIELDS_RESPONSE";
62
+ export declare const GET_META_FIELDS = "GET_META_FIELDS";
63
+ export declare const GET_META_FIELDS_RESPONSE = "GET_META_FIELDS_RESPONSE";
62
64
  export declare const STOP = "STOP";
63
65
  export declare const STOP_RESPONSE = "STOP_RESPONSE";
64
66
  export declare const DELETE_HISTORY = "DELETE_HISTORY";
@@ -3,6 +3,7 @@ export type HostTelemetrySurface = "mobile" | "browser";
3
3
  export type HostTelemetryPlatform = "ios" | "android" | "react-native" | "web" | "unknown";
4
4
  export type HostTelemetryWebSdkOrigin = "legacy" | "messaging";
5
5
  export type HostTelemetryMobileShell = "messaging-sdk" | "legacy-sdk";
6
+ export type HostTelemetryMode = "headless" | "visible";
6
7
  export interface HostTelemetryContext {
7
8
  surface: HostTelemetrySurface;
8
9
  hostPlatform: HostTelemetryPlatform;
@@ -10,6 +11,13 @@ export interface HostTelemetryContext {
10
11
  mobileShell?: HostTelemetryMobileShell;
11
12
  mobilePackage?: string;
12
13
  mobileVersion?: string;
14
+ /**
15
+ * MES-981 P1 #10: distinguishes SDK-driven headless sessions from
16
+ * sessions with a visible chat surface. Tagged on every log/RUM line
17
+ * emitted through `setGlobalLogContext(toMonitoringContext(...))` so
18
+ * dashboards can filter by mode without a separate event.
19
+ */
20
+ mode?: HostTelemetryMode;
13
21
  }
14
22
  export declare const HOST_TELEMETRY_QUERY_PARAM = "ada_host_telemetry";
15
23
  type StartOptionsWithHostTelemetry = StartOptions & {
@@ -0,0 +1,5 @@
1
+ import { AdaEmbedError } from "common/helpers/errors";
2
+ export declare const PROGRAMMATIC_CONTROL_DISABLED_ERROR: AdaEmbedError;
3
+ export declare function isProgrammaticControlEnabled(adaSettings: object | undefined | null): boolean;
4
+ export declare function requireProgrammaticControl(adaSettings: object | undefined | null): void;
5
+ export declare function shouldDeliverTranscriptEvent(eventKey: string, adaSettings: object | undefined | null): boolean;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Race `promise` against a timeout. If the timeout fires first, reject
3
+ * with an AdaEmbedError carrying `errorMessage`. If the promise settles
4
+ * first (resolve or reject), clear the pending timer so it doesn't
5
+ * keep the event loop alive past resolution.
6
+ *
7
+ * MES-981 P1 #7: used to cap host-supplied beforeSend hooks so a
8
+ * never-resolving delegate can't hang sendMessage forever.
9
+ */
10
+ export declare function withTimeout<T>(promise: PromiseLike<T> | T, ms: number, errorMessage: string): Promise<T>;
@@ -15,7 +15,10 @@ export interface EmbedRequestPayloadByEvent {
15
15
  DISPATCH: StoreDispatchPayload;
16
16
  EVAL_CAMPAIGN_CONDITIONS: CampaignParams;
17
17
  GET: StoreGetPayload;
18
+ GET_CONVERSATION: unknown;
18
19
  GET_INFO: unknown;
20
+ GET_MESSAGES: unknown;
21
+ GET_META_FIELDS: unknown;
19
22
  GET_STATE: unknown;
20
23
  RESET: ResetParams;
21
24
  SET_META_FIELDS: MetaFieldPayload;
@@ -34,6 +37,12 @@ export interface EmbedRequestPayloadByEvent {
34
37
  SET_LANGUAGE: {
35
38
  language: string;
36
39
  };
40
+ SET_COMPOSER_TEXT: {
41
+ text: string;
42
+ };
43
+ SEND_MESSAGE: {
44
+ text: string;
45
+ };
37
46
  TRIGGER_PROACTIVE: TriggerProactiveParams;
38
47
  INJECT_PROACTIVE_CONVERSATION: {
39
48
  key: string;
@@ -51,8 +60,42 @@ export interface EmbedResponsePayloadByEvent {
51
60
  "DISPATCH_RESPONSE": StoreState;
52
61
  "EVAL_CAMPAIGN_CONDITIONS_RESPONSE": unknown;
53
62
  "GET_RESPONSE": StoreState[keyof StoreState];
63
+ "GET_CONVERSATION_RESPONSE": {
64
+ id: string | null;
65
+ handoff: {
66
+ inLiveChat: boolean;
67
+ agent?: {
68
+ id: string;
69
+ name: string;
70
+ avatarUrl?: string;
71
+ };
72
+ };
73
+ };
54
74
  "GET_INFO_RESPONSE": unknown;
75
+ "GET_MESSAGES_RESPONSE": Array<{
76
+ id: string;
77
+ role: "user" | "bot" | "agent";
78
+ type: string;
79
+ body?: string;
80
+ createdAt: number;
81
+ agent?: {
82
+ id: string;
83
+ name: string;
84
+ avatarUrl?: string;
85
+ };
86
+ }>;
87
+ "GET_META_FIELDS_RESPONSE": MetaFieldPayload;
55
88
  "GET_STATE_RESPONSE": StoreState;
89
+ "SEND_MESSAGE_RESPONSE": {
90
+ id: string;
91
+ } | {
92
+ error: string;
93
+ };
94
+ "SET_COMPOSER_TEXT_RESPONSE": {
95
+ ok: true;
96
+ } | {
97
+ error: string;
98
+ };
56
99
  "GREETING": unknown;
57
100
  "JWT_AUTH_RESPONSE": unknown;
58
101
  "RESET_RESPONSE": unknown;
@@ -0,0 +1,191 @@
1
+ import type { AdaEmbedAPI, AdaEventContext, AdaEventKey, AdaEventSubscriptionCallback, StartOptions } from "@ada-support/embed-types";
2
+ import type { MetaFieldPayload } from "common/types/store";
3
+ /**
4
+ * MES-981: new ada:* event keys + their payload shapes, added alongside
5
+ * the existing `@ada-support/embed-types` `AdaEventDataKeyedByEvent` union.
6
+ * Once the upstream package is updated these are folded back in and this
7
+ * file goes away.
8
+ */
9
+ export interface ExtendedAdaEventDataKeyedByEvent {
10
+ /**
11
+ * MES-981 P1 #10: tagged with `mode` so downstream consumers can
12
+ * distinguish between visible-drawer sessions and SDK-driven headless
13
+ * sessions.
14
+ */
15
+ "ada:ready": {
16
+ mode: "headless" | "visible";
17
+ };
18
+ "ada:message:sent": {
19
+ message: Message;
20
+ };
21
+ "ada:message:received": {
22
+ message: Message;
23
+ };
24
+ "ada:typing:start": {
25
+ agentId: string;
26
+ };
27
+ "ada:typing:stop": {
28
+ agentId: string;
29
+ };
30
+ "ada:connection:change": {
31
+ state: "connected" | "reconnecting" | "disconnected";
32
+ };
33
+ "ada:conversation:change": {
34
+ id: string;
35
+ };
36
+ }
37
+ export type ExtendedAdaEventKey = AdaEventKey | keyof ExtendedAdaEventDataKeyedByEvent;
38
+ /**
39
+ * MES-981: forward-compatible start options that include the new
40
+ * MES-981 flags. The flags are consumed at runtime by `embed-2`; the
41
+ * type augmentation here lets host pages set them without a type error
42
+ * while the upstream `@ada-support/embed-types` package is being updated.
43
+ */
44
+ export type ExtendedStartOptions = StartOptions & {
45
+ /** Loads chat without rendering the default button/drawer. */
46
+ headless?: boolean;
47
+ /**
48
+ * Opt-in to the programmatic transcript-bearing surface introduced by
49
+ * MES-981: `sendMessage`, `setComposerText`, `getMessages`, `setDelegate`,
50
+ * and the `ada:message:sent` / `ada:message:received` events.
51
+ *
52
+ * Defaults to `false`. While the flag is off the methods reject with an
53
+ * explicit error and the events are not delivered to host subscribers,
54
+ * because every script that lives in the host page (analytics, GTM, ad
55
+ * pixels, session replay tools, supply-chain XSS) inherits the host's
56
+ * trust — and the host site's product/security team should explicitly
57
+ * accept the transcript-exposure risk before that capability is reachable.
58
+ */
59
+ enableProgrammaticControl?: boolean;
60
+ };
61
+ /**
62
+ * Local augmentation of the public AdaEmbedAPI surface for methods that are
63
+ * being rolled out via MES-981 ahead of a matching release of
64
+ * @ada-support/embed-types. Once those types are published, the methods below
65
+ * should be moved into the upstream interface and this file can be removed.
66
+ */
67
+ export interface ExtendedAdaEmbedAPI extends AdaEmbedAPI {
68
+ /**
69
+ * MES-981: widened start() overload that accepts the new ExtendedStartOptions
70
+ * (`headless`, `enableProgrammaticControl`) without requiring an `as`
71
+ * cast at the call site. Returns void to match the upstream signature.
72
+ */
73
+ start(adaSettings: ExtendedStartOptions): Promise<void>;
74
+ /**
75
+ * Convenience accessor for whether the chat drawer is currently open.
76
+ * Equivalent to `(await getInfo()).isChatOpen`.
77
+ *
78
+ * Rejects with `EMBED_NOT_INITIALIZED_ERROR` if called before `start()`.
79
+ *
80
+ * @example
81
+ * if (await adaEmbed.isOpen()) {
82
+ * // chat is visible
83
+ * }
84
+ */
85
+ isOpen(): Promise<boolean>;
86
+ /**
87
+ * Returns the metaFields currently held in embed-2's state. Reflects the
88
+ * value initially passed to `start({ metaFields })` plus any subsequent
89
+ * `setMetaFields` updates. Sensitive metaFields are deliberately not
90
+ * exposed.
91
+ *
92
+ * Rejects with `EMBED_NOT_INITIALIZED_ERROR` if called before `start()`.
93
+ */
94
+ getMetaFields(): Promise<MetaFieldPayload>;
95
+ /**
96
+ * Pre-fills the chat composer input with `text` without sending it.
97
+ * The value can still be edited or cleared by the end user.
98
+ *
99
+ * Rejects with `EMBED_NOT_INITIALIZED_ERROR` if called before `start()`,
100
+ * or with an `AdaEmbedError` if the chat frame is not yet ready.
101
+ */
102
+ setComposerText(text: string): Promise<void>;
103
+ /**
104
+ * Returns metadata about the current conversation: its id (or `null`
105
+ * if there is no active conversation), and the handoff state — whether
106
+ * the conversation is in live chat and, if so, basic info about the
107
+ * connected agent.
108
+ *
109
+ * Rejects with `EMBED_NOT_INITIALIZED_ERROR` if called before `start()`,
110
+ * or with an `AdaEmbedError` if the chat frame is not yet ready.
111
+ */
112
+ getConversation(): Promise<ConversationInfo>;
113
+ /**
114
+ * Returns the full in-memory message list for the current session.
115
+ * Generative has no server-side history endpoint, so this returns
116
+ * what the client has rendered (user, bot, and any handoff messages).
117
+ * Returns an empty array if no messages have been exchanged yet.
118
+ *
119
+ * Rejects with `EMBED_NOT_INITIALIZED_ERROR` if called before `start()`,
120
+ * or with an `AdaEmbedError` if the chat frame is not yet ready.
121
+ */
122
+ getMessages(): Promise<Message[]>;
123
+ /**
124
+ * Sends a free-text user message through the same path as the visible
125
+ * composer (POSTs to `/message/chat/`). Resolves with the generated
126
+ * message id so the caller can correlate it with later `getMessages()`
127
+ * reads.
128
+ *
129
+ * Generative does not support per-message metadata; user-level metadata
130
+ * is set via `setMetaFields`.
131
+ *
132
+ * Rejects with `EMBED_NOT_INITIALIZED_ERROR` if called before `start()`,
133
+ * an `AdaEmbedError` if the chat frame is not yet ready, or an
134
+ * `AdaEmbedError` if `text` is empty.
135
+ */
136
+ sendMessage(text: string): Promise<{
137
+ id: string;
138
+ }>;
139
+ /**
140
+ * Registers a host-side interceptor that runs in embed-2 before
141
+ * outgoing messages are forwarded to chat. Use `beforeSend` to
142
+ * modify, cancel, or pass through the user message. Set `beforeSend`
143
+ * to `undefined` to clear an existing delegate.
144
+ *
145
+ * The delegate may be synchronous or async. Returning `false` (or a
146
+ * Promise of `false`) cancels the send and causes `sendMessage` to
147
+ * reject with `DelegateRejected`.
148
+ */
149
+ setDelegate(delegate: Delegate): void;
150
+ /**
151
+ * MES-981: widened subscribeEvent overload to accept the new ada:* event
152
+ * keys until @ada-support/embed-types is republished. The upstream signature
153
+ * is preserved for the original key set via the union return type.
154
+ */
155
+ subscribeEvent<K extends ExtendedAdaEventKey>(eventKey: K, callback: K extends AdaEventKey ? AdaEventSubscriptionCallback<K> : K extends keyof ExtendedAdaEventDataKeyedByEvent ? (data: ExtendedAdaEventDataKeyedByEvent[K], context: AdaEventContext) => void : never): Promise<number>;
156
+ }
157
+ export type BeforeSend = (message: {
158
+ text: string;
159
+ }) => {
160
+ text: string;
161
+ } | false | Promise<{
162
+ text: string;
163
+ } | false>;
164
+ export interface Delegate {
165
+ beforeSend?: BeforeSend;
166
+ }
167
+ export interface ConversationInfo {
168
+ id: string | null;
169
+ handoff: {
170
+ inLiveChat: boolean;
171
+ agent?: {
172
+ id: string;
173
+ name: string;
174
+ avatarUrl?: string;
175
+ };
176
+ };
177
+ }
178
+ export type MessageRole = "user" | "bot" | "agent";
179
+ export interface Message {
180
+ id: string;
181
+ role: MessageRole;
182
+ type: string;
183
+ body?: string;
184
+ /** Unix epoch seconds (subsecond precision), as stored by the chat session. */
185
+ createdAt: number;
186
+ agent?: {
187
+ id: string;
188
+ name: string;
189
+ avatarUrl?: string;
190
+ };
191
+ }
@@ -1,5 +1,5 @@
1
- import type { AdaEmbedAPI } from "@ada-support/embed-types";
2
1
  import type { ActionTypes } from "common/constants/actions";
2
+ import type { ExtendedAdaEmbedAPI } from "./extended-api";
3
3
  import type { StoreState } from "./store-state";
4
4
  export * from "./interface";
5
5
  export interface StoreAction {
@@ -30,7 +30,7 @@ export interface TriggerCampaignParams {
30
30
  ignoreFrequency?: boolean;
31
31
  }
32
32
  export interface CustomWindow extends Window {
33
- adaEmbed: AdaEmbedAPI;
33
+ adaEmbed: ExtendedAdaEmbedAPI;
34
34
  navigator: Navigator;
35
35
  adaEmbedLoadedCallback?: () => void;
36
36
  }
@@ -65,4 +65,8 @@ export interface StartOptionsNoFunction {
65
65
  testMode?: boolean;
66
66
  preload?: boolean;
67
67
  showFallbackOnTimeout?: boolean;
68
+ /** MES-981: programmatic-only mode — entry UI is hidden, host renders its own surface */
69
+ headless?: boolean;
70
+ /** MES-981 P0 #4: opt-in for the transcript-bearing programmatic surface. */
71
+ enableProgrammaticControl?: boolean;
68
72
  }
@@ -1,2 +1,2 @@
1
- declare const _default: import("@ada-support/embed-types").AdaEmbedAPI;
1
+ declare const _default: import("./common/types/extended-api").ExtendedAdaEmbedAPI;
2
2
  export default _default;
@@ -15919,7 +15919,7 @@ const client = new BrowserClient({
15919
15919
  return event;
15920
15920
  },
15921
15921
  environment: "production",
15922
- release: "1.13.6-406ab08",
15922
+ release: "1.14.3-00422f5",
15923
15923
  sampleRate: 0.25,
15924
15924
  autoSessionTracking: false,
15925
15925
  // Integrations don't seem to work with Sentry: https://github.com/getsentry/sentry-javascript/issues/2541
@@ -16019,6 +16019,8 @@ const GET_INFO = "GET_INFO";
16019
16019
  const GET_INFO_RESPONSE = "GET_INFO_RESPONSE";
16020
16020
  const SET_META_FIELDS = "SET_META_FIELDS";
16021
16021
  const SET_META_FIELDS_RESPONSE = "SET_META_FIELDS_RESPONSE";
16022
+ const GET_META_FIELDS = "GET_META_FIELDS";
16023
+ const GET_META_FIELDS_RESPONSE = "GET_META_FIELDS_RESPONSE";
16022
16024
  const STOP = "STOP";
16023
16025
  const STOP_RESPONSE = "STOP_RESPONSE";
16024
16026
  const DELETE_HISTORY = "DELETE_HISTORY";
@@ -16058,6 +16060,12 @@ let FetchEventStatus = /*#__PURE__*/function (FetchEventStatus) {
16058
16060
 
16059
16061
 
16060
16062
 
16063
+ // MES-981 P1 #6 follow-up: request events whose responses are surfaced
16064
+ // directly to host-page callers. For these, channel.fetch() differentiates
16065
+ // timeouts (resolves to undefined) from other failures (re-throws with a
16066
+ // descriptive message) so the caller's "response === undefined" check
16067
+ // always means "timeout" specifically, not "something else went wrong".
16068
+ const PRIVILEGED_FETCH_EVENTS = new Set(["SEND_MESSAGE", "SET_COMPOSER_TEXT", "GET_MESSAGES", "GET_CONVERSATION"]);
16061
16069
  class Channel {
16062
16070
  constructor() {
16063
16071
  _defineProperty(this, "trackedListeners", void 0);
@@ -16111,6 +16119,21 @@ class Channel {
16111
16119
  throw new AdaEmbedError(`Failed to respond to "GET_STATE" request. Reason: "${e.message}".`);
16112
16120
  }
16113
16121
 
16122
+ // MES-981 P1 #6 follow-up: privileged transcript-bearing request/
16123
+ // response pairs must distinguish timeouts from other failures.
16124
+ // Without this, the "if (response === undefined)" check in
16125
+ // interface.ts throws a misleading "… timed out" error even when
16126
+ // the real failure was a payload-shape rejection or a handler
16127
+ // exception. Re-throw on any non-timeout error so the caller sees
16128
+ // the actual reason.
16129
+ if (PRIVILEGED_FETCH_EVENTS.has(requestEvent)) {
16130
+ if (e instanceof Error && e.message === "Could not connect frame channel.") {
16131
+ // genuine timeout — fall through, resolves to undefined
16132
+ } else {
16133
+ throw new AdaEmbedError(`"${requestEvent}" failed: ${e instanceof Error ? e.message : String(e)}`);
16134
+ }
16135
+ }
16136
+
16114
16137
  // handling promise
16115
16138
  if (e.message !== "Could not connect frame channel.") {
16116
16139
  error_tracker.trackException(e);
@@ -16395,14 +16418,25 @@ function subscribeEvent(eventKey, callback) {
16395
16418
  });
16396
16419
  return id;
16397
16420
  }
16421
+
16422
+ // MES-981 P2 #11: events that carry transcript bodies (or other
16423
+ // sensitive payloads) must match exactly, never via prefix. Prefix
16424
+ // subscriptions to `ada:` would otherwise leak `ada:message:sent` and
16425
+ // `ada:message:received` to anyone broadly subscribed to the `ada:`
16426
+ // namespace.
16427
+ const EXACT_MATCH_EVENT_KEYS = new Set(["ada:message:sent", "ada:message:received"]);
16398
16428
  function publishEvent(eventKey, data) {
16399
16429
  subscriptions.forEach(subscription => {
16400
- // Using startsWith instead of === allows subscribing to a specific event OR a namespace,
16401
- // e.g. ada:campaigns subscribes to ada:campaigns:opened, ada:campaigns:engaged, and
16402
- // ada:campaigns:shown. Broadcasting this way simultaneously reaches subscriptions
16403
- // that have subscribed to a specific event, and those that have subscribed to a namespace
16404
- if (eventKey.startsWith(subscription.eventKey)) {
16405
- // try/catch to guard against missing or bad callbacks
16430
+ const matches = EXACT_MATCH_EVENT_KEYS.has(eventKey) ?
16431
+ // Sensitive events require an exact subscription key match —
16432
+ // never deliver via a prefix subscription to e.g. "ada:" or
16433
+ // "ada:message".
16434
+ subscription.eventKey === eventKey :
16435
+ // Prefix match remains the default so subscribers can listen to
16436
+ // a namespace like `ada:campaigns` and catch
16437
+ // `ada:campaigns:opened` / `:engaged` / `:shown`.
16438
+ eventKey.startsWith(subscription.eventKey);
16439
+ if (matches) {
16406
16440
  try {
16407
16441
  subscription.callback(data, {
16408
16442
  eventKey
@@ -16509,7 +16543,7 @@ function getEmbedURL(_ref) {
16509
16543
  } else {
16510
16544
  host = `http://${handle}.localhost:${ports.localhost.default}`;
16511
16545
  }
16512
- return `${host}/embed/${frameName}/${"406ab08"}/index.html`;
16546
+ return `${host}/embed/${frameName}/${"00422f5"}/index.html`;
16513
16547
  }
16514
16548
  function constructQueryString(query) {
16515
16549
  return Object.keys(query).map(key => {
@@ -16720,6 +16754,15 @@ const mutations = (state, action) => {
16720
16754
  switch (action.type) {
16721
16755
  case ActionTypes.TOGGLE_CHAT_TYPE:
16722
16756
  {
16757
+ // MES-981 P1 #9 follow-up: defensive guard against future accidental
16758
+ // TOGGLE_CHAT_ACTION dispatches in headless mode. The host page has
16759
+ // no drawer to toggle, so flipping isDrawerOpen / drawerHasBeenOpened
16760
+ // and persisting `ada-embed_is-drawer-open: true` to localStorage
16761
+ // would leak across reloads with no visible effect. start() doesn't
16762
+ // dispatch this in headless any more, but the mutation stays robust.
16763
+ if (state.adaSettings.headless) {
16764
+ return state;
16765
+ }
16723
16766
  let {
16724
16767
  hasChatOpenedAfterProactiveMessagesShown
16725
16768
  } = state;
@@ -17011,6 +17054,8 @@ const getInitialState = adaSettings => ({
17011
17054
  language: undefined,
17012
17055
  greeting: undefined,
17013
17056
  crossWindowPersistence: true,
17057
+ headless: false,
17058
+ enableProgrammaticControl: false,
17014
17059
  hideMask: true,
17015
17060
  metaFields: {},
17016
17061
  sensitiveMetaFields: {},
@@ -17052,6 +17097,58 @@ const getInitialState = adaSettings => ({
17052
17097
  deviceToken: null,
17053
17098
  showFallbackOnTimeout: true
17054
17099
  });
17100
+ // EXTERNAL MODULE: ./node_modules/lodash.memoize/index.js
17101
+ var lodash_memoize = __webpack_require__(7654);
17102
+ var lodash_memoize_default = /*#__PURE__*/__webpack_require__.n(lodash_memoize);
17103
+ ;// CONCATENATED MODULE: ./src/services/logger/index.ts
17104
+
17105
+ function logger_ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
17106
+ function logger_objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? logger_ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : logger_ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
17107
+
17108
+
17109
+ const DEFAULT_LOG_SAMPLE_RATE = 0.01;
17110
+ const DD_BASE_URL = "https://browser-http-intake.logs.datadoghq.com/v1/input/";
17111
+ const DD_TOKEN = "pubfe23baedd2ea322bebb5ed2020fa2fa1";
17112
+ let globalLogContext = {};
17113
+ function setGlobalLogContext(context) {
17114
+ globalLogContext = logger_objectSpread({}, context);
17115
+ }
17116
+ function clearGlobalLogContext() {
17117
+ globalLogContext = {};
17118
+ }
17119
+
17120
+ // We need memoization to ensure consistency of logs
17121
+ const shouldLog = lodash_memoize_default()(sampleRate => {
17122
+ if (!DD_TOKEN || sampleRate === 0) {
17123
+ return false;
17124
+ }
17125
+ return Math.random() < (sampleRate || DEFAULT_LOG_SAMPLE_RATE);
17126
+ });
17127
+
17128
+ /**
17129
+ * Sends log to Datadog
17130
+ */
17131
+ async function log(message, extra, options) {
17132
+ if (!shouldLog(options === null || options === void 0 ? void 0 : options.sampleRate)) {
17133
+ return;
17134
+ }
17135
+ await httpRequest({
17136
+ url: `${DD_BASE_URL}${DD_TOKEN}?ddsource=browser&ddtags=version:1.5.0,env:${"production"}`,
17137
+ method: "POST",
17138
+ body: JSON.stringify(logger_objectSpread(logger_objectSpread(logger_objectSpread({
17139
+ message
17140
+ }, extra), globalLogContext), {}, {
17141
+ level: (options === null || options === void 0 ? void 0 : options.level) || "info",
17142
+ sampleRate: (options === null || options === void 0 ? void 0 : options.sampleRate) || DEFAULT_LOG_SAMPLE_RATE,
17143
+ service: "embed",
17144
+ env: "production",
17145
+ embedVersion: 2,
17146
+ version: "1.14.3",
17147
+ isNpm: true,
17148
+ commitHash: "00422f5"
17149
+ }))
17150
+ });
17151
+ }
17055
17152
  ;// CONCATENATED MODULE: ./src/common/helpers/get-intro-for-url.ts
17056
17153
  function getProcessedPath(path) {
17057
17154
  const regex = /^http(s)?:\/\/(www.)?/;
@@ -17651,58 +17748,6 @@ function getButtonText(client) {
17651
17748
  }
17652
17749
  return buttonTextMap[languageKey];
17653
17750
  }
17654
- // EXTERNAL MODULE: ./node_modules/lodash.memoize/index.js
17655
- var lodash_memoize = __webpack_require__(7654);
17656
- var lodash_memoize_default = /*#__PURE__*/__webpack_require__.n(lodash_memoize);
17657
- ;// CONCATENATED MODULE: ./src/services/logger/index.ts
17658
-
17659
- function logger_ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
17660
- function logger_objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? logger_ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : logger_ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
17661
-
17662
-
17663
- const DEFAULT_LOG_SAMPLE_RATE = 0.01;
17664
- const DD_BASE_URL = "https://browser-http-intake.logs.datadoghq.com/v1/input/";
17665
- const DD_TOKEN = "pubfe23baedd2ea322bebb5ed2020fa2fa1";
17666
- let globalLogContext = {};
17667
- function setGlobalLogContext(context) {
17668
- globalLogContext = logger_objectSpread({}, context);
17669
- }
17670
- function clearGlobalLogContext() {
17671
- globalLogContext = {};
17672
- }
17673
-
17674
- // We need memoization to ensure consistency of logs
17675
- const shouldLog = lodash_memoize_default()(sampleRate => {
17676
- if (!DD_TOKEN || sampleRate === 0) {
17677
- return false;
17678
- }
17679
- return Math.random() < (sampleRate || DEFAULT_LOG_SAMPLE_RATE);
17680
- });
17681
-
17682
- /**
17683
- * Sends log to Datadog
17684
- */
17685
- async function log(message, extra, options) {
17686
- if (!shouldLog(options === null || options === void 0 ? void 0 : options.sampleRate)) {
17687
- return;
17688
- }
17689
- await httpRequest({
17690
- url: `${DD_BASE_URL}${DD_TOKEN}?ddsource=browser&ddtags=version:1.5.0,env:${"production"}`,
17691
- method: "POST",
17692
- body: JSON.stringify(logger_objectSpread(logger_objectSpread(logger_objectSpread({
17693
- message
17694
- }, extra), globalLogContext), {}, {
17695
- level: (options === null || options === void 0 ? void 0 : options.level) || "info",
17696
- sampleRate: (options === null || options === void 0 ? void 0 : options.sampleRate) || DEFAULT_LOG_SAMPLE_RATE,
17697
- service: "embed",
17698
- env: "production",
17699
- embedVersion: 2,
17700
- version: "1.13.6",
17701
- isNpm: true,
17702
- commitHash: "406ab08"
17703
- }))
17704
- });
17705
- }
17706
17751
  ;// CONCATENATED MODULE: ./src/client/helpers/alternative-bot-rollout/index.ts
17707
17752
 
17708
17753
  function alternative_bot_rollout_ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
@@ -19263,6 +19308,12 @@ function deriveMessagingMobileShell(mobilePackage) {
19263
19308
  function deriveLegacyMobileShell(surface, hostPlatform) {
19264
19309
  return surface === "mobile" && hostPlatform !== "web" ? "legacy-sdk" : undefined;
19265
19310
  }
19311
+
19312
+ // MES-981 P1 #10: extracted to keep resolveHostTelemetryFromStartOptions
19313
+ // under the sonarjs cognitive-complexity threshold.
19314
+ function deriveMode(adaSettings) {
19315
+ return adaSettings.headless ? "headless" : undefined;
19316
+ }
19266
19317
  function normalizeHostTelemetry(value) {
19267
19318
  const record = asRecord(value);
19268
19319
  if (!record) {
@@ -19345,11 +19396,14 @@ function resolveHostTelemetryFromStartOptions(adaSettings) {
19345
19396
  const mobilePackage = explicit === null || explicit === void 0 ? void 0 : explicit.mobilePackage;
19346
19397
  const mobileVersion = explicit === null || explicit === void 0 ? void 0 : explicit.mobileVersion;
19347
19398
  const mobileShell = deriveMessagingMobileShell(mobilePackage) ?? (explicit === null || explicit === void 0 ? void 0 : explicit.mobileShell) ?? (legacyFingerprint === null || legacyFingerprint === void 0 ? void 0 : legacyFingerprint.mobileShell) ?? (!hasExplicitHostTelemetry ? deriveLegacyMobileShell(surface, hostPlatform) : undefined);
19348
- return host_telemetry_objectSpread(host_telemetry_objectSpread(host_telemetry_objectSpread({
19399
+ const mode = deriveMode(adaSettings);
19400
+ return host_telemetry_objectSpread(host_telemetry_objectSpread(host_telemetry_objectSpread(host_telemetry_objectSpread({
19349
19401
  surface,
19350
19402
  hostPlatform,
19351
19403
  webSdkOrigin
19352
- }, mobileShell ? {
19404
+ }, mode ? {
19405
+ mode
19406
+ } : {}), mobileShell ? {
19353
19407
  mobileShell
19354
19408
  } : {}), mobilePackage ? {
19355
19409
  mobilePackage
@@ -19367,7 +19421,11 @@ function toMonitoringContext(hostTelemetry) {
19367
19421
  return host_telemetry_objectSpread(host_telemetry_objectSpread(host_telemetry_objectSpread({
19368
19422
  surface: hostTelemetry.surface,
19369
19423
  host_platform: hostTelemetry.hostPlatform,
19370
- web_sdk_origin: hostTelemetry.webSdkOrigin
19424
+ web_sdk_origin: hostTelemetry.webSdkOrigin,
19425
+ // MES-981 P1 #10: every log/RUM line carries `mode`; default to
19426
+ // "visible" when not set on the context so dashboards can filter
19427
+ // by mode without a separate query.
19428
+ mode: hostTelemetry.mode ?? "visible"
19371
19429
  }, hostTelemetry.mobileShell ? {
19372
19430
  mobile_shell: hostTelemetry.mobileShell
19373
19431
  } : {}), hostTelemetry.mobilePackage ? {
@@ -19378,6 +19436,32 @@ function toMonitoringContext(hostTelemetry) {
19378
19436
  }
19379
19437
  ;// CONCATENATED MODULE: ./src/common/helpers/is-mobile.ts
19380
19438
  const isMobile = /(iPhone)|(iPod)|(android)|(webOS)/i.exec(navigator.userAgent) !== null;
19439
+ ;// CONCATENATED MODULE: ./src/common/helpers/programmatic-control.ts
19440
+
19441
+
19442
+ // MES-981: privileged transcript-bearing surface is opt-in. Host pages
19443
+ // must explicitly accept the exposure risk via
19444
+ // `start({ enableProgrammaticControl: true })`, because every third-party
19445
+ // script in the host page (analytics, GTM, ad pixels, session replay,
19446
+ // supply-chain XSS) inherits the host's trust.
19447
+ const PROGRAMMATIC_CONTROL_DISABLED_ERROR = new AdaEmbedError("ProgrammaticControlNotEnabled: this API requires `start({ enableProgrammaticControl: true })`.");
19448
+ function isProgrammaticControlEnabled(adaSettings) {
19449
+ return Boolean(adaSettings === null || adaSettings === void 0 ? void 0 : adaSettings.enableProgrammaticControl);
19450
+ }
19451
+ function requireProgrammaticControl(adaSettings) {
19452
+ if (!isProgrammaticControlEnabled(adaSettings)) {
19453
+ throw PROGRAMMATIC_CONTROL_DISABLED_ERROR;
19454
+ }
19455
+ }
19456
+
19457
+ // ada:message:sent and ada:message:received carry user/agent transcript
19458
+ // bodies. Other ada:* events (ready, connection, conversation, typing)
19459
+ // only carry id/state/timing data and are not subject to this gate.
19460
+ const TRANSCRIPT_EVENT_KEYS = new Set(["ada:message:sent", "ada:message:received"]);
19461
+ function shouldDeliverTranscriptEvent(eventKey, adaSettings) {
19462
+ if (!TRANSCRIPT_EVENT_KEYS.has(eventKey)) return true;
19463
+ return isProgrammaticControlEnabled(adaSettings);
19464
+ }
19381
19465
  ;// CONCATENATED MODULE: ./src/client/components/ChatFrame/chatFrameEvents/chatterEvent/index.ts
19382
19466
 
19383
19467
 
@@ -19548,6 +19632,31 @@ function getFallbackDataURI() {
19548
19632
 
19549
19633
 
19550
19634
 
19635
+
19636
+ // MES-981: route an inbound PUBLISH_EVENT from the chat iframe to the host's
19637
+ // event-subscription registry. Pulled out of handleChatEvent so its switch
19638
+ // stays under the sonarjs cognitive-complexity threshold.
19639
+ //
19640
+ // Two host-facing transforms happen here:
19641
+ // - shouldDeliverTranscriptEvent drops ada:message:* unless the host
19642
+ // opted in via enableProgrammaticControl (P0 #4).
19643
+ // - ada:ready is re-emitted with `{ mode: "headless" | "visible" }` so
19644
+ // subscribers can branch without inspecting adaSettings (P1 #10).
19645
+ function forwardPublishEvent(publishPayload, adaSettings) {
19646
+ const {
19647
+ eventKey,
19648
+ data
19649
+ } = publishPayload;
19650
+ if (!shouldDeliverTranscriptEvent(eventKey, adaSettings)) return;
19651
+ if (eventKey === "ada:ready") {
19652
+ const mode = adaSettings.headless ? "headless" : "visible";
19653
+ publishEvent(eventKey, {
19654
+ mode
19655
+ });
19656
+ return;
19657
+ }
19658
+ publishEvent(eventKey, data);
19659
+ }
19551
19660
  class ChatFrame extends preact_module_d {
19552
19661
  constructor() {
19553
19662
  super(...arguments);
@@ -19597,7 +19706,7 @@ class ChatFrame extends preact_module_d {
19597
19706
  log("Chat frame mount", {
19598
19707
  handle,
19599
19708
  chatUrl: this.url,
19600
- embedVersion: "406ab08".slice(0, 7),
19709
+ embedVersion: "00422f5".slice(0, 7),
19601
19710
  embedSettings: adaSettings
19602
19711
  });
19603
19712
 
@@ -19685,7 +19794,7 @@ class ChatFrame extends preact_module_d {
19685
19794
  const hostPageUrlParams = new URL(window.location.href).searchParams;
19686
19795
  const smsToken = hostPageUrlParams.get("adaSMSToken");
19687
19796
  const queryParams = {
19688
- embedVersion: "406ab08".slice(0, 7),
19797
+ embedVersion: "00422f5".slice(0, 7),
19689
19798
  greeting,
19690
19799
  language,
19691
19800
  skipGreeting,
@@ -20004,11 +20113,7 @@ class ChatFrame extends preact_module_d {
20004
20113
  }
20005
20114
  case PUBLISH_EVENT:
20006
20115
  {
20007
- const {
20008
- eventKey,
20009
- data
20010
- } = payload;
20011
- publishEvent(eventKey, data);
20116
+ forwardPublishEvent(payload, adaSettings);
20012
20117
  break;
20013
20118
  }
20014
20119
  case ANALYTICS_EVENT:
@@ -20658,10 +20763,36 @@ class XStorageFrame extends preact_module_d {
20658
20763
  }
20659
20764
  }
20660
20765
  /* harmony default export */ var components_XStorageFrame = (connect()(XStorageFrame));
20766
+ ;// CONCATENATED MODULE: ./src/client/components/Container/host-facing-meta-fields.ts
20767
+
20768
+ function host_facing_meta_fields_ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
20769
+ function host_facing_meta_fields_objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? host_facing_meta_fields_ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : host_facing_meta_fields_ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
20770
+ // MES-981 metaFields helpers. Extracted from Container/index.tsx so the
20771
+ // transforms don't push that already-large module over its max-lines budget.
20772
+
20773
+ /** Strips the internal `proactive_conversation` tracker for getMetaFields(). */
20774
+ function toHostFacingMetaFields(metaFields) {
20775
+ const hostFacing = host_facing_meta_fields_objectSpread({}, metaFields);
20776
+ delete hostFacing.proactive_conversation;
20777
+ return hostFacing;
20778
+ }
20779
+
20780
+ /** Merges a proactive_conversation key onto existing metaFields. */
20781
+ function withProactiveKey(metaFields, key) {
20782
+ return host_facing_meta_fields_objectSpread(host_facing_meta_fields_objectSpread({}, metaFields), {}, {
20783
+ proactive_conversation: key
20784
+ });
20785
+ }
20661
20786
  ;// CONCATENATED MODULE: ./src/client/components/Container/index.tsx
20662
20787
 
20663
20788
  function Container_ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
20664
20789
  function Container_objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? Container_ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : Container_ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
20790
+ /* eslint-disable max-lines -- MES-981: file was already at the 1000-line
20791
+ max-lines cap on main; the additive programmatic-control work pushes it
20792
+ just over. A targeted refactor of this Container is being tracked
20793
+ separately and is out of scope for this PR. */
20794
+
20795
+
20665
20796
 
20666
20797
 
20667
20798
 
@@ -20740,11 +20871,11 @@ class Container extends preact_module_d {
20740
20871
  adaSettings
20741
20872
  });
20742
20873
 
20743
- // Nullify any previous conversation's proactive_conversation metavariable value
20874
+ // MES-981: seed state.metaFields with host-supplied values + reset tracker.
20744
20875
  setGlobalState({
20745
- metaFields: {
20876
+ metaFields: Container_objectSpread(Container_objectSpread({}, adaSettings.metaFields ?? {}), {}, {
20746
20877
  proactive_conversation: null
20747
- }
20878
+ })
20748
20879
  });
20749
20880
 
20750
20881
  // We want to make sure this happens before any publishEvent calls
@@ -20853,7 +20984,9 @@ class Container extends preact_module_d {
20853
20984
  // campaigns, we don't need this for engage (response_id) campaigns
20854
20985
  campaignToTrigger.response_id || appConnectionState === ConnectionState.Done) && !isDrawerOpen && !chatterInLiveChat) {
20855
20986
  triggerCampaignImpl({
20856
- adaSettings,
20987
+ adaSettings: Container_objectSpread(Container_objectSpread({}, adaSettings), {}, {
20988
+ handle: (client === null || client === void 0 ? void 0 : client.handle) || adaSettings.handle
20989
+ }),
20857
20990
  chatterToken,
20858
20991
  campaign: campaignToTrigger,
20859
20992
  ignoreStatus: Boolean(campaignTriggerOptions.ignoreStatus),
@@ -20901,12 +21034,11 @@ class Container extends preact_module_d {
20901
21034
  if (isChatWebsocketConnected && isDrawerOpen && recentProactiveConversationConfig) {
20902
21035
  const chatChannel = messageService.getChannel(CHAT_IFRAME);
20903
21036
  if (chatChannel) {
20904
- // Set meta field to indicate that a proactive conversation has been injected
20905
- // This can be used by the AI manager to customize the greeting
21037
+ const {
21038
+ metaFields
21039
+ } = this.props;
20906
21040
  setGlobalState({
20907
- metaFields: {
20908
- proactive_conversation: recentProactiveConversationConfig.key
20909
- }
21041
+ metaFields: withProactiveKey(metaFields, recentProactiveConversationConfig.key)
20910
21042
  });
20911
21043
  chatChannel.postMessage(INJECT_PROACTIVE_CONVERSATION, {
20912
21044
  key: recentProactiveConversationConfig.key,
@@ -20961,6 +21093,13 @@ class Container extends preact_module_d {
20961
21093
  if (hideChatOverride) {
20962
21094
  return false;
20963
21095
  }
21096
+
21097
+ // MES-981: in headless mode the host page never opens the drawer, so
21098
+ // treat headless as an implicit preload to ensure the chat iframe loads
21099
+ // and registers its frame-channel handler.
21100
+ if (adaSettings.headless) {
21101
+ return true;
21102
+ }
20964
21103
  if (preload) {
20965
21104
  return true;
20966
21105
  }
@@ -21101,7 +21240,9 @@ class Container extends preact_module_d {
21101
21240
  if (!client) {
21102
21241
  throw new AdaEmbedError("`client` is not defined");
21103
21242
  }
21104
- const campaignToTrigger = getCampaignToTrigger(adaSettings, client.marketing_campaigns, params, !client.features.afm_proactive_messaging);
21243
+ const campaignToTrigger = getCampaignToTrigger(Container_objectSpread(Container_objectSpread({}, adaSettings), {}, {
21244
+ handle: client.handle
21245
+ }), client.marketing_campaigns, params, !client.features.afm_proactive_messaging);
21105
21246
  if (campaignToTrigger) {
21106
21247
  if (useDelay) {
21107
21248
  setTimeout(() => {
@@ -21238,9 +21379,13 @@ class Container extends preact_module_d {
21238
21379
  drawerHasBeenOpened,
21239
21380
  chatterToken
21240
21381
  } = this.props;
21382
+ // MES-981 P1 #9: in headless mode the UI is intentionally hidden,
21383
+ // so `isChatOpen` always returns false regardless of any drawer
21384
+ // state. Matches the contract that isOpen() returns in headless.
21385
+ const isHeadless = Boolean(adaSettings.headless);
21241
21386
  const contentToReturn = {
21242
- isDrawerOpen,
21243
- isChatOpen: isDrawerOpen || Boolean(parentElement),
21387
+ isDrawerOpen: isHeadless ? false : isDrawerOpen,
21388
+ isChatOpen: isHeadless ? false : isDrawerOpen || Boolean(parentElement),
21244
21389
  hasActiveChatter: Boolean(chatterToken),
21245
21390
  hasClosedChat: drawerHasBeenOpened && !isDrawerOpen
21246
21391
  };
@@ -21261,6 +21406,14 @@ class Container extends preact_module_d {
21261
21406
  localChannel.postMessage(SET_META_FIELDS_RESPONSE, null, id);
21262
21407
  break;
21263
21408
  }
21409
+ case GET_META_FIELDS:
21410
+ {
21411
+ const {
21412
+ metaFields
21413
+ } = this.props;
21414
+ localChannel.postMessage(GET_META_FIELDS_RESPONSE, toHostFacingMetaFields(metaFields), id);
21415
+ break;
21416
+ }
21264
21417
  case "SET_SENSITIVE_META_FIELDS":
21265
21418
  {
21266
21419
  const {
@@ -21826,6 +21979,7 @@ function embed_objectSpread(e) { for (var r = 1; r < arguments.length; r++) { va
21826
21979
 
21827
21980
 
21828
21981
 
21982
+
21829
21983
  function renderApp(_ref) {
21830
21984
  let {
21831
21985
  adaSettings,
@@ -21850,6 +22004,25 @@ function renderApp(_ref) {
21850
22004
  if (!parentContainer) {
21851
22005
  throw new AdaEmbedError("`parentContainer` is null");
21852
22006
  }
22007
+
22008
+ // MES-981: in headless mode the React tree still mounts (so the websocket,
22009
+ // Redux, and FC handlers run) but the entry container is moved off-screen
22010
+ // — the host page controls its own UI on top of the programmatic surface.
22011
+ // We use absolute positioning + 0x0 + clip instead of `display: none`
22012
+ // because Chrome (and Safari) defer iframe loads for elements that are
22013
+ // display:none, which would block the chat iframe from ever registering
22014
+ // its frame-channel handlers.
22015
+ if (adaSettings.headless) {
22016
+ entryContainer.style.position = "absolute";
22017
+ entryContainer.style.left = "0";
22018
+ entryContainer.style.top = "0";
22019
+ entryContainer.style.width = "0";
22020
+ entryContainer.style.height = "0";
22021
+ entryContainer.style.overflow = "hidden";
22022
+ entryContainer.style.clip = "rect(0, 0, 0, 0)";
22023
+ entryContainer.style.pointerEvents = "none";
22024
+ entryContainer.setAttribute("aria-hidden", "true");
22025
+ }
21853
22026
  parentContainer.appendChild(entryContainer);
21854
22027
  M(v(StoreContext.Provider, {
21855
22028
  value: store
@@ -21899,6 +22072,31 @@ function createEmbed(adaSettings) {
21899
22072
  initializationReject: reject
21900
22073
  });
21901
22074
  });
22075
+
22076
+ // MES-981 P1 #10: emit a high-priority log entry on every headless
22077
+ // session start so ops can spot abuse patterns (compromised host site
22078
+ // silently running chatter sessions under the visiting user's token).
22079
+ // Sampled at 1.0 so we never miss one; the volume is bounded by the
22080
+ // host explicitly opting in.
22081
+ if (adaSettings.headless) {
22082
+ log("headless_session_initialized", {
22083
+ handle: adaSettings.handle,
22084
+ parent_origin: window.location.origin
22085
+ }, {
22086
+ sampleRate: 1,
22087
+ level: "info"
22088
+ });
22089
+ }
22090
+
22091
+ // MES-981 P1 #9: headless boot no longer reuses TOGGLE_CHAT_ACTION.
22092
+ // The old dispatch persisted drawer-open state to localStorage and set
22093
+ // `drawerHasBeenOpened` / `focusIsOnAda` even though there is no
22094
+ // visible drawer — which then leaked across reloads. The chat iframe
22095
+ // mounts directly via `shouldRenderChatFrame` returning true for
22096
+ // headless (Container/index.tsx), and the iframe's own init drives
22097
+ // chatter creation, pusher subscription, and greeting fire. No drawer
22098
+ // state needs to flip.
22099
+
21902
22100
  const debounceCampaignTrigger = lodash_debounce_default()(async function (campaignKey) {
21903
22101
  let triggerCampaignParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
21904
22102
  await initialized;
@@ -21921,6 +22119,10 @@ function createEmbed(adaSettings) {
21921
22119
  await initialized;
21922
22120
  await localChannel.fetch(SET_META_FIELDS, SET_META_FIELDS_RESPONSE, options);
21923
22121
  },
22122
+ async getMetaFields() {
22123
+ await initialized;
22124
+ return await localChannel.fetch(GET_META_FIELDS, GET_META_FIELDS_RESPONSE);
22125
+ },
21924
22126
  async setSensitiveMetaFields(options) {
21925
22127
  await initialized;
21926
22128
  await localChannel.fetch("SET_SENSITIVE_META_FIELDS", "SET_SENSITIVE_META_FIELDS_RESPONSE", options);
@@ -21990,6 +22192,30 @@ function createEmbed(adaSettings) {
21990
22192
  }
21991
22193
  };
21992
22194
  }
22195
+ ;// CONCATENATED MODULE: ./src/common/helpers/with-timeout.ts
22196
+
22197
+
22198
+ /**
22199
+ * Race `promise` against a timeout. If the timeout fires first, reject
22200
+ * with an AdaEmbedError carrying `errorMessage`. If the promise settles
22201
+ * first (resolve or reject), clear the pending timer so it doesn't
22202
+ * keep the event loop alive past resolution.
22203
+ *
22204
+ * MES-981 P1 #7: used to cap host-supplied beforeSend hooks so a
22205
+ * never-resolving delegate can't hang sendMessage forever.
22206
+ */
22207
+ function withTimeout(promise, ms, errorMessage) {
22208
+ return new Promise((resolve, reject) => {
22209
+ const handle = setTimeout(() => reject(new AdaEmbedError(errorMessage)), ms);
22210
+ Promise.resolve(promise).then(value => {
22211
+ clearTimeout(handle);
22212
+ resolve(value);
22213
+ }, err => {
22214
+ clearTimeout(handle);
22215
+ reject(err);
22216
+ });
22217
+ });
22218
+ }
21993
22219
  ;// CONCATENATED MODULE: ./src/interface.ts
21994
22220
 
21995
22221
 
@@ -21998,13 +22224,56 @@ function createEmbed(adaSettings) {
21998
22224
 
21999
22225
 
22000
22226
 
22227
+
22228
+
22001
22229
  const EMBED_NOT_INITIALIZED_ERROR = new AdaEmbedError("Actions cannot be called until Embed has been instantiated. Try running `adaEmbed.start({...})`.");
22002
22230
 
22231
+ // MES-981 P1 #7: run the host's beforeSend hook with a 5s timeout, validate
22232
+ // its return value, and surface DelegateRejected / DelegateTimeout / shape
22233
+ // errors with the same wording as before. Extracted from sendMessage to
22234
+ // keep that method below the sonarjs cognitive-complexity threshold.
22235
+ async function applyBeforeSend(beforeSend, originalText) {
22236
+ let result;
22237
+ try {
22238
+ result = await withTimeout(beforeSend({
22239
+ text: originalText
22240
+ }), 5000, "DelegateTimeout");
22241
+ } catch (err) {
22242
+ if (err instanceof AdaEmbedError && err.message === "DelegateTimeout") {
22243
+ log("Delegate beforeSend timed out", {
22244
+ timeoutMs: 5000
22245
+ });
22246
+ } else {
22247
+ log("Delegate beforeSend threw", {
22248
+ error: err instanceof Error ? err.message : String(err)
22249
+ });
22250
+ }
22251
+ throw err;
22252
+ }
22253
+ if (result === false) {
22254
+ log("Delegate beforeSend canceled the send", {});
22255
+ throw new AdaEmbedError("DelegateRejected");
22256
+ }
22257
+ const candidate = result;
22258
+ if (!candidate || typeof candidate !== "object" || typeof candidate.text !== "string") {
22259
+ throw new AdaEmbedError("Delegate beforeSend must return `{ text: string }`, `false`, or a Promise resolving to one of these.");
22260
+ }
22261
+ const transformed = candidate.text;
22262
+ if (transformed.trim().length === 0) {
22263
+ throw new AdaEmbedError("sendMessage requires non-empty text after delegate transform");
22264
+ }
22265
+ log("Delegate beforeSend completed", {
22266
+ transformed: transformed !== originalText
22267
+ });
22268
+ return transformed;
22269
+ }
22270
+
22003
22271
  /**
22004
22272
  * Returns the public Embed methods to be bound to the window object.
22005
22273
  */
22006
22274
  function createEmbedInterface() {
22007
22275
  let embed = null;
22276
+ let delegate = {};
22008
22277
  const toggle = async () => {
22009
22278
  if (!embed) {
22010
22279
  throw EMBED_NOT_INITIALIZED_ERROR;
@@ -22048,6 +22317,9 @@ function createEmbedInterface() {
22048
22317
  }
22049
22318
  const stopPromise = embed.stop();
22050
22319
  embed = null;
22320
+ // MES-981: clear the registered delegate so a subsequent start() begins
22321
+ // with a clean interception state.
22322
+ delegate = {};
22051
22323
  try {
22052
22324
  await stopPromise;
22053
22325
  } finally {
@@ -22067,6 +22339,12 @@ function createEmbedInterface() {
22067
22339
  }
22068
22340
  return embed.setMetaFields(options);
22069
22341
  },
22342
+ getMetaFields: async () => {
22343
+ if (!embed) {
22344
+ throw EMBED_NOT_INITIALIZED_ERROR;
22345
+ }
22346
+ return embed.getMetaFields();
22347
+ },
22070
22348
  setSensitiveMetaFields: async options => {
22071
22349
  if (!embed) {
22072
22350
  throw EMBED_NOT_INITIALIZED_ERROR;
@@ -22079,6 +22357,20 @@ function createEmbedInterface() {
22079
22357
  }
22080
22358
  return embed.getInfo();
22081
22359
  },
22360
+ isOpen: async () => {
22361
+ if (!embed) {
22362
+ throw EMBED_NOT_INITIALIZED_ERROR;
22363
+ }
22364
+
22365
+ // MES-981: in headless mode the entry container is display:none, so
22366
+ // even a parentElement-mounted embed isn't visible to the user. Treat
22367
+ // it as "not open" regardless of the underlying isDrawerOpen flag.
22368
+ if (embed.adaSettings.headless) {
22369
+ return false;
22370
+ }
22371
+ const info = await embed.getInfo();
22372
+ return info.isChatOpen;
22373
+ },
22082
22374
  reset: async resetParams => {
22083
22375
  if (!embed) {
22084
22376
  throw EMBED_NOT_INITIALIZED_ERROR;
@@ -22120,6 +22412,12 @@ function createEmbedInterface() {
22120
22412
  }
22121
22413
  return embed.evaluateCampaignConditions(options);
22122
22414
  },
22415
+ // MES-981: the public surface accepts both the upstream AdaEventKey set
22416
+ // and the new ada:* keys (ExtendedAdaEventKey). The underlying event
22417
+ // subscription registry is a string-keyed map at runtime, so casting
22418
+ // through any is safe and lets the implementation reuse the existing
22419
+ // subscribeEvent helper.
22420
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22123
22421
  subscribeEvent: async (eventKey, callback) => subscribeEvent(eventKey, callback),
22124
22422
  unsubscribeEvent: async id => unsubscribeEvent(id),
22125
22423
  closeCampaign: async () => {
@@ -22146,6 +22444,126 @@ function createEmbedInterface() {
22146
22444
  language
22147
22445
  });
22148
22446
  },
22447
+ setComposerText: async text => {
22448
+ if (!embed) {
22449
+ throw EMBED_NOT_INITIALIZED_ERROR;
22450
+ }
22451
+ requireProgrammaticControl(embed.adaSettings);
22452
+ if (embed.adaSettings.headless) {
22453
+ throw new AdaEmbedError("HeadlessModeError: setComposerText is not available in headless mode");
22454
+ }
22455
+
22456
+ // MES-981: wait for chat to finish init before forwarding the
22457
+ // postMessage, matching the existing behaviour of setMetaFields etc.
22458
+ await embed.initialized;
22459
+ const channel = embed.messageService.getChannel("chat");
22460
+ if (!channel) {
22461
+ throw new AdaEmbedError("Chat is not ready yet to have composer text set.");
22462
+ }
22463
+
22464
+ // MES-981 P0 #3: SET_COMPOSER_TEXT is request/response so chat-side
22465
+ // validation failures (INVALID_PAYLOAD, TOO_LONG) reach the caller.
22466
+ const response = await channel.fetch("SET_COMPOSER_TEXT", "SET_COMPOSER_TEXT_RESPONSE", {
22467
+ text
22468
+ });
22469
+ if (!response) {
22470
+ throw new AdaEmbedError("setComposerText timed out — chat frame did not respond in time.");
22471
+ }
22472
+ if ("error" in response) {
22473
+ throw new AdaEmbedError(`setComposerText rejected by chat: ${response.error}`);
22474
+ }
22475
+ },
22476
+ getConversation: async () => {
22477
+ if (!embed) {
22478
+ throw EMBED_NOT_INITIALIZED_ERROR;
22479
+ }
22480
+
22481
+ // MES-981 P0 #4: getConversation returns the live-agent handoff
22482
+ // identity (id / name / avatarUrl) alongside the conversation id.
22483
+ // Same host-page exposure surface as ada:message:* and getMessages,
22484
+ // so the same opt-in gate applies.
22485
+ requireProgrammaticControl(embed.adaSettings);
22486
+ await embed.initialized;
22487
+ const channel = embed.messageService.getChannel("chat");
22488
+ if (!channel) {
22489
+ throw new AdaEmbedError("Chat is not ready yet to read conversation.");
22490
+ }
22491
+ const response = await channel.fetch("GET_CONVERSATION", "GET_CONVERSATION_RESPONSE");
22492
+ if (!response) {
22493
+ throw new AdaEmbedError("getConversation timed out — chat frame did not respond in time.");
22494
+ }
22495
+ return response;
22496
+ },
22497
+ getMessages: async () => {
22498
+ if (!embed) {
22499
+ throw EMBED_NOT_INITIALIZED_ERROR;
22500
+ }
22501
+ requireProgrammaticControl(embed.adaSettings);
22502
+ await embed.initialized;
22503
+ const channel = embed.messageService.getChannel("chat");
22504
+ if (!channel) {
22505
+ throw new AdaEmbedError("Chat is not ready yet to read messages.");
22506
+ }
22507
+ const response = await channel.fetch("GET_MESSAGES", "GET_MESSAGES_RESPONSE");
22508
+
22509
+ // MES-981 P1 #6: channel.fetch resolves to undefined on timeout.
22510
+ // The previous behavior normalized that to [], which made a real
22511
+ // chat-frame failure indistinguishable from an empty transcript.
22512
+ // Fail loudly instead so hosts can branch on the error.
22513
+ if (response === undefined) {
22514
+ throw new AdaEmbedError("getMessages timed out — chat frame did not respond in time.");
22515
+ }
22516
+ return response;
22517
+ },
22518
+ sendMessage: async text => {
22519
+ if (!embed) {
22520
+ throw EMBED_NOT_INITIALIZED_ERROR;
22521
+ }
22522
+ requireProgrammaticControl(embed.adaSettings);
22523
+
22524
+ // MES-981: reject empty AND whitespace-only text so we never POST a
22525
+ // meaningless body to /message/chat/.
22526
+ if (typeof text !== "string" || text.trim().length === 0) {
22527
+ throw new AdaEmbedError("sendMessage requires non-empty text");
22528
+ }
22529
+
22530
+ // MES-981: gate on init BEFORE running the host's beforeSend hook.
22531
+ // Otherwise an init failure (or a missing chat channel) leaves the
22532
+ // host with delegate side-effects — analytics logs, UI optimism — for
22533
+ // a message that can never be sent. Match the gating order used by
22534
+ // every other public method on this surface.
22535
+ await embed.initialized;
22536
+ const channel = embed.messageService.getChannel("chat");
22537
+ if (!channel) {
22538
+ throw new AdaEmbedError("Chat is not ready yet to send a message.");
22539
+ }
22540
+ const {
22541
+ beforeSend
22542
+ } = delegate;
22543
+ const finalText = beforeSend ? await applyBeforeSend(beforeSend, text) : text;
22544
+ const response = await channel.fetch("SEND_MESSAGE", "SEND_MESSAGE_RESPONSE", {
22545
+ text: finalText
22546
+ });
22547
+ if (!response) {
22548
+ throw new AdaEmbedError("sendMessage timed out — chat frame did not respond in time.");
22549
+ }
22550
+
22551
+ // MES-981 P0 #2: chat-side validation/rate-limit/dispatch failures
22552
+ // come back as `{ error: <code> }`. Surface the code to the caller
22553
+ // so they can branch on machine-readable reasons (TOO_LONG,
22554
+ // RATE_LIMITED, EMPTY_TEXT, INVALID_PAYLOAD, DISPATCH_FAILED).
22555
+ if ("error" in response) {
22556
+ throw new AdaEmbedError(`sendMessage rejected by chat: ${response.error}`);
22557
+ }
22558
+ return response;
22559
+ },
22560
+ setDelegate: newDelegate => {
22561
+ if (!embed) {
22562
+ throw EMBED_NOT_INITIALIZED_ERROR;
22563
+ }
22564
+ requireProgrammaticControl(embed.adaSettings);
22565
+ delegate = newDelegate ?? {};
22566
+ },
22149
22567
  handleNotification: async () => {
22150
22568
  if (!embed) {
22151
22569
  throw EMBED_NOT_INITIALIZED_ERROR;
@@ -1,7 +1,7 @@
1
- import type { AdaEmbedAPI } from "@ada-support/embed-types";
2
1
  import { AdaEmbedError } from "common/helpers/errors";
2
+ import type { ExtendedAdaEmbedAPI } from "common/types/extended-api";
3
3
  export declare const EMBED_NOT_INITIALIZED_ERROR: AdaEmbedError;
4
4
  /**
5
5
  * Returns the public Embed methods to be bound to the window object.
6
6
  */
7
- export declare function createEmbedInterface(): AdaEmbedAPI;
7
+ export declare function createEmbedInterface(): ExtendedAdaEmbedAPI;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ada-support/embed2",
3
- "version": "1.13.6",
3
+ "version": "1.14.3",
4
4
  "description": "",
5
5
  "main": "dist/npm-entry",
6
6
  "typings": "dist/npm-entry/index-npm.d.ts",