@ada-cx/messaging-sdk 1.0.1 → 1.2.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.
@@ -18,6 +18,31 @@ export declare const FRAME_PACKAGES: Record<FrameName, "sdk" | "integrations">;
18
18
  * inside the chat.
19
19
  */
20
20
  export declare const FRAME_TITLES: Record<FrameName, string>;
21
+ /**
22
+ * Element ids embed-2 put on its frames, kept alive here for the frames whose
23
+ * role is unchanged.
24
+ *
25
+ * embed-2 set `id`/`name`/`title`/`className` to `ada-<name>-frame` on every
26
+ * frame it mounted, and hiding `#ada-button-frame` was the ONLY way to run a
27
+ * custom launcher — it exposed no `hideChatButton` setting. So a host that draws
28
+ * its own launcher reaches for that id, and dropping it silently leaves two
29
+ * buttons on the page (EXP-1180; observed on a live customer).
30
+ *
31
+ * Only the launcher is listed. `core` is not the old `#ada-chat-frame`: that was
32
+ * the visible drawer, whereas core is a headless frame that merely contains one,
33
+ * so inheriting the id would apply a host's drawer sizing to the wrong element.
34
+ * The same reasoning excludes `intro`, the mask (no longer a frame at all) and
35
+ * the entry container. Add an entry here only where the element's role genuinely
36
+ * survived the rewrite.
37
+ *
38
+ * `id`, `className` and `name` are all restored, because embed-2 set all three to
39
+ * the same value and any of them can be the selector a host kept. `title` is NOT:
40
+ * embed-2 used it as a fourth copy of the id, and reinstating that would replace
41
+ * the launcher's accessible name with "ada-button-frame" (axe `frame-title`,
42
+ * WCAG 4.1.2). Nothing reads the launcher's `window.name`, so `name` is inert
43
+ * internally.
44
+ */
45
+ export declare const LEGACY_FRAME_ELEMENT_IDS: Partial<Record<FrameName, string>>;
21
46
  /**
22
47
  * One name for every integration runtime. They are headless and permanently 0×0, so
23
48
  * a reader never lands in one and there is nothing to tell them apart by.
@@ -12,6 +12,10 @@ export interface DeliverConfigRuntimeOptions {
12
12
  identityToken?: string;
13
13
  greetingResponseId?: string | null;
14
14
  metaFields?: Record<string, unknown>;
15
+ /** Immutable snapshot of the host's initial metaFields, delivered so Core seeds
16
+ * the opt-out reset baseline from it rather than from the runtime `metaFields`
17
+ * (which `setMetaFields()` mutates). (SUP-42) */
18
+ metaFieldsBaseline?: Record<string, unknown>;
15
19
  sensitiveMetaFields?: Record<string, unknown>;
16
20
  allowMetaFieldsInReset?: boolean;
17
21
  enableProgrammaticControl?: boolean;
@@ -32,6 +36,13 @@ export interface DeliverConfigRuntimeOptions {
32
36
  clearPersistenceStorageModes?: Array<"normal" | "session" | "private">;
33
37
  /** Sticky SDK-owned fact matching legacy Embed2 `wasIntroShown`. */
34
38
  introShown?: boolean;
39
+ /**
40
+ * SDK-owned current page URL; `null` on the standalone `chat.html` surface, where
41
+ * legacy direct `/chat` records no initial URL. Defaults to the current href.
42
+ */
43
+ initialURL?: string | null;
44
+ /** Internal (EXP-1082): the mobile webview runtime mirrors session state natively. */
45
+ nativeSessionMirror?: boolean;
35
46
  }
36
47
  /** Send the client configuration to a frame. */
37
48
  export declare function deliverConfigRuntime(channel: FrameChannel, config: DeliverConfigRuntimeOptions): void;
@@ -46,6 +57,7 @@ export type EventSubscriber = {
46
57
  wildcard: true;
47
58
  callback: (key: string, data: unknown) => void;
48
59
  };
60
+ export declare const EXACT_MATCH_EVENT_KEYS: ReadonlySet<string>;
49
61
  export declare function subscriberMatchesEventKeyRuntime(subscriber: EventSubscriber, key: string): boolean;
50
62
  /** Route a published event key to all matching subscribeEvent subscribers. */
51
63
  export declare function routePublishedEventRuntime(subscribers: Map<number, EventSubscriber>, key: string, data: unknown): void;
@@ -37,7 +37,10 @@ export interface AdaMessagingSettings {
37
37
  * "eu") combined with `domain`.
38
38
  */
39
39
  cluster?: string | null;
40
- /** Language code (e.g. "en", "fr") */
40
+ /**
41
+ * Language code (e.g. "en", "fr"). Applies on every page load and takes
42
+ * precedence over a saved language choice. Omit to restore a saved choice.
43
+ */
41
44
  language?: string;
42
45
  /** Answer response id to trigger as the initial greeting (legacy CDN parity). */
43
46
  greeting?: string;
@@ -68,10 +71,11 @@ export interface AdaMessagingSettings {
68
71
  /** Metafield key/value pairs passed to the bot */
69
72
  metaFields?: Record<string, MetaFieldValue>;
70
73
  /**
71
- * When true, `reset({ metaFields })` may apply caller-supplied metaFields.
72
- * Defaults to false so an end user cannot inject or override metaFields by
73
- * calling `reset()` (e.g. from the browser console). The value is locked on
74
- * the first config and cannot be changed at runtime. (MES-542)
74
+ * Whether `reset({ metaFields })` may apply caller-supplied metaFields.
75
+ * Defaults to allow (omit or set `true`); set `false` so an end user cannot
76
+ * inject or override metaFields by calling `reset()` (e.g. from the browser
77
+ * console). The value is locked on the first config and cannot be changed at
78
+ * runtime. (SUP-42 / MES-542)
75
79
  */
76
80
  allowMetaFieldsInReset?: boolean;
77
81
  /**
@@ -30,6 +30,7 @@ import type { AdaMessagingClientConfig, AdaMessagingStartOptions, AdaSdkEventCal
30
30
  declare class AdaMessagingClient {
31
31
  private _config;
32
32
  private _assetRolloutCluster;
33
+ private _hostMetaFieldsBaseline;
33
34
  private _frameManager;
34
35
  private readonly MAX_PENDING_MESSAGES;
35
36
  private _pendingCoreMessages;
@@ -113,6 +114,14 @@ declare class AdaMessagingClient {
113
114
  private _pendingCrossWindowDrawerRestore;
114
115
  /** Host-provided push token, retained independently of chatter-sensitive metadata. */
115
116
  private _deviceToken;
117
+ /**
118
+ * Push device OS resolved from the host `sdkType` at setDeviceToken() REGISTRATION and
119
+ * retained in this trusted slot — like core's `session.deviceOs` and embed-2's recorded
120
+ * slot. A reset that wipes `sdkType` from the runtime metaFields would otherwise
121
+ * re-derive WEB on the fallback reload and re-tag a native APNs/FCM token as a web
122
+ * device; sourcing from this slot keeps the registration-time OS. (SUP-42)
123
+ */
124
+ private _deviceOs;
116
125
  /**
117
126
  * Chatter whose unread count the user has already acknowledged by opening the
118
127
  * chat. An in-flight seed carries a pre-clear count, so it has to lose to this
@@ -137,6 +146,16 @@ declare class AdaMessagingClient {
137
146
  private readonly _registeredInternalEventCallbacks;
138
147
  private _crossWindowLiveChatActive;
139
148
  private _cleanupErrorReporter;
149
+ /**
150
+ * Reports never enter `_pendingCoreMessages`: that queue is capped, evicts
151
+ * control messages such as `sdk.activate` on overflow, and dispatches
152
+ * `ada:error` on the host page for each eviction. A host whose callback
153
+ * throws before core mounts would otherwise break Ada's own startup delivery.
154
+ * Reports wait in their own small buffer and excess ones are dropped quietly.
155
+ */
156
+ private readonly MAX_PENDING_HOST_CALLBACK_ERRORS;
157
+ private _pendingHostCallbackErrors;
158
+ private readonly _hostCallbackErrorSink;
140
159
  private _cleanupCoreChannelHandlers;
141
160
  private _cleanupServiceWorkerNotificationClick;
142
161
  private _chatActivationEventTimer;
@@ -181,6 +200,9 @@ declare class AdaMessagingClient {
181
200
  private _introSurfaceSeq;
182
201
  private _currentIntroSurface;
183
202
  private _internalHostTelemetry?;
203
+ private _isStandaloneChatPage;
204
+ /** Internal (EXP-1082): set only via the webview runtime's armed grant. */
205
+ private _nativeSessionMirror;
184
206
  private _onIdentityTokenConsumed?;
185
207
  private _isEmbedLoadedPublicationActive?;
186
208
  private _zdChatterAuthCallback;
@@ -250,6 +272,12 @@ declare class AdaMessagingClient {
250
272
  private _acknowledgeDurableClearIntent;
251
273
  private _clearDurableClearIntent;
252
274
  private _abandonDurableClearIntent;
275
+ /**
276
+ * `_setConfig` normalizes `privateMode` to a boolean once; every private-mode
277
+ * read goes through here so the SDK's storage scope, candidate discovery and the
278
+ * `sdk.config.set` payload cannot disagree.
279
+ */
280
+ private get _isPrivateMode();
253
281
  private _persistenceScopeKey;
254
282
  private _outgoingPersistenceMode;
255
283
  /**
@@ -589,6 +617,17 @@ declare class AdaMessagingClient {
589
617
  private _openProactiveCtaUrl;
590
618
  /** Send the client configuration to a frame. */
591
619
  private _deliverConfig;
620
+ /**
621
+ * Internal (EXP-1082): route a native `ada.sessionMirrorClearAck` to core's
622
+ * pending durable-clear barrier. Called by the mobile webview runtime only.
623
+ */
624
+ acknowledgeSessionMirrorClear(requestId: string): void;
625
+ /**
626
+ * Internal (EXP-1107): route a native boot-seed reply (`sdk.sessionMirror.seed`)
627
+ * to core's pending `requestSeed()` barrier. Called by the mobile webview
628
+ * runtime only. `seed` is the mirror blob native read live, or null when absent.
629
+ */
630
+ deliverSessionMirrorSeed(requestId: string, seed: unknown): void;
592
631
  /** Safely invoke a window-level callback if it exists and is a function */
593
632
  private _invokeWindowCallback;
594
633
  /**
@@ -730,6 +769,13 @@ declare class AdaMessagingClient {
730
769
  private _applyResetOverridesToConfig;
731
770
  private _sendPendingHistoryPreservingReset;
732
771
  private _markCoreProtocolReady;
772
+ /**
773
+ * Freeze the host's declared metaFields as the immutable opt-out reset baseline.
774
+ * Called from the constructor and from start() when the host explicitly supplies
775
+ * metaFields — never from setMetaFields(), which is the ungated runtime laundering
776
+ * path the baseline must resist. (SUP-42)
777
+ */
778
+ private _captureHostMetaFieldsBaseline;
733
779
  private _applyStartOverrides;
734
780
  private _subscribeInternalEvent;
735
781
  private _registerInternalEventCallbacks;
@@ -801,6 +847,21 @@ declare class AdaMessagingClient {
801
847
  private _markGreetingIntroShown;
802
848
  /** Patch only the current chatter when an accepted proactive is removed. */
803
849
  private _markProactiveIntroShown;
850
+ /**
851
+ * Legacy direct `/chat` records no initial URL, so the SDK's own full-page entry
852
+ * points (`chat.html`, `webview.html`) send `null` rather than their own URL, whose
853
+ * query carries the host-supplied meta fields. A page counts as one only when its
854
+ * config carries the entry point's marker AND the document is served from the SDK's
855
+ * own origin; a host page can obtain the marked config from the public
856
+ * `resolveChatHtmlConfig` but is never served from that origin.
857
+ */
858
+ private _initialUrlMetaField;
859
+ /**
860
+ * Where this build loads its own frames from. Preserves any path prefix (e.g.
861
+ * "/messaging" on the static.ada.support root) so frame URLs don't 404 there;
862
+ * `.origin` alone would drop it.
863
+ */
864
+ private _sdkAssetsBase;
804
865
  private _currentRuntimeMeta;
805
866
  /** Invalidate a queued microtask once the same latest snapshot is staged/sent. */
806
867
  private _cancelScheduledRuntimeMetaSync;
@@ -107,6 +107,7 @@ export declare function resolveAdaBaseDomainFromSdkUrl(sdkUrl: string): string |
107
107
  * Used when no explicit `endpoint` override is provided in config.
108
108
  */
109
109
  export declare function deriveDefaultApiBase(handle: string, sdkUrl: string, cluster?: string | null): string;
110
+ export declare function isReservedMetaFieldKey(key: string): boolean;
110
111
  /** Strip non-primitive values and empty keys from a metafield record.
111
112
  * Also enforces key/value length limits and a maximum entry count to
112
113
  * prevent oversized payloads from reaching the API (which would 413).
@@ -119,20 +120,35 @@ export declare function sanitizeMetaFieldRecord(fields: Record<string, unknown>,
119
120
  * Lets a host (e.g. the dashboard "Ask for Help" popup) pass context to the bot
120
121
  * without baking business keys into the entry point. */
121
122
  export declare const CHAT_HTML_META_FIELD_PREFIX = "mf_";
123
+ export declare function markStandaloneChatPageConfig<T extends object>(config: T): T;
124
+ export declare function isStandaloneChatPageConfig(value: unknown): boolean;
125
+ /**
126
+ * Whether the current document is one the SDK serves itself. `chat.html` and
127
+ * `webview.html` import the SDK bundle relatively, so their origin is the origin the SDK
128
+ * loads its own frames from; a host page runs on its own origin and cannot present this
129
+ * evidence whatever its constructor config carries. An opaque (`"null"`) origin never
130
+ * matches.
131
+ */
132
+ export declare function isSdkOwnedDocumentOrigin(documentOrigin: string, sdkAssetsBase: string): boolean;
122
133
  export interface ChatHtmlConfig {
123
134
  handle: string;
124
135
  cluster?: string;
125
136
  language?: string;
126
137
  greeting?: string;
138
+ privateMode?: boolean;
127
139
  metaFields: Record<string, MetaFieldValue>;
128
140
  }
129
141
  /**
130
142
  * Build the standalone `chat.html` SDK config (sans `parentElement`) from its
131
- * query string. Reads `ada_handle` (required) / `ada_cluster` / `language` /
132
- * `greeting`, and forwards every `mf_<key>` param as a sanitized `metaFields`
133
- * entry. Returns `null` when `ada_handle` is absent.
143
+ * URL. Reads `ada_handle` (required) / `ada_cluster` / `language` / `greeting`,
144
+ * forwards every `mf_<key>` param as a sanitized `metaFields` entry, and turns the
145
+ * legacy direct-`/chat` private-mode tokens (`?private=1|true`, `#private`) on this
146
+ * page's own URL into `privateMode`. The result is marked as the standalone surface;
147
+ * the client records no `initialURL` for it when the document is also served from the
148
+ * SDK's own origin, matching legacy direct `/chat`. Returns `null` when `ada_handle`
149
+ * is absent.
134
150
  */
135
- export declare function resolveChatHtmlConfig(search: string | URLSearchParams): ChatHtmlConfig | null;
151
+ export declare function resolveChatHtmlConfig(location: string | URLSearchParams | Pick<Location, "search" | "hash">): ChatHtmlConfig | null;
136
152
  /**
137
153
  * Return only the host overrides that are both supported and safe to paint.
138
154
  *
@@ -158,14 +174,16 @@ export declare function resolveIntegrationTarget(command: IntegrationCommand, ac
158
174
  };
159
175
  export { CSAT_INTEGRATION_ID, CSAT_SUBMIT_ACTION, normalizeCsatSubmitAction };
160
176
  /**
161
- * Check whether the embedding page URL contains a private-mode override.
177
+ * Check whether a URL carries legacy Chat's private-mode override.
162
178
  *
163
179
  * Detects:
164
180
  * - ?private=true
165
181
  * - ?private=1
166
182
  * - #private (hash must be exactly "#private")
167
183
  *
168
- * When any of these is present, storage is forced to "private" mode
169
- * regardless of the client config's persistence setting.
184
+ * Legacy reads these only inside Chat's own document, so a host page's URL never
185
+ * forces private mode (embed-2 forwards `adaSettings.privateMode` alone). Messaging
186
+ * honours them only on the standalone `chat.html` URL, the Ada-owned surface that
187
+ * stands in for legacy direct `/chat`.
170
188
  */
171
- export declare function detectPrivateUrlOverride(location?: Pick<Location, "search" | "hash">): boolean;
189
+ export declare function detectPrivateUrlOverride(location: Pick<Location, "search" | "hash">): boolean;
@@ -35,8 +35,8 @@ export interface AdaEmbedInterface {
35
35
  /**
36
36
  * Reset the session. By default this clears history and creates a new
37
37
  * chatter; pass `resetChatHistory: false` to update config while keeping
38
- * history. Caller-supplied `metaFields` apply on a full reset only when
39
- * the host opted in via `allowMetaFieldsInReset`.
38
+ * history. Caller-supplied `metaFields` apply on a full reset by default;
39
+ * set `allowMetaFieldsInReset: false` to block them.
40
40
  */
41
41
  reset: (params?: ResetParams) => Promise<void>;
42
42
  /** Merge non-sensitive metafield key/value pairs onto the chatter. */
@@ -101,6 +101,24 @@ export interface AdaEmbedInterface {
101
101
  setTheme: (theme: "light" | "dark" | "auto") => Promise<void>;
102
102
  /** Register a push-notification device token for this chatter. */
103
103
  setDeviceToken: (token: string) => Promise<void>;
104
+ /**
105
+ * Internal (EXP-1082): route a native session-mirror clear acknowledgement
106
+ * to core's pending durable-clear barrier. Called by the mobile webview
107
+ * runtime only; a no-op signal for any other caller. Optional on the public
108
+ * interface — it carries no host-page meaning, so a structural implementer
109
+ * (test double, wrapper, typed adapter) need not provide it. The concrete
110
+ * client still implements it for the mobile webview bridge.
111
+ */
112
+ acknowledgeSessionMirrorClear?: (requestId: string) => Promise<void>;
113
+ /**
114
+ * Internal (EXP-1107): route a native boot-seed reply to core's pending
115
+ * `requestSeed()` barrier. Called by the mobile webview runtime only; a no-op
116
+ * signal for any other caller. Optional on the public interface — it carries
117
+ * no host-page meaning, so a structural implementer (test double, wrapper,
118
+ * typed adapter) need not provide it. The concrete client still implements it
119
+ * for the mobile webview bridge.
120
+ */
121
+ deliverSessionMirrorSeed?: (requestId: string, seed: unknown) => Promise<void>;
104
122
  /**
105
123
  * Send a message into the conversation as the user. Requires
106
124
  * `enableProgrammaticControl`; resolves the accepted message id.
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Telemetry for a host-supplied callback that threw.
3
+ *
4
+ * The SDK catches every throw from host code (an `adaEmbed` subscriber, a
5
+ * window callback, `eventCallbacks`, `zdChatterAuthCallback`) so one broken
6
+ * integration cannot take the widget down. The catch also hid the break: the
7
+ * only trace was a host-page `console.error` nobody at Ada reads; the
8
+ * EXP-1180 duplicate launcher ran undetected for the whole rollout.
9
+ *
10
+ * Each caught throw is now also forwarded to core, which records a RUM action
11
+ * and a Sentry event. The SDK has no telemetry client of its own on the host
12
+ * page, so the distinct `sdk.hostCallback.error` message uses the existing
13
+ * SDK -> core transport. Only bounded diagnostic identities travel with a fixed
14
+ * message placeholder; a callback can quote private data from its closure even
15
+ * when its arguments contain no sensitive data. The SDK never reads the message
16
+ * or stack, and core independently uses the same placeholder.
17
+ */
18
+ export declare const HOST_CALLBACK_ERROR_MESSAGE_TYPE = "sdk.hostCallback.error";
19
+ export declare const REDACTED_SENSITIVE_MESSAGE: string;
20
+ /** Reports per (callback, event) pair per page session. */
21
+ export declare const HOST_CALLBACK_ERROR_LIMIT_PER_KEY: number;
22
+ /**
23
+ * Repeat reports (a pair's second and third) per page session, shared across
24
+ * pairs. A pair's first report is exempt so one noisy hook, such as a broken
25
+ * `subscribeAll` seeing every event, cannot bury a different hook's first
26
+ * break later in the session.
27
+ */
28
+ export declare const HOST_CALLBACK_ERROR_LIMIT_REPEATS_PER_SESSION: number;
29
+ /** Distinct pairs that may report per page session. */
30
+ export declare const HOST_CALLBACK_ERROR_LIMIT_DISTINCT_KEYS: number;
31
+ export interface HostCallbackErrorPayload {
32
+ /** Which host hook threw: `subscribeEvent`, `subscribeAll`, `adaReadyCallback`, ... */
33
+ callbackName: string;
34
+ /** The SDK event key, custom registration diagnostic identifier or trigger. */
35
+ eventName: string;
36
+ errorName: string;
37
+ errorMessage: string;
38
+ }
39
+ export type HostCallbackErrorSink = (type: string, payload: unknown) => void;
40
+ export declare function isHostCallbackRejectionEvent(event: PromiseRejectionEvent): boolean;
41
+ /** Route reports to the active client's core channel. */
42
+ export declare function setHostCallbackErrorSink(next: HostCallbackErrorSink): void;
43
+ /**
44
+ * Detach `owner` only if it is still the active sink. A stale client's late
45
+ * `destroy()` must not silence the client that replaced it.
46
+ */
47
+ export declare function clearHostCallbackErrorSink(owner: HostCallbackErrorSink): void;
48
+ /**
49
+ * Record a caught host-callback throw. Never throws itself and never
50
+ * rethrows: the caller has already decided to swallow the error.
51
+ */
52
+ export declare function reportHostCallbackError(callbackName: string, eventName: string, error: unknown): void;
53
+ /**
54
+ * A host callback that returns a promise rejects outside every catch, and the
55
+ * global `unhandledrejection` reporter forwards it only when the stack names
56
+ * an Ada frame. Observe the rejection the way the synchronous catch does:
57
+ * log, report, never rethrow.
58
+ *
59
+ * Attaching a handler marks the host's promise handled, which would silence
60
+ * the integrator's own `unhandledrejection` listeners (their error tracker).
61
+ * So after reporting, the same promise and reason are re-dispatched to the
62
+ * host window as an `unhandledrejection` event. The general SDK reporter skips
63
+ * these marked events: synchronous callback stacks can contain SDK frames,
64
+ * but their raw message and stack must not bypass this reporter's redaction
65
+ * or rate limits. Native promises from another realm follow the same path.
66
+ * Non-native thenables are reported without synthetic events. Observing a
67
+ * library promise may silence that library's rejection tracker; the SDK does
68
+ * not restore library-specific signals. The browser's own
69
+ * "Uncaught (in promise)" console line is the one thing that does not fire;
70
+ * Ada's `[Ada] ... rejected:` console error stands in for it.
71
+ */
72
+ export declare function watchHostCallbackResult(result: unknown, callbackName: string, eventName: string, logLabel: string, onRejected?: (error: unknown) => void): void;
73
+ /** Test seam: the limits and the sink are per page session, which a test file shares. */
74
+ export declare function resetHostCallbackErrorReportsForTests(): void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Internal start-config channel for the native session mirror (EXP-1082).
3
+ *
4
+ * The `ada.sdk.internal.` config namespace is reserved: the client strips any
5
+ * host-supplied key with that prefix from constructor config,
6
+ * `window.adaSettings`, and start()/reset() payloads before merging, so no
7
+ * browser embed can enable the mobile identified-session carve-out. Only
8
+ * `mobile-webview-runtime.ts` can set it — it arms this module immediately
9
+ * before calling `adaEmbed.start()`, and the client consumes the grant during
10
+ * initialization. This module is not exported from the package index.
11
+ */
12
+ export declare const INTERNAL_CONFIG_KEY_PREFIX = "ada.sdk.internal.";
13
+ export declare const INTERNAL_NATIVE_SESSION_MIRROR_CONFIG_KEY = "ada.sdk.internal.nativeSessionMirror";
14
+ export interface InternalSessionMirrorStartConfig {
15
+ nativeSessionMirror: true;
16
+ }
17
+ /** Called only by the mobile webview runtime, immediately before start(). */
18
+ export declare function armInternalSessionMirrorStartConfig(config: InternalSessionMirrorStartConfig): void;
19
+ /**
20
+ * Drop an armed grant that was never consumed. The webview runtime calls this
21
+ * after start() settles: a failed start (e.g. a duplicate-client claim throwing
22
+ * before initialization) must not leave a stale credential-bearing grant for a
23
+ * later, unrelated client construction to consume. Idempotent — a consumed
24
+ * grant is already gone.
25
+ */
26
+ export declare function disarmInternalSessionMirrorStartConfig(): void;
27
+ /**
28
+ * One-shot read by the client during initialization. Returns null for every
29
+ * ordinary browser embed. The grant sits on a page-reachable window global, so
30
+ * its mere presence cannot be trusted: a browser page could set it to forge the
31
+ * mobile carve-out. Honour it
32
+ * only in the privileged webview document that arming itself requires — the same
33
+ * `isPrivilegedWebViewLocation` structural signal `isPrivilegedWebViewDocument`
34
+ * gates arming on — so a forged grant on a browser embed reads null. The read
35
+ * always clears the global, so no later script or client construction can
36
+ * observe it even on the unhonoured path.
37
+ */
38
+ export declare function consumeInternalSessionMirrorStartConfig(): InternalSessionMirrorStartConfig | null;
39
+ /**
40
+ * Remove every reserved `ada.sdk.internal.`-prefixed key from a host-supplied
41
+ * config record. Applied to constructor config (which start() and
42
+ * window.adaSettings merge into) and reset payload ingestion.
43
+ */
44
+ export declare function stripInternalConfigKeys<T extends object>(record: T): T;
@@ -14,9 +14,9 @@
14
14
  * Note: inline scripts on the host page also produce an empty filename;
15
15
  * those will be forwarded but will not contain Ada stack frames and will
16
16
  * appear as attributionless errors in Sentry.
17
- * - `unhandledrejection` events: check the rejection's stack trace for our
18
- * assets origin; skip rejections with no stack or a stack that doesn't
19
- * reference our origin (can't attribute them to our code).
17
+ * - `unhandledrejection` events: first skip marked host-callback redispatches
18
+ * so their raw reasons cannot bypass callback redaction. Then check the
19
+ * rejection's stack for our assets origin; skip missing or unrelated stacks.
20
20
  * - `ada:error` CustomEvents: always ours — forwarded with full detail.
21
21
  */
22
22
  export declare function setupSdkErrorReporter(sendToCore: (type: string, payload: unknown) => void): () => void;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Removes `//` and (nesting) block comments while preserving string, char and
3
+ * template literals, so a pin can never be satisfied by prose.
4
+ */
5
+ export declare function stripComments(source: string): string;
6
+ export declare function compact(source: string): string;
7
+ export declare function code(source: string): string;
8
+ /** One `?raw`-pinned source, with a code anchor and a prose-only anchor. */
9
+ export interface PinnedRawSource {
10
+ name: string;
11
+ raw: string;
12
+ stripped: string;
13
+ codeAnchor: string;
14
+ proseAnchor: string;
15
+ }
16
+ /**
17
+ * The stripper self-test every `?raw` pinning suite runs over its own sources.
18
+ * Call it first inside the suite so a broken stripper reports itself rather
19
+ * than turning the pins below it into no-ops.
20
+ */
21
+ export declare function describeCommentStripping(sources: readonly PinnedRawSource[]): void;
@@ -99,6 +99,12 @@ export interface StartupConfigProactiveConversation {
99
99
  url: string;
100
100
  openInNewTab: boolean;
101
101
  }>;
102
+ /**
103
+ * Playbook api starts in place of this proactive's static messages; api emits it
104
+ * only under its conversation-start EAP flag. Core keeps the normal greeting
105
+ * held for such a proactive, mirroring legacy embed-2's `skipGreeting`.
106
+ */
107
+ playbook_id?: string | null;
102
108
  }
103
109
  /**
104
110
  * Serializable snapshot of client config broadcast from core to SDK
@@ -165,6 +171,9 @@ export interface SdkToFrameEvents {
165
171
  /** When false, a failing custom appUrl does not fall back to the default app. */
166
172
  appUrlFallback?: boolean;
167
173
  metaFields?: Record<string, MetaFieldValue>;
174
+ /** Host-declared metaFields baseline (SDK-owned keys already stripped) used to detect a
175
+ * dropped-on-opt-out reset; core reads it into the session. (SUP-42) */
176
+ metaFieldsBaseline?: Record<string, MetaFieldValue>;
168
177
  hostTelemetry?: HostTelemetryContext;
169
178
  styles?: Record<string, string>;
170
179
  /** Build/runtime mode; customer testMode never changes infrastructure. */
@@ -177,6 +186,8 @@ export interface SdkToFrameEvents {
177
186
  * page can measure this; a desktop widget frame is capped at 375px.
178
187
  */
179
188
  isMobileViewport?: boolean;
189
+ /** Internal (EXP-1082): the mobile webview runtime mirrors session state natively. */
190
+ nativeSessionMirror?: boolean;
180
191
  /** One-shot bootstrap flag to clear persisted chatter state before prepare(). */
181
192
  clearPersistenceOnPrepare?: boolean;
182
193
  /** Correlate a reset/deleteHistory clear across a replacement core frame. */
@@ -358,9 +369,41 @@ export interface SdkToFrameEvents {
358
369
  "sdk.history.delete": {
359
370
  requestId?: string;
360
371
  } | undefined;
361
- /** Register a device push notification token (SDK → core) */
372
+ /**
373
+ * Native acknowledged a durable session-mirror delete; releases core's
374
+ * pending clear barrier (EXP-1082).
375
+ */
376
+ "sdk.sessionMirror.clearAck": {
377
+ requestId: string;
378
+ };
379
+ /**
380
+ * Native answered the boot-time seed pull (EXP-1107); resolves core's pending
381
+ * `requestSeed()`. `seed` is the mirror blob native read live, or null/omitted
382
+ * when absent (a clear ordered before the read yields no seed).
383
+ */
384
+ "sdk.sessionMirror.seed": {
385
+ requestId: string;
386
+ seed?: unknown;
387
+ };
388
+ /**
389
+ * Register a device push notification token (SDK → core). `deviceOs` is the OS the SDK
390
+ * recorded at registration and forwards on replay; core honors a recognized enum value or
391
+ * `null` (deliberately none) and re-derives from metaFields for anything else. (SUP-42)
392
+ */
362
393
  "sdk.notifications.deviceToken.set": {
363
394
  token: string;
395
+ deviceOs?: "WEB" | "ANDROID" | "IOS" | null;
396
+ };
397
+ /**
398
+ * A host-supplied callback threw and the SDK swallowed it (SDK → core).
399
+ * Core records bounded diagnostic identities and a fixed message placeholder.
400
+ * A stack or raw message could carry private host-page data.
401
+ */
402
+ "sdk.hostCallback.error": {
403
+ callbackName: string;
404
+ eventName: string;
405
+ errorName: string;
406
+ errorMessage: string;
364
407
  };
365
408
  /** Track an analytics event (SDK → core) */
366
409
  "sdk.analytics.trackEvent": {
package/dist/logger.d.ts CHANGED
@@ -13,9 +13,11 @@
13
13
  * - **`error` is never gated.** The remaining `logger.error` sites are all
14
14
  * caught exceptions from host-supplied code — an `adaEmbed` subscriber, a
15
15
  * window callback, custom JS — plus frame origin mismatches. Because the throw
16
- * is caught, nothing else reports it: `setupSdkErrorReporter` only sees
17
- * `window.onerror`, `unhandledrejection`, and `ada:error`. Suppressing those
18
- * removed observability rather than noise.
16
+ * is caught, no global handler reports it: `setupSdkErrorReporter` only sees
17
+ * `window.onerror`, `unhandledrejection`, and `ada:error`. The console line is
18
+ * the integrator's signal; Ada's is the paired `reportHostCallbackError()`
19
+ * (`client/host-callback-error.ts`). Suppressing either removes
20
+ * observability rather than noise.
19
21
  *
20
22
  * - **`dataLoss` is never gated.** This is the warning channel for a host value
21
23
  * the SDK accepted but then had to drop or truncate. A production integration
@@ -52,6 +52,24 @@ export interface AdaInjectedWebViewConfig {
52
52
  identityToken?: string;
53
53
  /** Custom app frame URL, merged into `adaEmbed.start()` as `appUrl`. The app origin must be in the bot's Allowed Websites list. */
54
54
  appUrl?: string;
55
+ /**
56
+ * Native session-mirror capability handshake (EXP-1082). Set `true` only by a
57
+ * native version that implements the `sdk.session.mirror` /
58
+ * `sdk.session.mirrorClear` handlers AND emits `ada.sessionMirrorClearAck`.
59
+ * The web runtime is CDN-only and unversioned, so it loads inside every
60
+ * already-shipped native wrapper — including old versions with no mirror
61
+ * handlers. Without this native-vouched signal the mirror and its durable
62
+ * clear ack-barrier stay disarmed, so a reset/deleteHistory on an old native
63
+ * never waits on an ack that host can never send. Readers treat absent or
64
+ * `!== true` as unsupported.
65
+ */
66
+ nativeSessionMirrorSupported?: boolean;
67
+ /**
68
+ * Reserved companion to {@link nativeSessionMirrorSupported} for future
69
+ * protocol evolution. Not read today; present so a later version can advertise
70
+ * a higher mirror protocol without a second capability field.
71
+ */
72
+ sessionMirrorProtocolVersion?: number;
55
73
  }
56
74
  /**
57
75
  * sessionStorage key recording that an injected `identityToken` was consumed
@@ -67,6 +85,8 @@ export interface AdaInjectedWebViewConfig {
67
85
  */
68
86
  export declare const IDENTITY_TOKEN_CONSUMED_STORAGE_KEY = "__ada_identity_token_consumed__";
69
87
  type AdaWebViewRuntimeAssetConfig = Pick<AdaWebViewRuntimeConfig, "cluster" | "greeting" | "handle" | "hostTelemetry" | "language" | "metaFields" | "webSdk">;
88
+ /** The `sdk/webview.html` document a native wrapper loaded for its environment. */
89
+ export type WebViewDocumentLocation = Pick<Location, "hostname" | "origin" | "pathname" | "protocol">;
70
90
  export interface ResolvedAdaWebViewRuntimeAssets {
71
91
  webSdk: AdaWebSdkMode;
72
92
  scriptUrl?: string;
@@ -83,6 +103,8 @@ interface LegacyAdaEmbed {
83
103
  setSensitiveMetaFields?: (fields: Record<string, unknown>) => unknown;
84
104
  setDeviceToken?: (token: string) => unknown;
85
105
  deleteHistory?: () => unknown;
106
+ acknowledgeSessionMirrorClear?: (requestId: string) => unknown;
107
+ deliverSessionMirrorSeed?: (requestId: string, seed: unknown) => unknown;
86
108
  subscribeAll?: (callback: (key: string, payload: unknown) => void) => (() => void) | undefined;
87
109
  subscribeEvent?: (key: string, callback: (payload: unknown) => void) => number | Promise<number>;
88
110
  }
@@ -97,11 +119,17 @@ declare global {
97
119
  }
98
120
  /**
99
121
  * Resolve which web runtime assets a native wrapper should load for the given
100
- * webview config: the Messaging SDK bundle URL for the requested cluster, the
101
- * static embed2 script for non-local legacy, or the local Chat iframe assets
102
- * for localhost legacy development.
122
+ * webview config: the Messaging SDK bundle URL, the static embed2 script for
123
+ * non-local legacy, or the local Chat iframe assets for localhost legacy
124
+ * development.
125
+ *
126
+ * The Messaging bundle is served from the origin of the `sdk/webview.html`
127
+ * document the wrapper loaded, which the wrapper chose from the host-supplied
128
+ * environment — pass `documentLocation` when resolving from outside that
129
+ * document; it defaults to the executing one. The cluster decides only when
130
+ * there is no such document to read.
103
131
  */
104
- export declare function resolveAdaWebViewRuntimeAssets(config: AdaWebViewRuntimeAssetConfig): ResolvedAdaWebViewRuntimeAssets;
132
+ export declare function resolveAdaWebViewRuntimeAssets(config: AdaWebViewRuntimeAssetConfig, documentLocation?: WebViewDocumentLocation | undefined): ResolvedAdaWebViewRuntimeAssets;
105
133
  /**
106
134
  * Mount the Ada web runtime inside a native mobile webview (iOS, Android,
107
135
  * React Native). Loads the Messaging SDK (or the legacy embed2/Chat runtime),
@@ -13,7 +13,7 @@ var UNSTAMPED_SHA_PATTERN = /^0{40}$/;
13
13
  * unstamped 40-zero placeholder ({@link isCdnBuildShaStamped} returns
14
14
  * `false`); only published tarballs carry a real SHA.
15
15
  */
16
- var CDN_BUILD_SHA = "811ee8fe6daeb693bc1b95e3a41ccb82a9d763eb";
16
+ var CDN_BUILD_SHA = "9fb7ddd694e3b909a69beb04310922e2e97ffa49";
17
17
  /**
18
18
  * Whether `sha` (default {@link CDN_BUILD_SHA}) is a stamped full git commit
19
19
  * SHA rather than the unstamped repo placeholder.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/npm/build-info.ts","../../src/client/embed-lifecycle.ts","../../src/npm/loader.ts"],"sourcesContent":["// The repo copy carries the 40-zero placeholder; the publish workflow\n// (.github/workflows/messaging-publish-web-packages.yml) stamps the real\n// commit SHA into the built output via scripts/stamp-cdn-build-sha.mjs and\n// verifies the packed tarball before `npm publish`. Keep the placeholder as\n// a single 40-zero string literal: the stamp is a same-length text\n// replacement, so source maps stay valid.\nconst GIT_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_SHA_PATTERN = /^0{40}$/;\n\n/**\n * Git commit SHA of the monorepo commit this npm version was published from.\n * The CDN pipeline deploys each main commit's runtime under an immutable\n * SHA root, so this value names the CDN build associated with this npm\n * version. Pass it as the `pinBuildSha` loader option to load exactly that\n * runtime build — not recommended for production (see the README).\n *\n * In the repository, and in any locally built copy, the value is the\n * unstamped 40-zero placeholder ({@link isCdnBuildShaStamped} returns\n * `false`); only published tarballs carry a real SHA.\n */\nexport const CDN_BUILD_SHA: string = \"811ee8fe6daeb693bc1b95e3a41ccb82a9d763eb\";\n\n/**\n * Whether `sha` (default {@link CDN_BUILD_SHA}) is a stamped full git commit\n * SHA rather than the unstamped repo placeholder.\n */\nexport function isCdnBuildShaStamped(sha: string = CDN_BUILD_SHA): boolean {\n\tconst normalized = sha.trim().toLowerCase();\n\treturn (\n\t\tGIT_SHA_PATTERN.test(normalized) && !UNSTAMPED_SHA_PATTERN.test(normalized)\n\t);\n}\n","/**\n * Lifecycle channel between `createAdaEmbedInterface` and the npm loader's\n * memo: a non-enumerable getter over closure state, so the loader never\n * mutates the public surface and `Object.freeze` on the facade still works.\n * `Symbol.for` because the loader and the CDN runtime are separate builds;\n * a runtime that predates the getter yields `undefined` — degrade gracefully.\n */\nexport const EMBED_LIFECYCLE_STOPPED = Symbol.for(\n\t\"ada-cx/messaging-sdk/embed-stopped\",\n);\n\n/** Read the facade's stopped flag; `undefined` when the runtime predates it. */\nexport function readEmbedStoppedFlag(ada: object): boolean | undefined {\n\tconst value = (ada as Record<symbol, unknown>)[EMBED_LIFECYCLE_STOPPED];\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\n/** Define the getter at facade creation time, before any host freeze. */\nexport function defineEmbedStoppedFlag(\n\tada: object,\n\treadStopped: () => boolean,\n): void {\n\tObject.defineProperty(ada, EMBED_LIFECYCLE_STOPPED, {\n\t\tget: readStopped,\n\t\tenumerable: false,\n\t\tconfigurable: false,\n\t});\n}\n","import type { AdaMessagingClient } from \"../client/client\";\nimport type { AdaMessagingStartOptions } from \"../client/client-types\";\nimport type {\n\tAdaEmbedInterface,\n\tcreateAdaEmbedInterface,\n\tdispatchEmbedLoaderInitialActionQueue,\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff,\n} from \"../client/embed-interface\";\nimport { readEmbedStoppedFlag } from \"../client/embed-lifecycle\";\nimport type {\n\tmountAdaWebViewRuntime,\n\tresolveAdaWebViewRuntimeAssets,\n} from \"../mobile-webview-runtime\";\n\n/**\n * Production CDN root the loader resolves the SDK bootstrap from when no\n * `cdnBase` override is given. The npm package never pins a runtime version;\n * the bootstrap at this root resolves the current rollout at load time.\n */\nexport const DEFAULT_MESSAGING_CDN_BASE =\n\t\"https://messaging-assets.ada.support/\";\n\nconst SDK_BOOTSTRAP_FILENAME = \"sdk.js\";\n\n// The CDN pipeline deploys each main commit's runtime as an immutable\n// SHA-rooted entry (`<cdnBase>/<sha>/sdk/index.js`); the mutable `sdk.js`\n// bootstrap resolves the current rollout to one of those roots at load time.\n// A pin bypasses the bootstrap and imports one root directly.\nconst PINNED_SDK_ENTRY_PATH = \"sdk/index.js\";\n\n// Strict on purpose: the pin is interpolated into a CDN path, so only a full\n// lowercase 40-hex git SHA may pass (mirrors scripts/sdk.js's\n// VERSION_OVERRIDE_PATTERN security boundary, minus the short-SHA and pr-N\n// forms — the npm surface pins exactly what CDN_BUILD_SHA carries).\nconst PINNED_BUILD_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_BUILD_SHA_PATTERN = /^0{40}$/;\n\n/** Why a {@link MessagingSdkLoadError} was raised. */\nexport type MessagingSdkLoadErrorCode =\n\t/**\n\t * The `cdnBase` option is not a valid `https:` URL (plain `http:` is\n\t * allowed only for loopback hosts).\n\t */\n\t| \"invalid_cdn_base\"\n\t/** The `pinBuildSha` option is not a full 40-character hex git SHA. */\n\t| \"invalid_build_sha\"\n\t/**\n\t * The `pinBuildSha` option is the unstamped 40-zero placeholder: this\n\t * copy of the package was built from the repository, not published, so\n\t * its `CDN_BUILD_SHA` names no CDN build.\n\t */\n\t| \"unstamped_build_sha\"\n\t/** The dynamic import of the CDN bootstrap failed (network, CSP, 404). */\n\t| \"sdk_import_failed\"\n\t/** The imported module does not expose the Ada Messaging SDK surface. */\n\t| \"sdk_module_invalid\";\n\n/** Typed failure raised by {@link loadMessagingSdk} and {@link loadAdaMessaging}. */\nexport class MessagingSdkLoadError extends Error {\n\treadonly code: MessagingSdkLoadErrorCode;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: MessagingSdkLoadErrorCode,\n\t\tcause?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MessagingSdkLoadError\";\n\t\tthis.code = code;\n\t\tif (cause !== undefined) {\n\t\t\tthis.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * The programmatic messaging interface {@link loadAdaMessaging} resolves. An\n * alias of the runtime's `AdaEmbedInterface`, which keeps that name because it\n * is also the legacy `window.adaEmbed` compat facade.\n */\nexport type AdaMessagingInterface = AdaEmbedInterface;\n\n/**\n * The public surface of the CDN-resolved SDK module, as loaded from\n * `<cdnBase>/sdk.js`. Types only — the implementation always comes from the\n * CDN at load time and is never part of this npm package.\n */\nexport interface MessagingSdkModule {\n\tAdaMessagingClient: typeof AdaMessagingClient;\n\tcreateAdaEmbedInterface: typeof createAdaEmbedInterface;\n\tdispatchEmbedLoaderInitialActionQueue: typeof dispatchEmbedLoaderInitialActionQueue;\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff: typeof dispatchEmbedLoaderInitialActionQueueForAtomicHandoff;\n\tmountAdaWebViewRuntime: typeof mountAdaWebViewRuntime;\n\tresolveAdaWebViewRuntimeAssets: typeof resolveAdaWebViewRuntimeAssets;\n}\n\ntype ImportSdkModule = (url: string) => Promise<unknown>;\n\nexport interface LoadMessagingSdkOptions {\n\t/**\n\t * CDN root to resolve `sdk.js` from. Must be an `https:` URL; plain\n\t * `http:` is accepted only for loopback hosts (`localhost`, `127.0.0.1`,\n\t * `[::1]`, `*.localhost`) during local development. Defaults to the\n\t * production root ({@link DEFAULT_MESSAGING_CDN_BASE}). Override it with\n\t * the Ada-provided asset host for a staging validation window only;\n\t * production pages must keep the default.\n\t */\n\tcdnBase?: string;\n\t/**\n\t * Full 40-character git commit SHA of a deployed CDN build. When set, the\n\t * loader skips the rollout bootstrap and imports that build's immutable\n\t * entry (`<cdnBase>/<sha>/sdk/index.js`) directly. Pass the package's\n\t * `CDN_BUILD_SHA` export to pin the build associated with this npm\n\t * version. NOT recommended for production: a pinned runtime misses Ada's\n\t * fixes and rollouts, and may predate server contract changes. Composes\n\t * with `cdnBase` for staged validation.\n\t */\n\tpinBuildSha?: string;\n\t/**\n\t * Import seam for unit tests. Defaults to a native dynamic `import()` of\n\t * the resolved bootstrap URL.\n\t */\n\timportModule?: ImportSdkModule;\n}\n\n// Both annotations are load-bearing: bundlers rewrite bare dynamic imports,\n// and this specifier must reach the browser untouched so the runtime always\n// resolves from Ada's CDN. The npm build for this entry is unminified so the\n// annotations survive into the published output.\nconst defaultImportModule: ImportSdkModule = (url) =>\n\timport(/* webpackIgnore: true */ /* @vite-ignore */ url);\n\n// Keyed by import seam so production memoizes on the module-level default\n// while each injected test seam gets an isolated cache.\nconst inFlightByImporter = new WeakMap<\n\tImportSdkModule,\n\tMap<string, Promise<MessagingSdkModule>>\n>();\n\n// Mirrors the loopback semantics of the shared `isLoopbackHost` /\n// `validateMessagingEndpoint` (shared/utils/src/url.ts) and the bridge\n// loader's copy (packages/bridge/src/npm/loader.ts). Inlined because the\n// published loader must stay dependency-free; keep the host set in sync.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\n\nfunction isLoopbackHostname(hostname: string): boolean {\n\tconst normalized = hostname.trim().toLowerCase();\n\t// `*.localhost` resolves to the loopback interface and is \"potentially\n\t// trustworthy\" per the Secure Contexts spec; no production host ends in\n\t// `.localhost`.\n\treturn LOOPBACK_HOSTS.has(normalized) || normalized.endsWith(\".localhost\");\n}\n\nfunction resolvePinnedBuildSha(pinBuildSha: string): string {\n\tconst sha = pinBuildSha.trim().toLowerCase();\n\tif (!PINNED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The pinBuildSha option must be a full 40-character hex git SHA: \"${pinBuildSha}\".`,\n\t\t\t\"invalid_build_sha\",\n\t\t);\n\t}\n\tif (UNSTAMPED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t\"The pinBuildSha option is the unstamped CDN_BUILD_SHA placeholder. Only the published npm package carries a stamped SHA; a repository or local build cannot pin a CDN build.\",\n\t\t\t\"unstamped_build_sha\",\n\t\t);\n\t}\n\treturn sha;\n}\n\n/**\n * The runtime this URL serves carries the SDK's security logic (endpoint\n * validation, session handling, the origin checks), so a cleartext transport\n * would let an on-path attacker replace it wholesale: `https:` is required,\n * with a loopback-only `http:` exception for local development.\n */\nfunction resolveSdkBootstrapUrl(\n\tcdnBase: string | undefined,\n\tpinBuildSha: string | undefined,\n): string {\n\tconst base = (cdnBase ?? DEFAULT_MESSAGING_CDN_BASE).trim();\n\t// Resolving against a base without a trailing slash would drop its last\n\t// path segment, so normalize before building the bootstrap URL.\n\tconst normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n\tconst entryPath =\n\t\tpinBuildSha === undefined\n\t\t\t? SDK_BOOTSTRAP_FILENAME\n\t\t\t: `${resolvePinnedBuildSha(pinBuildSha)}/${PINNED_SDK_ENTRY_PATH}`;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(entryPath, normalizedBase);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option is not a valid URL: \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t\tcause,\n\t\t);\n\t}\n\tconst isHttps = url.protocol === \"https:\";\n\tconst isLoopbackHttp =\n\t\turl.protocol === \"http:\" && isLoopbackHostname(url.hostname);\n\tif (!isHttps && !isLoopbackHttp) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option must use https (plain http is allowed only for loopback hosts): \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t);\n\t}\n\treturn url.toString();\n}\n\n// A partial CDN module (mid-deploy, or a rolled-back build missing a newer\n// export) must fail here as a typed sdk_module_invalid error, not later as a\n// TypeError on the first missing call — so every export is checked. The\n// `satisfies` clause rejects names that are not interface members; the\n// exhaustiveness type below is what forces this list to grow with it.\nconst REQUIRED_SDK_EXPORTS = [\n\t\"AdaMessagingClient\",\n\t\"createAdaEmbedInterface\",\n\t\"dispatchEmbedLoaderInitialActionQueue\",\n\t\"dispatchEmbedLoaderInitialActionQueueForAtomicHandoff\",\n\t\"mountAdaWebViewRuntime\",\n\t\"resolveAdaWebViewRuntimeAssets\",\n] as const satisfies readonly (keyof MessagingSdkModule)[];\n\n// Compile-time exhaustiveness: adding a MessagingSdkModule member without\n// listing it in REQUIRED_SDK_EXPORTS turns this type into `never`, failing\n// the `true` assignment below.\ntype AllSdkExportsListed =\n\tExclude<\n\t\tkeyof MessagingSdkModule,\n\t\t(typeof REQUIRED_SDK_EXPORTS)[number]\n\t> extends never\n\t\t? true\n\t\t: never;\nconst _allSdkExportsListed: AllSdkExportsListed = true;\nvoid _allSdkExportsListed;\n\nfunction isMessagingSdkModule(value: unknown): value is MessagingSdkModule {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst candidate = value as Record<string, unknown>;\n\treturn REQUIRED_SDK_EXPORTS.every(\n\t\t(key) => typeof candidate[key] === \"function\",\n\t);\n}\n\nasync function importSdkModule(\n\turl: string,\n\timportModule: ImportSdkModule,\n): Promise<MessagingSdkModule> {\n\tlet loaded: unknown;\n\ttry {\n\t\tloaded = await importModule(url);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The Ada Messaging SDK failed to load from ${url}.`,\n\t\t\t\"sdk_import_failed\",\n\t\t\tcause,\n\t\t);\n\t}\n\tif (!isMessagingSdkModule(loaded)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The module at ${url} does not expose the Ada Messaging SDK interface.`,\n\t\t\t\"sdk_module_invalid\",\n\t\t);\n\t}\n\treturn loaded;\n}\n\n/**\n * Load the Ada Messaging SDK module from Ada's CDN.\n *\n * The returned promise is memoized per resolved URL: concurrent and repeated\n * calls share one in-flight import. A failed load clears the memo so a later\n * call retries. Rejections are always {@link MessagingSdkLoadError}.\n *\n * @example\n * const { createAdaEmbedInterface } = await loadMessagingSdk();\n */\nexport function loadMessagingSdk(\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<MessagingSdkModule> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tlet url: string;\n\ttry {\n\t\turl = resolveSdkBootstrapUrl(options.cdnBase, options.pinBuildSha);\n\t} catch (error) {\n\t\treturn Promise.reject(error);\n\t}\n\n\tlet inFlight = inFlightByImporter.get(importModule);\n\tif (!inFlight) {\n\t\tinFlight = new Map();\n\t\tinFlightByImporter.set(importModule, inFlight);\n\t}\n\tconst existing = inFlight.get(url);\n\tif (existing) {\n\t\treturn existing;\n\t}\n\n\tconst cache = inFlight;\n\tconst pending = importSdkModule(url, importModule);\n\tcache.set(url, pending);\n\tpending.catch(() => {\n\t\tif (cache.get(url) === pending) {\n\t\t\tcache.delete(url);\n\t\t}\n\t});\n\treturn pending;\n}\n\n// Keyed by import seam like the module memo above: production memoizes on\n// the module-level default while each injected test seam gets an isolated\n// cache. `ada` is set when the promise resolves, so the stopped-interface\n// diagnostic can consult the resolved facade without wrapping it.\ninterface StartedInterfaceMemo {\n\tpromise: Promise<AdaMessagingInterface>;\n\tsettings: AdaMessagingStartOptions;\n\tada?: AdaMessagingInterface;\n}\n\nconst startedInterfaceByImporter = new WeakMap<\n\tImportSdkModule,\n\tStartedInterfaceMemo\n>();\n\n// Ungated like the runtime logger's dropped-host-value channel: both call\n// sites fire only on host misuse, never on an ordinary page load.\nfunction warnHost(message: string): void {\n\ttry {\n\t\t// biome-ignore lint/suspicious/noConsole: the loader's sanctioned host-page diagnostic sink\n\t\tconsole.warn(`[Ada] ${message}`);\n\t} catch {\n\t\t// A host may replace console methods; diagnostics must not throw.\n\t}\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\n// Structural equality over the settings surface. Two functions compare equal:\n// fresh inline callbacks on a re-run (React Strict Mode effects) are not a\n// settings change worth a warning. Exotic objects compare by reference.\nfunction settingsEqual(a: unknown, b: unknown): boolean {\n\tif (Object.is(a, b)) {\n\t\treturn true;\n\t}\n\tif (typeof a === \"function\" && typeof b === \"function\") {\n\t\treturn true;\n\t}\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\treturn (\n\t\t\ta.length === b.length &&\n\t\t\ta.every((item, index) => settingsEqual(item, b[index]))\n\t\t);\n\t}\n\tif (isPlainObject(a) && isPlainObject(b)) {\n\t\tconst aKeys = Object.keys(a);\n\t\tconst bKeys = new Set(Object.keys(b));\n\t\treturn (\n\t\t\taKeys.length === bKeys.size &&\n\t\t\taKeys.every((key) => bKeys.has(key) && settingsEqual(a[key], b[key]))\n\t\t);\n\t}\n\treturn false;\n}\n\n// Observes the facade's symbol-keyed lifecycle getter (embed-lifecycle.ts)\n// instead of wrapping stop/start, so the customer's interface is never\n// mutated; a pinned runtime that predates the getter yields no flag and the\n// stopped-interface diagnostic is skipped rather than guessed.\nfunction isInterfaceStopped(ada: AdaMessagingInterface): boolean {\n\treturn readEmbedStoppedFlag(ada) === true;\n}\n\nasync function startAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions,\n): Promise<AdaMessagingInterface> {\n\tconst sdkModule = await loadMessagingSdk(options);\n\tconst ada = sdkModule.createAdaEmbedInterface();\n\tawait ada.start(settings);\n\treturn ada;\n}\n\n/**\n * Load the SDK, create the messaging interface, and start the widget —\n * the npm-first equivalent of the script-tag + `window.adaSettings` setup.\n *\n * One widget per page: the returned promise is memoized, so repeated and\n * concurrent calls resolve to the same started interface and the first\n * call's settings win. A failed load or start clears the memo so a later\n * call retries. Manage the running widget through the returned interface:\n * `stop()` tears it down and a later `start(settings)` on the same interface\n * runs again. Calling {@link loadAdaMessaging} after `stop()` returns the\n * same stopped interface without restarting it. Both memo outcomes log a\n * console warning: ignored differing settings, and a resolved interface\n * whose widget was stopped.\n *\n * Never assigns the interface to `window` or any other global: the returned\n * reference is the only handle, so hold it in module scope. A reference in\n * module scope is reachable only by your own code, never by other scripts on\n * the page.\n *\n * @example\n * // module scope: your code keeps the only reference\n * const ada = await loadAdaMessaging({\n * handle: \"my-bot\",\n * enableProgrammaticControl: true,\n * });\n * await ada.sendMessage(\"Hello\");\n */\nexport function loadAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<AdaMessagingInterface> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tconst existing = startedInterfaceByImporter.get(importModule);\n\tif (existing) {\n\t\tif (!settingsEqual(settings, existing.settings)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() was already called: the new settings are ignored and the existing interface is returned. To apply new settings, call stop() then start(settings) on it.\",\n\t\t\t);\n\t\t}\n\t\tif (existing.ada && isInterfaceStopped(existing.ada)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() resolved an interface whose widget was stopped and never restarts it. Call start(settings) on it to run the widget again.\",\n\t\t\t);\n\t\t}\n\t\treturn existing.promise;\n\t}\n\tconst memo: StartedInterfaceMemo = {\n\t\tpromise: startAdaMessaging(settings, options),\n\t\tsettings,\n\t};\n\tstartedInterfaceByImporter.set(importModule, memo);\n\tmemo.promise.then(\n\t\t(ada) => {\n\t\t\tmemo.ada = ada;\n\t\t},\n\t\t() => {\n\t\t\tif (startedInterfaceByImporter.get(importModule) === memo) {\n\t\t\t\tstartedInterfaceByImporter.delete(importModule);\n\t\t\t}\n\t\t},\n\t);\n\treturn memo.promise;\n}\n"],"mappings":";;AAMA,IAAM,kBAAkB;AACxB,IAAM,wBAAwB;;;;;;;;;;;;AAa9B,IAAa,gBAAwB;;;;;AAMrC,SAAgB,qBAAqB,MAAc,eAAwB;CAC1E,MAAM,aAAa,IAAI,MAAM,CAAC,aAAa;AAC3C,QACC,gBAAgB,KAAK,WAAW,IAAI,CAAC,sBAAsB,KAAK,WAAW;;;;;;;;;;;ACtB7E,IAAa,0BAA0B,OAAO,IAC7C,qCACA;;AAGD,SAAgB,qBAAqB,KAAkC;CACtE,MAAM,QAAS,IAAgC;AAC/C,QAAO,OAAO,UAAU,YAAY,QAAQ,KAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACK7C,IAAa,6BACZ;AAED,IAAM,yBAAyB;AAM/B,IAAM,wBAAwB;AAM9B,IAAM,2BAA2B;AACjC,IAAM,8BAA8B;;AAuBpC,IAAa,wBAAb,cAA2C,MAAM;CAGhD,YACC,SACA,MACA,OACC;AACD,QAAM,QAAQ;wBAPf,QAAA,KAAA,EAAS;AAQR,OAAK,OAAO;AACZ,OAAK,OAAO;AACZ,MAAI,UAAU,KAAA,EACb,MAAK,QAAQ;;;AA2DhB,IAAM,uBAAwC,QAC7C;;;CAAoD;;AAIrD,IAAM,qCAAqB,IAAI,SAG5B;AAMH,IAAM,iBAAiB,IAAI,IAAI;CAAC;CAAa;CAAa;CAAS;CAAM,CAAC;AAE1E,SAAS,mBAAmB,UAA2B;CACtD,MAAM,aAAa,SAAS,MAAM,CAAC,aAAa;AAIhD,QAAO,eAAe,IAAI,WAAW,IAAI,WAAW,SAAS,aAAa;;AAG3E,SAAS,sBAAsB,aAA6B;CAC3D,MAAM,MAAM,YAAY,MAAM,CAAC,aAAa;AAC5C,KAAI,CAAC,yBAAyB,KAAK,IAAI,CACtC,OAAM,IAAI,sBACT,oEAAoE,YAAY,KAChF,oBACA;AAEF,KAAI,4BAA4B,KAAK,IAAI,CACxC,OAAM,IAAI,sBACT,gLACA,sBACA;AAEF,QAAO;;;;;;;;AASR,SAAS,uBACR,SACA,aACS;CACT,MAAM,QAAQ,WAAA,yCAAuC,MAAM;CAG3D,MAAM,iBAAiB,KAAK,SAAS,IAAI,GAAG,OAAO,GAAG,KAAK;CAC3D,MAAM,YACL,gBAAgB,KAAA,IACb,yBACA,GAAG,sBAAsB,YAAY,CAAC,GAAG;CAC7C,IAAI;AACJ,KAAI;AACH,QAAM,IAAI,IAAI,WAAW,eAAe;UAChC,OAAO;AACf,QAAM,IAAI,sBACT,2CAA2C,KAAK,KAChD,oBACA,MACA;;CAEF,MAAM,UAAU,IAAI,aAAa;CACjC,MAAM,iBACL,IAAI,aAAa,WAAW,mBAAmB,IAAI,SAAS;AAC7D,KAAI,CAAC,WAAW,CAAC,eAChB,OAAM,IAAI,sBACT,uFAAuF,KAAK,KAC5F,mBACA;AAEF,QAAO,IAAI,UAAU;;AAQtB,IAAM,uBAAuB;CAC5B;CACA;CACA;CACA;CACA;CACA;CACA;AAeD,SAAS,qBAAqB,OAA6C;AAC1E,KAAI,OAAO,UAAU,YAAY,UAAU,KAC1C,QAAO;CAER,MAAM,YAAY;AAClB,QAAO,qBAAqB,OAC1B,QAAQ,OAAO,UAAU,SAAS,WACnC;;AAGF,eAAe,gBACd,KACA,cAC8B;CAC9B,IAAI;AACJ,KAAI;AACH,WAAS,MAAM,aAAa,IAAI;UACxB,OAAO;AACf,QAAM,IAAI,sBACT,6CAA6C,IAAI,IACjD,qBACA,MACA;;AAEF,KAAI,CAAC,qBAAqB,OAAO,CAChC,OAAM,IAAI,sBACT,iBAAiB,IAAI,oDACrB,qBACA;AAEF,QAAO;;;;;;;;;;;;AAaR,SAAgB,iBACf,UAAmC,EAAE,EACP;CAC9B,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,IAAI;AACJ,KAAI;AACH,QAAM,uBAAuB,QAAQ,SAAS,QAAQ,YAAY;UAC1D,OAAO;AACf,SAAO,QAAQ,OAAO,MAAM;;CAG7B,IAAI,WAAW,mBAAmB,IAAI,aAAa;AACnD,KAAI,CAAC,UAAU;AACd,6BAAW,IAAI,KAAK;AACpB,qBAAmB,IAAI,cAAc,SAAS;;CAE/C,MAAM,WAAW,SAAS,IAAI,IAAI;AAClC,KAAI,SACH,QAAO;CAGR,MAAM,QAAQ;CACd,MAAM,UAAU,gBAAgB,KAAK,aAAa;AAClD,OAAM,IAAI,KAAK,QAAQ;AACvB,SAAQ,YAAY;AACnB,MAAI,MAAM,IAAI,IAAI,KAAK,QACtB,OAAM,OAAO,IAAI;GAEjB;AACF,QAAO;;AAaR,IAAM,6CAA6B,IAAI,SAGpC;AAIH,SAAS,SAAS,SAAuB;AACxC,KAAI;AAEH,UAAQ,KAAK,SAAS,UAAU;SACzB;;AAKT,SAAS,cAAc,OAAkD;AACxE,KAAI,OAAO,UAAU,YAAY,UAAU,KAC1C,QAAO;CAER,MAAM,YAAY,OAAO,eAAe,MAAM;AAC9C,QAAO,cAAc,OAAO,aAAa,cAAc;;AAMxD,SAAS,cAAc,GAAY,GAAqB;AACvD,KAAI,OAAO,GAAG,GAAG,EAAE,CAClB,QAAO;AAER,KAAI,OAAO,MAAM,cAAc,OAAO,MAAM,WAC3C,QAAO;AAER,KAAI,MAAM,QAAQ,EAAE,IAAI,MAAM,QAAQ,EAAE,CACvC,QACC,EAAE,WAAW,EAAE,UACf,EAAE,OAAO,MAAM,UAAU,cAAc,MAAM,EAAE,OAAO,CAAC;AAGzD,KAAI,cAAc,EAAE,IAAI,cAAc,EAAE,EAAE;EACzC,MAAM,QAAQ,OAAO,KAAK,EAAE;EAC5B,MAAM,QAAQ,IAAI,IAAI,OAAO,KAAK,EAAE,CAAC;AACrC,SACC,MAAM,WAAW,MAAM,QACvB,MAAM,OAAO,QAAQ,MAAM,IAAI,IAAI,IAAI,cAAc,EAAE,MAAM,EAAE,KAAK,CAAC;;AAGvE,QAAO;;AAOR,SAAS,mBAAmB,KAAqC;AAChE,QAAO,qBAAqB,IAAI,KAAK;;AAGtC,eAAe,kBACd,UACA,SACiC;CAEjC,MAAM,OADY,MAAM,iBAAiB,QAAQ,EAC3B,yBAAyB;AAC/C,OAAM,IAAI,MAAM,SAAS;AACzB,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BR,SAAgB,iBACf,UACA,UAAmC,EAAE,EACJ;CACjC,MAAM,eAAe,QAAQ,gBAAgB;CAC7C,MAAM,WAAW,2BAA2B,IAAI,aAAa;AAC7D,KAAI,UAAU;AACb,MAAI,CAAC,cAAc,UAAU,SAAS,SAAS,CAC9C,UACC,6KACA;AAEF,MAAI,SAAS,OAAO,mBAAmB,SAAS,IAAI,CACnD,UACC,+IACA;AAEF,SAAO,SAAS;;CAEjB,MAAM,OAA6B;EAClC,SAAS,kBAAkB,UAAU,QAAQ;EAC7C;EACA;AACD,4BAA2B,IAAI,cAAc,KAAK;AAClD,MAAK,QAAQ,MACX,QAAQ;AACR,OAAK,MAAM;UAEN;AACL,MAAI,2BAA2B,IAAI,aAAa,KAAK,KACpD,4BAA2B,OAAO,aAAa;GAGjD;AACD,QAAO,KAAK"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/npm/build-info.ts","../../src/client/embed-lifecycle.ts","../../src/npm/loader.ts"],"sourcesContent":["// The repo copy carries the 40-zero placeholder; the publish workflow\n// (.github/workflows/messaging-publish-web-packages.yml) stamps the real\n// commit SHA into the built output via scripts/stamp-cdn-build-sha.mjs and\n// verifies the packed tarball before `npm publish`. Keep the placeholder as\n// a single 40-zero string literal: the stamp is a same-length text\n// replacement, so source maps stay valid.\nconst GIT_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_SHA_PATTERN = /^0{40}$/;\n\n/**\n * Git commit SHA of the monorepo commit this npm version was published from.\n * The CDN pipeline deploys each main commit's runtime under an immutable\n * SHA root, so this value names the CDN build associated with this npm\n * version. Pass it as the `pinBuildSha` loader option to load exactly that\n * runtime build — not recommended for production (see the README).\n *\n * In the repository, and in any locally built copy, the value is the\n * unstamped 40-zero placeholder ({@link isCdnBuildShaStamped} returns\n * `false`); only published tarballs carry a real SHA.\n */\nexport const CDN_BUILD_SHA: string = \"9fb7ddd694e3b909a69beb04310922e2e97ffa49\";\n\n/**\n * Whether `sha` (default {@link CDN_BUILD_SHA}) is a stamped full git commit\n * SHA rather than the unstamped repo placeholder.\n */\nexport function isCdnBuildShaStamped(sha: string = CDN_BUILD_SHA): boolean {\n\tconst normalized = sha.trim().toLowerCase();\n\treturn (\n\t\tGIT_SHA_PATTERN.test(normalized) && !UNSTAMPED_SHA_PATTERN.test(normalized)\n\t);\n}\n","/**\n * Lifecycle channel between `createAdaEmbedInterface` and the npm loader's\n * memo: a non-enumerable getter over closure state, so the loader never\n * mutates the public surface and `Object.freeze` on the facade still works.\n * `Symbol.for` because the loader and the CDN runtime are separate builds;\n * a runtime that predates the getter yields `undefined` — degrade gracefully.\n */\nexport const EMBED_LIFECYCLE_STOPPED = Symbol.for(\n\t\"ada-cx/messaging-sdk/embed-stopped\",\n);\n\n/** Read the facade's stopped flag; `undefined` when the runtime predates it. */\nexport function readEmbedStoppedFlag(ada: object): boolean | undefined {\n\tconst value = (ada as Record<symbol, unknown>)[EMBED_LIFECYCLE_STOPPED];\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\n/** Define the getter at facade creation time, before any host freeze. */\nexport function defineEmbedStoppedFlag(\n\tada: object,\n\treadStopped: () => boolean,\n): void {\n\tObject.defineProperty(ada, EMBED_LIFECYCLE_STOPPED, {\n\t\tget: readStopped,\n\t\tenumerable: false,\n\t\tconfigurable: false,\n\t});\n}\n","import type { AdaMessagingClient } from \"../client/client\";\nimport type { AdaMessagingStartOptions } from \"../client/client-types\";\nimport type {\n\tAdaEmbedInterface,\n\tcreateAdaEmbedInterface,\n\tdispatchEmbedLoaderInitialActionQueue,\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff,\n} from \"../client/embed-interface\";\nimport { readEmbedStoppedFlag } from \"../client/embed-lifecycle\";\nimport type {\n\tmountAdaWebViewRuntime,\n\tresolveAdaWebViewRuntimeAssets,\n} from \"../mobile-webview-runtime\";\n\n/**\n * Production CDN root the loader resolves the SDK bootstrap from when no\n * `cdnBase` override is given. The npm package never pins a runtime version;\n * the bootstrap at this root resolves the current rollout at load time.\n */\nexport const DEFAULT_MESSAGING_CDN_BASE =\n\t\"https://messaging-assets.ada.support/\";\n\nconst SDK_BOOTSTRAP_FILENAME = \"sdk.js\";\n\n// The CDN pipeline deploys each main commit's runtime as an immutable\n// SHA-rooted entry (`<cdnBase>/<sha>/sdk/index.js`); the mutable `sdk.js`\n// bootstrap resolves the current rollout to one of those roots at load time.\n// A pin bypasses the bootstrap and imports one root directly.\nconst PINNED_SDK_ENTRY_PATH = \"sdk/index.js\";\n\n// Strict on purpose: the pin is interpolated into a CDN path, so only a full\n// lowercase 40-hex git SHA may pass (mirrors scripts/sdk.js's\n// VERSION_OVERRIDE_PATTERN security boundary, minus the short-SHA and pr-N\n// forms — the npm surface pins exactly what CDN_BUILD_SHA carries).\nconst PINNED_BUILD_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_BUILD_SHA_PATTERN = /^0{40}$/;\n\n/** Why a {@link MessagingSdkLoadError} was raised. */\nexport type MessagingSdkLoadErrorCode =\n\t/**\n\t * The `cdnBase` option is not a valid `https:` URL (plain `http:` is\n\t * allowed only for loopback hosts).\n\t */\n\t| \"invalid_cdn_base\"\n\t/** The `pinBuildSha` option is not a full 40-character hex git SHA. */\n\t| \"invalid_build_sha\"\n\t/**\n\t * The `pinBuildSha` option is the unstamped 40-zero placeholder: this\n\t * copy of the package was built from the repository, not published, so\n\t * its `CDN_BUILD_SHA` names no CDN build.\n\t */\n\t| \"unstamped_build_sha\"\n\t/** The dynamic import of the CDN bootstrap failed (network, CSP, 404). */\n\t| \"sdk_import_failed\"\n\t/** The imported module does not expose the Ada Messaging SDK surface. */\n\t| \"sdk_module_invalid\";\n\n/** Typed failure raised by {@link loadMessagingSdk} and {@link loadAdaMessaging}. */\nexport class MessagingSdkLoadError extends Error {\n\treadonly code: MessagingSdkLoadErrorCode;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: MessagingSdkLoadErrorCode,\n\t\tcause?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MessagingSdkLoadError\";\n\t\tthis.code = code;\n\t\tif (cause !== undefined) {\n\t\t\tthis.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * The programmatic messaging interface {@link loadAdaMessaging} resolves. An\n * alias of the runtime's `AdaEmbedInterface`, which keeps that name because it\n * is also the legacy `window.adaEmbed` compat facade.\n */\nexport type AdaMessagingInterface = AdaEmbedInterface;\n\n/**\n * The public surface of the CDN-resolved SDK module, as loaded from\n * `<cdnBase>/sdk.js`. Types only — the implementation always comes from the\n * CDN at load time and is never part of this npm package.\n */\nexport interface MessagingSdkModule {\n\tAdaMessagingClient: typeof AdaMessagingClient;\n\tcreateAdaEmbedInterface: typeof createAdaEmbedInterface;\n\tdispatchEmbedLoaderInitialActionQueue: typeof dispatchEmbedLoaderInitialActionQueue;\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff: typeof dispatchEmbedLoaderInitialActionQueueForAtomicHandoff;\n\tmountAdaWebViewRuntime: typeof mountAdaWebViewRuntime;\n\tresolveAdaWebViewRuntimeAssets: typeof resolveAdaWebViewRuntimeAssets;\n}\n\ntype ImportSdkModule = (url: string) => Promise<unknown>;\n\nexport interface LoadMessagingSdkOptions {\n\t/**\n\t * CDN root to resolve `sdk.js` from. Must be an `https:` URL; plain\n\t * `http:` is accepted only for loopback hosts (`localhost`, `127.0.0.1`,\n\t * `[::1]`, `*.localhost`) during local development. Defaults to the\n\t * production root ({@link DEFAULT_MESSAGING_CDN_BASE}). Override it with\n\t * the Ada-provided asset host for a staging validation window only;\n\t * production pages must keep the default.\n\t */\n\tcdnBase?: string;\n\t/**\n\t * Full 40-character git commit SHA of a deployed CDN build. When set, the\n\t * loader skips the rollout bootstrap and imports that build's immutable\n\t * entry (`<cdnBase>/<sha>/sdk/index.js`) directly. Pass the package's\n\t * `CDN_BUILD_SHA` export to pin the build associated with this npm\n\t * version. NOT recommended for production: a pinned runtime misses Ada's\n\t * fixes and rollouts, and may predate server contract changes. Composes\n\t * with `cdnBase` for staged validation.\n\t */\n\tpinBuildSha?: string;\n\t/**\n\t * Import seam for unit tests. Defaults to a native dynamic `import()` of\n\t * the resolved bootstrap URL.\n\t */\n\timportModule?: ImportSdkModule;\n}\n\n// Both annotations are load-bearing: bundlers rewrite bare dynamic imports,\n// and this specifier must reach the browser untouched so the runtime always\n// resolves from Ada's CDN. The npm build for this entry is unminified so the\n// annotations survive into the published output.\nconst defaultImportModule: ImportSdkModule = (url) =>\n\timport(/* webpackIgnore: true */ /* @vite-ignore */ url);\n\n// Keyed by import seam so production memoizes on the module-level default\n// while each injected test seam gets an isolated cache.\nconst inFlightByImporter = new WeakMap<\n\tImportSdkModule,\n\tMap<string, Promise<MessagingSdkModule>>\n>();\n\n// Mirrors the loopback semantics of the shared `isLoopbackHost` /\n// `validateMessagingEndpoint` (shared/utils/src/url.ts) and the bridge\n// loader's copy (packages/bridge/src/npm/loader.ts). Inlined because the\n// published loader must stay dependency-free; keep the host set in sync.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\n\nfunction isLoopbackHostname(hostname: string): boolean {\n\tconst normalized = hostname.trim().toLowerCase();\n\t// `*.localhost` resolves to the loopback interface and is \"potentially\n\t// trustworthy\" per the Secure Contexts spec; no production host ends in\n\t// `.localhost`.\n\treturn LOOPBACK_HOSTS.has(normalized) || normalized.endsWith(\".localhost\");\n}\n\nfunction resolvePinnedBuildSha(pinBuildSha: string): string {\n\tconst sha = pinBuildSha.trim().toLowerCase();\n\tif (!PINNED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The pinBuildSha option must be a full 40-character hex git SHA: \"${pinBuildSha}\".`,\n\t\t\t\"invalid_build_sha\",\n\t\t);\n\t}\n\tif (UNSTAMPED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t\"The pinBuildSha option is the unstamped CDN_BUILD_SHA placeholder. Only the published npm package carries a stamped SHA; a repository or local build cannot pin a CDN build.\",\n\t\t\t\"unstamped_build_sha\",\n\t\t);\n\t}\n\treturn sha;\n}\n\n/**\n * The runtime this URL serves carries the SDK's security logic (endpoint\n * validation, session handling, the origin checks), so a cleartext transport\n * would let an on-path attacker replace it wholesale: `https:` is required,\n * with a loopback-only `http:` exception for local development.\n */\nfunction resolveSdkBootstrapUrl(\n\tcdnBase: string | undefined,\n\tpinBuildSha: string | undefined,\n): string {\n\tconst base = (cdnBase ?? DEFAULT_MESSAGING_CDN_BASE).trim();\n\t// Resolving against a base without a trailing slash would drop its last\n\t// path segment, so normalize before building the bootstrap URL.\n\tconst normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n\tconst entryPath =\n\t\tpinBuildSha === undefined\n\t\t\t? SDK_BOOTSTRAP_FILENAME\n\t\t\t: `${resolvePinnedBuildSha(pinBuildSha)}/${PINNED_SDK_ENTRY_PATH}`;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(entryPath, normalizedBase);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option is not a valid URL: \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t\tcause,\n\t\t);\n\t}\n\tconst isHttps = url.protocol === \"https:\";\n\tconst isLoopbackHttp =\n\t\turl.protocol === \"http:\" && isLoopbackHostname(url.hostname);\n\tif (!isHttps && !isLoopbackHttp) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option must use https (plain http is allowed only for loopback hosts): \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t);\n\t}\n\treturn url.toString();\n}\n\n// A partial CDN module (mid-deploy, or a rolled-back build missing a newer\n// export) must fail here as a typed sdk_module_invalid error, not later as a\n// TypeError on the first missing call — so every export is checked. The\n// `satisfies` clause rejects names that are not interface members; the\n// exhaustiveness type below is what forces this list to grow with it.\nconst REQUIRED_SDK_EXPORTS = [\n\t\"AdaMessagingClient\",\n\t\"createAdaEmbedInterface\",\n\t\"dispatchEmbedLoaderInitialActionQueue\",\n\t\"dispatchEmbedLoaderInitialActionQueueForAtomicHandoff\",\n\t\"mountAdaWebViewRuntime\",\n\t\"resolveAdaWebViewRuntimeAssets\",\n] as const satisfies readonly (keyof MessagingSdkModule)[];\n\n// Compile-time exhaustiveness: adding a MessagingSdkModule member without\n// listing it in REQUIRED_SDK_EXPORTS turns this type into `never`, failing\n// the `true` assignment below.\ntype AllSdkExportsListed =\n\tExclude<\n\t\tkeyof MessagingSdkModule,\n\t\t(typeof REQUIRED_SDK_EXPORTS)[number]\n\t> extends never\n\t\t? true\n\t\t: never;\nconst _allSdkExportsListed: AllSdkExportsListed = true;\nvoid _allSdkExportsListed;\n\nfunction isMessagingSdkModule(value: unknown): value is MessagingSdkModule {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst candidate = value as Record<string, unknown>;\n\treturn REQUIRED_SDK_EXPORTS.every(\n\t\t(key) => typeof candidate[key] === \"function\",\n\t);\n}\n\nasync function importSdkModule(\n\turl: string,\n\timportModule: ImportSdkModule,\n): Promise<MessagingSdkModule> {\n\tlet loaded: unknown;\n\ttry {\n\t\tloaded = await importModule(url);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The Ada Messaging SDK failed to load from ${url}.`,\n\t\t\t\"sdk_import_failed\",\n\t\t\tcause,\n\t\t);\n\t}\n\tif (!isMessagingSdkModule(loaded)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The module at ${url} does not expose the Ada Messaging SDK interface.`,\n\t\t\t\"sdk_module_invalid\",\n\t\t);\n\t}\n\treturn loaded;\n}\n\n/**\n * Load the Ada Messaging SDK module from Ada's CDN.\n *\n * The returned promise is memoized per resolved URL: concurrent and repeated\n * calls share one in-flight import. A failed load clears the memo so a later\n * call retries. Rejections are always {@link MessagingSdkLoadError}.\n *\n * @example\n * const { createAdaEmbedInterface } = await loadMessagingSdk();\n */\nexport function loadMessagingSdk(\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<MessagingSdkModule> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tlet url: string;\n\ttry {\n\t\turl = resolveSdkBootstrapUrl(options.cdnBase, options.pinBuildSha);\n\t} catch (error) {\n\t\treturn Promise.reject(error);\n\t}\n\n\tlet inFlight = inFlightByImporter.get(importModule);\n\tif (!inFlight) {\n\t\tinFlight = new Map();\n\t\tinFlightByImporter.set(importModule, inFlight);\n\t}\n\tconst existing = inFlight.get(url);\n\tif (existing) {\n\t\treturn existing;\n\t}\n\n\tconst cache = inFlight;\n\tconst pending = importSdkModule(url, importModule);\n\tcache.set(url, pending);\n\tpending.catch(() => {\n\t\tif (cache.get(url) === pending) {\n\t\t\tcache.delete(url);\n\t\t}\n\t});\n\treturn pending;\n}\n\n// Keyed by import seam like the module memo above: production memoizes on\n// the module-level default while each injected test seam gets an isolated\n// cache. `ada` is set when the promise resolves, so the stopped-interface\n// diagnostic can consult the resolved facade without wrapping it.\ninterface StartedInterfaceMemo {\n\tpromise: Promise<AdaMessagingInterface>;\n\tsettings: AdaMessagingStartOptions;\n\tada?: AdaMessagingInterface;\n}\n\nconst startedInterfaceByImporter = new WeakMap<\n\tImportSdkModule,\n\tStartedInterfaceMemo\n>();\n\n// Ungated like the runtime logger's dropped-host-value channel: both call\n// sites fire only on host misuse, never on an ordinary page load.\nfunction warnHost(message: string): void {\n\ttry {\n\t\t// biome-ignore lint/suspicious/noConsole: the loader's sanctioned host-page diagnostic sink\n\t\tconsole.warn(`[Ada] ${message}`);\n\t} catch {\n\t\t// A host may replace console methods; diagnostics must not throw.\n\t}\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\n// Structural equality over the settings surface. Two functions compare equal:\n// fresh inline callbacks on a re-run (React Strict Mode effects) are not a\n// settings change worth a warning. Exotic objects compare by reference.\nfunction settingsEqual(a: unknown, b: unknown): boolean {\n\tif (Object.is(a, b)) {\n\t\treturn true;\n\t}\n\tif (typeof a === \"function\" && typeof b === \"function\") {\n\t\treturn true;\n\t}\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\treturn (\n\t\t\ta.length === b.length &&\n\t\t\ta.every((item, index) => settingsEqual(item, b[index]))\n\t\t);\n\t}\n\tif (isPlainObject(a) && isPlainObject(b)) {\n\t\tconst aKeys = Object.keys(a);\n\t\tconst bKeys = new Set(Object.keys(b));\n\t\treturn (\n\t\t\taKeys.length === bKeys.size &&\n\t\t\taKeys.every((key) => bKeys.has(key) && settingsEqual(a[key], b[key]))\n\t\t);\n\t}\n\treturn false;\n}\n\n// Observes the facade's symbol-keyed lifecycle getter (embed-lifecycle.ts)\n// instead of wrapping stop/start, so the customer's interface is never\n// mutated; a pinned runtime that predates the getter yields no flag and the\n// stopped-interface diagnostic is skipped rather than guessed.\nfunction isInterfaceStopped(ada: AdaMessagingInterface): boolean {\n\treturn readEmbedStoppedFlag(ada) === true;\n}\n\nasync function startAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions,\n): Promise<AdaMessagingInterface> {\n\tconst sdkModule = await loadMessagingSdk(options);\n\tconst ada = sdkModule.createAdaEmbedInterface();\n\tawait ada.start(settings);\n\treturn ada;\n}\n\n/**\n * Load the SDK, create the messaging interface, and start the widget —\n * the npm-first equivalent of the script-tag + `window.adaSettings` setup.\n *\n * One widget per page: the returned promise is memoized, so repeated and\n * concurrent calls resolve to the same started interface and the first\n * call's settings win. A failed load or start clears the memo so a later\n * call retries. Manage the running widget through the returned interface:\n * `stop()` tears it down and a later `start(settings)` on the same interface\n * runs again. Calling {@link loadAdaMessaging} after `stop()` returns the\n * same stopped interface without restarting it. Both memo outcomes log a\n * console warning: ignored differing settings, and a resolved interface\n * whose widget was stopped.\n *\n * Never assigns the interface to `window` or any other global: the returned\n * reference is the only handle, so hold it in module scope. A reference in\n * module scope is reachable only by your own code, never by other scripts on\n * the page.\n *\n * @example\n * // module scope: your code keeps the only reference\n * const ada = await loadAdaMessaging({\n * handle: \"my-bot\",\n * enableProgrammaticControl: true,\n * });\n * await ada.sendMessage(\"Hello\");\n */\nexport function loadAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<AdaMessagingInterface> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tconst existing = startedInterfaceByImporter.get(importModule);\n\tif (existing) {\n\t\tif (!settingsEqual(settings, existing.settings)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() was already called: the new settings are ignored and the existing interface is returned. To apply new settings, call stop() then start(settings) on it.\",\n\t\t\t);\n\t\t}\n\t\tif (existing.ada && isInterfaceStopped(existing.ada)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() resolved an interface whose widget was stopped and never restarts it. Call start(settings) on it to run the widget again.\",\n\t\t\t);\n\t\t}\n\t\treturn existing.promise;\n\t}\n\tconst memo: StartedInterfaceMemo = {\n\t\tpromise: startAdaMessaging(settings, options),\n\t\tsettings,\n\t};\n\tstartedInterfaceByImporter.set(importModule, memo);\n\tmemo.promise.then(\n\t\t(ada) => {\n\t\t\tmemo.ada = ada;\n\t\t},\n\t\t() => {\n\t\t\tif (startedInterfaceByImporter.get(importModule) === memo) {\n\t\t\t\tstartedInterfaceByImporter.delete(importModule);\n\t\t\t}\n\t\t},\n\t);\n\treturn memo.promise;\n}\n"],"mappings":"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"}
package/dist/npm/index.js CHANGED
@@ -12,7 +12,7 @@ var UNSTAMPED_SHA_PATTERN = /^0{40}$/;
12
12
  * unstamped 40-zero placeholder ({@link isCdnBuildShaStamped} returns
13
13
  * `false`); only published tarballs carry a real SHA.
14
14
  */
15
- var CDN_BUILD_SHA = "811ee8fe6daeb693bc1b95e3a41ccb82a9d763eb";
15
+ var CDN_BUILD_SHA = "9fb7ddd694e3b909a69beb04310922e2e97ffa49";
16
16
  /**
17
17
  * Whether `sha` (default {@link CDN_BUILD_SHA}) is a stamped full git commit
18
18
  * SHA rather than the unstamped repo placeholder.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/npm/build-info.ts","../../src/client/embed-lifecycle.ts","../../src/npm/loader.ts"],"sourcesContent":["// The repo copy carries the 40-zero placeholder; the publish workflow\n// (.github/workflows/messaging-publish-web-packages.yml) stamps the real\n// commit SHA into the built output via scripts/stamp-cdn-build-sha.mjs and\n// verifies the packed tarball before `npm publish`. Keep the placeholder as\n// a single 40-zero string literal: the stamp is a same-length text\n// replacement, so source maps stay valid.\nconst GIT_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_SHA_PATTERN = /^0{40}$/;\n\n/**\n * Git commit SHA of the monorepo commit this npm version was published from.\n * The CDN pipeline deploys each main commit's runtime under an immutable\n * SHA root, so this value names the CDN build associated with this npm\n * version. Pass it as the `pinBuildSha` loader option to load exactly that\n * runtime build — not recommended for production (see the README).\n *\n * In the repository, and in any locally built copy, the value is the\n * unstamped 40-zero placeholder ({@link isCdnBuildShaStamped} returns\n * `false`); only published tarballs carry a real SHA.\n */\nexport const CDN_BUILD_SHA: string = \"811ee8fe6daeb693bc1b95e3a41ccb82a9d763eb\";\n\n/**\n * Whether `sha` (default {@link CDN_BUILD_SHA}) is a stamped full git commit\n * SHA rather than the unstamped repo placeholder.\n */\nexport function isCdnBuildShaStamped(sha: string = CDN_BUILD_SHA): boolean {\n\tconst normalized = sha.trim().toLowerCase();\n\treturn (\n\t\tGIT_SHA_PATTERN.test(normalized) && !UNSTAMPED_SHA_PATTERN.test(normalized)\n\t);\n}\n","/**\n * Lifecycle channel between `createAdaEmbedInterface` and the npm loader's\n * memo: a non-enumerable getter over closure state, so the loader never\n * mutates the public surface and `Object.freeze` on the facade still works.\n * `Symbol.for` because the loader and the CDN runtime are separate builds;\n * a runtime that predates the getter yields `undefined` — degrade gracefully.\n */\nexport const EMBED_LIFECYCLE_STOPPED = Symbol.for(\n\t\"ada-cx/messaging-sdk/embed-stopped\",\n);\n\n/** Read the facade's stopped flag; `undefined` when the runtime predates it. */\nexport function readEmbedStoppedFlag(ada: object): boolean | undefined {\n\tconst value = (ada as Record<symbol, unknown>)[EMBED_LIFECYCLE_STOPPED];\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\n/** Define the getter at facade creation time, before any host freeze. */\nexport function defineEmbedStoppedFlag(\n\tada: object,\n\treadStopped: () => boolean,\n): void {\n\tObject.defineProperty(ada, EMBED_LIFECYCLE_STOPPED, {\n\t\tget: readStopped,\n\t\tenumerable: false,\n\t\tconfigurable: false,\n\t});\n}\n","import type { AdaMessagingClient } from \"../client/client\";\nimport type { AdaMessagingStartOptions } from \"../client/client-types\";\nimport type {\n\tAdaEmbedInterface,\n\tcreateAdaEmbedInterface,\n\tdispatchEmbedLoaderInitialActionQueue,\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff,\n} from \"../client/embed-interface\";\nimport { readEmbedStoppedFlag } from \"../client/embed-lifecycle\";\nimport type {\n\tmountAdaWebViewRuntime,\n\tresolveAdaWebViewRuntimeAssets,\n} from \"../mobile-webview-runtime\";\n\n/**\n * Production CDN root the loader resolves the SDK bootstrap from when no\n * `cdnBase` override is given. The npm package never pins a runtime version;\n * the bootstrap at this root resolves the current rollout at load time.\n */\nexport const DEFAULT_MESSAGING_CDN_BASE =\n\t\"https://messaging-assets.ada.support/\";\n\nconst SDK_BOOTSTRAP_FILENAME = \"sdk.js\";\n\n// The CDN pipeline deploys each main commit's runtime as an immutable\n// SHA-rooted entry (`<cdnBase>/<sha>/sdk/index.js`); the mutable `sdk.js`\n// bootstrap resolves the current rollout to one of those roots at load time.\n// A pin bypasses the bootstrap and imports one root directly.\nconst PINNED_SDK_ENTRY_PATH = \"sdk/index.js\";\n\n// Strict on purpose: the pin is interpolated into a CDN path, so only a full\n// lowercase 40-hex git SHA may pass (mirrors scripts/sdk.js's\n// VERSION_OVERRIDE_PATTERN security boundary, minus the short-SHA and pr-N\n// forms — the npm surface pins exactly what CDN_BUILD_SHA carries).\nconst PINNED_BUILD_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_BUILD_SHA_PATTERN = /^0{40}$/;\n\n/** Why a {@link MessagingSdkLoadError} was raised. */\nexport type MessagingSdkLoadErrorCode =\n\t/**\n\t * The `cdnBase` option is not a valid `https:` URL (plain `http:` is\n\t * allowed only for loopback hosts).\n\t */\n\t| \"invalid_cdn_base\"\n\t/** The `pinBuildSha` option is not a full 40-character hex git SHA. */\n\t| \"invalid_build_sha\"\n\t/**\n\t * The `pinBuildSha` option is the unstamped 40-zero placeholder: this\n\t * copy of the package was built from the repository, not published, so\n\t * its `CDN_BUILD_SHA` names no CDN build.\n\t */\n\t| \"unstamped_build_sha\"\n\t/** The dynamic import of the CDN bootstrap failed (network, CSP, 404). */\n\t| \"sdk_import_failed\"\n\t/** The imported module does not expose the Ada Messaging SDK surface. */\n\t| \"sdk_module_invalid\";\n\n/** Typed failure raised by {@link loadMessagingSdk} and {@link loadAdaMessaging}. */\nexport class MessagingSdkLoadError extends Error {\n\treadonly code: MessagingSdkLoadErrorCode;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: MessagingSdkLoadErrorCode,\n\t\tcause?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MessagingSdkLoadError\";\n\t\tthis.code = code;\n\t\tif (cause !== undefined) {\n\t\t\tthis.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * The programmatic messaging interface {@link loadAdaMessaging} resolves. An\n * alias of the runtime's `AdaEmbedInterface`, which keeps that name because it\n * is also the legacy `window.adaEmbed` compat facade.\n */\nexport type AdaMessagingInterface = AdaEmbedInterface;\n\n/**\n * The public surface of the CDN-resolved SDK module, as loaded from\n * `<cdnBase>/sdk.js`. Types only — the implementation always comes from the\n * CDN at load time and is never part of this npm package.\n */\nexport interface MessagingSdkModule {\n\tAdaMessagingClient: typeof AdaMessagingClient;\n\tcreateAdaEmbedInterface: typeof createAdaEmbedInterface;\n\tdispatchEmbedLoaderInitialActionQueue: typeof dispatchEmbedLoaderInitialActionQueue;\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff: typeof dispatchEmbedLoaderInitialActionQueueForAtomicHandoff;\n\tmountAdaWebViewRuntime: typeof mountAdaWebViewRuntime;\n\tresolveAdaWebViewRuntimeAssets: typeof resolveAdaWebViewRuntimeAssets;\n}\n\ntype ImportSdkModule = (url: string) => Promise<unknown>;\n\nexport interface LoadMessagingSdkOptions {\n\t/**\n\t * CDN root to resolve `sdk.js` from. Must be an `https:` URL; plain\n\t * `http:` is accepted only for loopback hosts (`localhost`, `127.0.0.1`,\n\t * `[::1]`, `*.localhost`) during local development. Defaults to the\n\t * production root ({@link DEFAULT_MESSAGING_CDN_BASE}). Override it with\n\t * the Ada-provided asset host for a staging validation window only;\n\t * production pages must keep the default.\n\t */\n\tcdnBase?: string;\n\t/**\n\t * Full 40-character git commit SHA of a deployed CDN build. When set, the\n\t * loader skips the rollout bootstrap and imports that build's immutable\n\t * entry (`<cdnBase>/<sha>/sdk/index.js`) directly. Pass the package's\n\t * `CDN_BUILD_SHA` export to pin the build associated with this npm\n\t * version. NOT recommended for production: a pinned runtime misses Ada's\n\t * fixes and rollouts, and may predate server contract changes. Composes\n\t * with `cdnBase` for staged validation.\n\t */\n\tpinBuildSha?: string;\n\t/**\n\t * Import seam for unit tests. Defaults to a native dynamic `import()` of\n\t * the resolved bootstrap URL.\n\t */\n\timportModule?: ImportSdkModule;\n}\n\n// Both annotations are load-bearing: bundlers rewrite bare dynamic imports,\n// and this specifier must reach the browser untouched so the runtime always\n// resolves from Ada's CDN. The npm build for this entry is unminified so the\n// annotations survive into the published output.\nconst defaultImportModule: ImportSdkModule = (url) =>\n\timport(/* webpackIgnore: true */ /* @vite-ignore */ url);\n\n// Keyed by import seam so production memoizes on the module-level default\n// while each injected test seam gets an isolated cache.\nconst inFlightByImporter = new WeakMap<\n\tImportSdkModule,\n\tMap<string, Promise<MessagingSdkModule>>\n>();\n\n// Mirrors the loopback semantics of the shared `isLoopbackHost` /\n// `validateMessagingEndpoint` (shared/utils/src/url.ts) and the bridge\n// loader's copy (packages/bridge/src/npm/loader.ts). Inlined because the\n// published loader must stay dependency-free; keep the host set in sync.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\n\nfunction isLoopbackHostname(hostname: string): boolean {\n\tconst normalized = hostname.trim().toLowerCase();\n\t// `*.localhost` resolves to the loopback interface and is \"potentially\n\t// trustworthy\" per the Secure Contexts spec; no production host ends in\n\t// `.localhost`.\n\treturn LOOPBACK_HOSTS.has(normalized) || normalized.endsWith(\".localhost\");\n}\n\nfunction resolvePinnedBuildSha(pinBuildSha: string): string {\n\tconst sha = pinBuildSha.trim().toLowerCase();\n\tif (!PINNED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The pinBuildSha option must be a full 40-character hex git SHA: \"${pinBuildSha}\".`,\n\t\t\t\"invalid_build_sha\",\n\t\t);\n\t}\n\tif (UNSTAMPED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t\"The pinBuildSha option is the unstamped CDN_BUILD_SHA placeholder. Only the published npm package carries a stamped SHA; a repository or local build cannot pin a CDN build.\",\n\t\t\t\"unstamped_build_sha\",\n\t\t);\n\t}\n\treturn sha;\n}\n\n/**\n * The runtime this URL serves carries the SDK's security logic (endpoint\n * validation, session handling, the origin checks), so a cleartext transport\n * would let an on-path attacker replace it wholesale: `https:` is required,\n * with a loopback-only `http:` exception for local development.\n */\nfunction resolveSdkBootstrapUrl(\n\tcdnBase: string | undefined,\n\tpinBuildSha: string | undefined,\n): string {\n\tconst base = (cdnBase ?? DEFAULT_MESSAGING_CDN_BASE).trim();\n\t// Resolving against a base without a trailing slash would drop its last\n\t// path segment, so normalize before building the bootstrap URL.\n\tconst normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n\tconst entryPath =\n\t\tpinBuildSha === undefined\n\t\t\t? SDK_BOOTSTRAP_FILENAME\n\t\t\t: `${resolvePinnedBuildSha(pinBuildSha)}/${PINNED_SDK_ENTRY_PATH}`;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(entryPath, normalizedBase);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option is not a valid URL: \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t\tcause,\n\t\t);\n\t}\n\tconst isHttps = url.protocol === \"https:\";\n\tconst isLoopbackHttp =\n\t\turl.protocol === \"http:\" && isLoopbackHostname(url.hostname);\n\tif (!isHttps && !isLoopbackHttp) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option must use https (plain http is allowed only for loopback hosts): \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t);\n\t}\n\treturn url.toString();\n}\n\n// A partial CDN module (mid-deploy, or a rolled-back build missing a newer\n// export) must fail here as a typed sdk_module_invalid error, not later as a\n// TypeError on the first missing call — so every export is checked. The\n// `satisfies` clause rejects names that are not interface members; the\n// exhaustiveness type below is what forces this list to grow with it.\nconst REQUIRED_SDK_EXPORTS = [\n\t\"AdaMessagingClient\",\n\t\"createAdaEmbedInterface\",\n\t\"dispatchEmbedLoaderInitialActionQueue\",\n\t\"dispatchEmbedLoaderInitialActionQueueForAtomicHandoff\",\n\t\"mountAdaWebViewRuntime\",\n\t\"resolveAdaWebViewRuntimeAssets\",\n] as const satisfies readonly (keyof MessagingSdkModule)[];\n\n// Compile-time exhaustiveness: adding a MessagingSdkModule member without\n// listing it in REQUIRED_SDK_EXPORTS turns this type into `never`, failing\n// the `true` assignment below.\ntype AllSdkExportsListed =\n\tExclude<\n\t\tkeyof MessagingSdkModule,\n\t\t(typeof REQUIRED_SDK_EXPORTS)[number]\n\t> extends never\n\t\t? true\n\t\t: never;\nconst _allSdkExportsListed: AllSdkExportsListed = true;\nvoid _allSdkExportsListed;\n\nfunction isMessagingSdkModule(value: unknown): value is MessagingSdkModule {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst candidate = value as Record<string, unknown>;\n\treturn REQUIRED_SDK_EXPORTS.every(\n\t\t(key) => typeof candidate[key] === \"function\",\n\t);\n}\n\nasync function importSdkModule(\n\turl: string,\n\timportModule: ImportSdkModule,\n): Promise<MessagingSdkModule> {\n\tlet loaded: unknown;\n\ttry {\n\t\tloaded = await importModule(url);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The Ada Messaging SDK failed to load from ${url}.`,\n\t\t\t\"sdk_import_failed\",\n\t\t\tcause,\n\t\t);\n\t}\n\tif (!isMessagingSdkModule(loaded)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The module at ${url} does not expose the Ada Messaging SDK interface.`,\n\t\t\t\"sdk_module_invalid\",\n\t\t);\n\t}\n\treturn loaded;\n}\n\n/**\n * Load the Ada Messaging SDK module from Ada's CDN.\n *\n * The returned promise is memoized per resolved URL: concurrent and repeated\n * calls share one in-flight import. A failed load clears the memo so a later\n * call retries. Rejections are always {@link MessagingSdkLoadError}.\n *\n * @example\n * const { createAdaEmbedInterface } = await loadMessagingSdk();\n */\nexport function loadMessagingSdk(\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<MessagingSdkModule> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tlet url: string;\n\ttry {\n\t\turl = resolveSdkBootstrapUrl(options.cdnBase, options.pinBuildSha);\n\t} catch (error) {\n\t\treturn Promise.reject(error);\n\t}\n\n\tlet inFlight = inFlightByImporter.get(importModule);\n\tif (!inFlight) {\n\t\tinFlight = new Map();\n\t\tinFlightByImporter.set(importModule, inFlight);\n\t}\n\tconst existing = inFlight.get(url);\n\tif (existing) {\n\t\treturn existing;\n\t}\n\n\tconst cache = inFlight;\n\tconst pending = importSdkModule(url, importModule);\n\tcache.set(url, pending);\n\tpending.catch(() => {\n\t\tif (cache.get(url) === pending) {\n\t\t\tcache.delete(url);\n\t\t}\n\t});\n\treturn pending;\n}\n\n// Keyed by import seam like the module memo above: production memoizes on\n// the module-level default while each injected test seam gets an isolated\n// cache. `ada` is set when the promise resolves, so the stopped-interface\n// diagnostic can consult the resolved facade without wrapping it.\ninterface StartedInterfaceMemo {\n\tpromise: Promise<AdaMessagingInterface>;\n\tsettings: AdaMessagingStartOptions;\n\tada?: AdaMessagingInterface;\n}\n\nconst startedInterfaceByImporter = new WeakMap<\n\tImportSdkModule,\n\tStartedInterfaceMemo\n>();\n\n// Ungated like the runtime logger's dropped-host-value channel: both call\n// sites fire only on host misuse, never on an ordinary page load.\nfunction warnHost(message: string): void {\n\ttry {\n\t\t// biome-ignore lint/suspicious/noConsole: the loader's sanctioned host-page diagnostic sink\n\t\tconsole.warn(`[Ada] ${message}`);\n\t} catch {\n\t\t// A host may replace console methods; diagnostics must not throw.\n\t}\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\n// Structural equality over the settings surface. Two functions compare equal:\n// fresh inline callbacks on a re-run (React Strict Mode effects) are not a\n// settings change worth a warning. Exotic objects compare by reference.\nfunction settingsEqual(a: unknown, b: unknown): boolean {\n\tif (Object.is(a, b)) {\n\t\treturn true;\n\t}\n\tif (typeof a === \"function\" && typeof b === \"function\") {\n\t\treturn true;\n\t}\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\treturn (\n\t\t\ta.length === b.length &&\n\t\t\ta.every((item, index) => settingsEqual(item, b[index]))\n\t\t);\n\t}\n\tif (isPlainObject(a) && isPlainObject(b)) {\n\t\tconst aKeys = Object.keys(a);\n\t\tconst bKeys = new Set(Object.keys(b));\n\t\treturn (\n\t\t\taKeys.length === bKeys.size &&\n\t\t\taKeys.every((key) => bKeys.has(key) && settingsEqual(a[key], b[key]))\n\t\t);\n\t}\n\treturn false;\n}\n\n// Observes the facade's symbol-keyed lifecycle getter (embed-lifecycle.ts)\n// instead of wrapping stop/start, so the customer's interface is never\n// mutated; a pinned runtime that predates the getter yields no flag and the\n// stopped-interface diagnostic is skipped rather than guessed.\nfunction isInterfaceStopped(ada: AdaMessagingInterface): boolean {\n\treturn readEmbedStoppedFlag(ada) === true;\n}\n\nasync function startAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions,\n): Promise<AdaMessagingInterface> {\n\tconst sdkModule = await loadMessagingSdk(options);\n\tconst ada = sdkModule.createAdaEmbedInterface();\n\tawait ada.start(settings);\n\treturn ada;\n}\n\n/**\n * Load the SDK, create the messaging interface, and start the widget —\n * the npm-first equivalent of the script-tag + `window.adaSettings` setup.\n *\n * One widget per page: the returned promise is memoized, so repeated and\n * concurrent calls resolve to the same started interface and the first\n * call's settings win. A failed load or start clears the memo so a later\n * call retries. Manage the running widget through the returned interface:\n * `stop()` tears it down and a later `start(settings)` on the same interface\n * runs again. Calling {@link loadAdaMessaging} after `stop()` returns the\n * same stopped interface without restarting it. Both memo outcomes log a\n * console warning: ignored differing settings, and a resolved interface\n * whose widget was stopped.\n *\n * Never assigns the interface to `window` or any other global: the returned\n * reference is the only handle, so hold it in module scope. A reference in\n * module scope is reachable only by your own code, never by other scripts on\n * the page.\n *\n * @example\n * // module scope: your code keeps the only reference\n * const ada = await loadAdaMessaging({\n * handle: \"my-bot\",\n * enableProgrammaticControl: true,\n * });\n * await ada.sendMessage(\"Hello\");\n */\nexport function loadAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<AdaMessagingInterface> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tconst existing = startedInterfaceByImporter.get(importModule);\n\tif (existing) {\n\t\tif (!settingsEqual(settings, existing.settings)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() was already called: the new settings are ignored and the existing interface is returned. To apply new settings, call stop() then start(settings) on it.\",\n\t\t\t);\n\t\t}\n\t\tif (existing.ada && isInterfaceStopped(existing.ada)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() resolved an interface whose widget was stopped and never restarts it. Call start(settings) on it to run the widget again.\",\n\t\t\t);\n\t\t}\n\t\treturn existing.promise;\n\t}\n\tconst memo: StartedInterfaceMemo = {\n\t\tpromise: startAdaMessaging(settings, options),\n\t\tsettings,\n\t};\n\tstartedInterfaceByImporter.set(importModule, memo);\n\tmemo.promise.then(\n\t\t(ada) => {\n\t\t\tmemo.ada = ada;\n\t\t},\n\t\t() => {\n\t\t\tif (startedInterfaceByImporter.get(importModule) === memo) {\n\t\t\t\tstartedInterfaceByImporter.delete(importModule);\n\t\t\t}\n\t\t},\n\t);\n\treturn memo.promise;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/npm/build-info.ts","../../src/client/embed-lifecycle.ts","../../src/npm/loader.ts"],"sourcesContent":["// The repo copy carries the 40-zero placeholder; the publish workflow\n// (.github/workflows/messaging-publish-web-packages.yml) stamps the real\n// commit SHA into the built output via scripts/stamp-cdn-build-sha.mjs and\n// verifies the packed tarball before `npm publish`. Keep the placeholder as\n// a single 40-zero string literal: the stamp is a same-length text\n// replacement, so source maps stay valid.\nconst GIT_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_SHA_PATTERN = /^0{40}$/;\n\n/**\n * Git commit SHA of the monorepo commit this npm version was published from.\n * The CDN pipeline deploys each main commit's runtime under an immutable\n * SHA root, so this value names the CDN build associated with this npm\n * version. Pass it as the `pinBuildSha` loader option to load exactly that\n * runtime build — not recommended for production (see the README).\n *\n * In the repository, and in any locally built copy, the value is the\n * unstamped 40-zero placeholder ({@link isCdnBuildShaStamped} returns\n * `false`); only published tarballs carry a real SHA.\n */\nexport const CDN_BUILD_SHA: string = \"9fb7ddd694e3b909a69beb04310922e2e97ffa49\";\n\n/**\n * Whether `sha` (default {@link CDN_BUILD_SHA}) is a stamped full git commit\n * SHA rather than the unstamped repo placeholder.\n */\nexport function isCdnBuildShaStamped(sha: string = CDN_BUILD_SHA): boolean {\n\tconst normalized = sha.trim().toLowerCase();\n\treturn (\n\t\tGIT_SHA_PATTERN.test(normalized) && !UNSTAMPED_SHA_PATTERN.test(normalized)\n\t);\n}\n","/**\n * Lifecycle channel between `createAdaEmbedInterface` and the npm loader's\n * memo: a non-enumerable getter over closure state, so the loader never\n * mutates the public surface and `Object.freeze` on the facade still works.\n * `Symbol.for` because the loader and the CDN runtime are separate builds;\n * a runtime that predates the getter yields `undefined` — degrade gracefully.\n */\nexport const EMBED_LIFECYCLE_STOPPED = Symbol.for(\n\t\"ada-cx/messaging-sdk/embed-stopped\",\n);\n\n/** Read the facade's stopped flag; `undefined` when the runtime predates it. */\nexport function readEmbedStoppedFlag(ada: object): boolean | undefined {\n\tconst value = (ada as Record<symbol, unknown>)[EMBED_LIFECYCLE_STOPPED];\n\treturn typeof value === \"boolean\" ? value : undefined;\n}\n\n/** Define the getter at facade creation time, before any host freeze. */\nexport function defineEmbedStoppedFlag(\n\tada: object,\n\treadStopped: () => boolean,\n): void {\n\tObject.defineProperty(ada, EMBED_LIFECYCLE_STOPPED, {\n\t\tget: readStopped,\n\t\tenumerable: false,\n\t\tconfigurable: false,\n\t});\n}\n","import type { AdaMessagingClient } from \"../client/client\";\nimport type { AdaMessagingStartOptions } from \"../client/client-types\";\nimport type {\n\tAdaEmbedInterface,\n\tcreateAdaEmbedInterface,\n\tdispatchEmbedLoaderInitialActionQueue,\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff,\n} from \"../client/embed-interface\";\nimport { readEmbedStoppedFlag } from \"../client/embed-lifecycle\";\nimport type {\n\tmountAdaWebViewRuntime,\n\tresolveAdaWebViewRuntimeAssets,\n} from \"../mobile-webview-runtime\";\n\n/**\n * Production CDN root the loader resolves the SDK bootstrap from when no\n * `cdnBase` override is given. The npm package never pins a runtime version;\n * the bootstrap at this root resolves the current rollout at load time.\n */\nexport const DEFAULT_MESSAGING_CDN_BASE =\n\t\"https://messaging-assets.ada.support/\";\n\nconst SDK_BOOTSTRAP_FILENAME = \"sdk.js\";\n\n// The CDN pipeline deploys each main commit's runtime as an immutable\n// SHA-rooted entry (`<cdnBase>/<sha>/sdk/index.js`); the mutable `sdk.js`\n// bootstrap resolves the current rollout to one of those roots at load time.\n// A pin bypasses the bootstrap and imports one root directly.\nconst PINNED_SDK_ENTRY_PATH = \"sdk/index.js\";\n\n// Strict on purpose: the pin is interpolated into a CDN path, so only a full\n// lowercase 40-hex git SHA may pass (mirrors scripts/sdk.js's\n// VERSION_OVERRIDE_PATTERN security boundary, minus the short-SHA and pr-N\n// forms — the npm surface pins exactly what CDN_BUILD_SHA carries).\nconst PINNED_BUILD_SHA_PATTERN = /^[0-9a-f]{40}$/;\nconst UNSTAMPED_BUILD_SHA_PATTERN = /^0{40}$/;\n\n/** Why a {@link MessagingSdkLoadError} was raised. */\nexport type MessagingSdkLoadErrorCode =\n\t/**\n\t * The `cdnBase` option is not a valid `https:` URL (plain `http:` is\n\t * allowed only for loopback hosts).\n\t */\n\t| \"invalid_cdn_base\"\n\t/** The `pinBuildSha` option is not a full 40-character hex git SHA. */\n\t| \"invalid_build_sha\"\n\t/**\n\t * The `pinBuildSha` option is the unstamped 40-zero placeholder: this\n\t * copy of the package was built from the repository, not published, so\n\t * its `CDN_BUILD_SHA` names no CDN build.\n\t */\n\t| \"unstamped_build_sha\"\n\t/** The dynamic import of the CDN bootstrap failed (network, CSP, 404). */\n\t| \"sdk_import_failed\"\n\t/** The imported module does not expose the Ada Messaging SDK surface. */\n\t| \"sdk_module_invalid\";\n\n/** Typed failure raised by {@link loadMessagingSdk} and {@link loadAdaMessaging}. */\nexport class MessagingSdkLoadError extends Error {\n\treadonly code: MessagingSdkLoadErrorCode;\n\n\tconstructor(\n\t\tmessage: string,\n\t\tcode: MessagingSdkLoadErrorCode,\n\t\tcause?: unknown,\n\t) {\n\t\tsuper(message);\n\t\tthis.name = \"MessagingSdkLoadError\";\n\t\tthis.code = code;\n\t\tif (cause !== undefined) {\n\t\t\tthis.cause = cause;\n\t\t}\n\t}\n}\n\n/**\n * The programmatic messaging interface {@link loadAdaMessaging} resolves. An\n * alias of the runtime's `AdaEmbedInterface`, which keeps that name because it\n * is also the legacy `window.adaEmbed` compat facade.\n */\nexport type AdaMessagingInterface = AdaEmbedInterface;\n\n/**\n * The public surface of the CDN-resolved SDK module, as loaded from\n * `<cdnBase>/sdk.js`. Types only — the implementation always comes from the\n * CDN at load time and is never part of this npm package.\n */\nexport interface MessagingSdkModule {\n\tAdaMessagingClient: typeof AdaMessagingClient;\n\tcreateAdaEmbedInterface: typeof createAdaEmbedInterface;\n\tdispatchEmbedLoaderInitialActionQueue: typeof dispatchEmbedLoaderInitialActionQueue;\n\tdispatchEmbedLoaderInitialActionQueueForAtomicHandoff: typeof dispatchEmbedLoaderInitialActionQueueForAtomicHandoff;\n\tmountAdaWebViewRuntime: typeof mountAdaWebViewRuntime;\n\tresolveAdaWebViewRuntimeAssets: typeof resolveAdaWebViewRuntimeAssets;\n}\n\ntype ImportSdkModule = (url: string) => Promise<unknown>;\n\nexport interface LoadMessagingSdkOptions {\n\t/**\n\t * CDN root to resolve `sdk.js` from. Must be an `https:` URL; plain\n\t * `http:` is accepted only for loopback hosts (`localhost`, `127.0.0.1`,\n\t * `[::1]`, `*.localhost`) during local development. Defaults to the\n\t * production root ({@link DEFAULT_MESSAGING_CDN_BASE}). Override it with\n\t * the Ada-provided asset host for a staging validation window only;\n\t * production pages must keep the default.\n\t */\n\tcdnBase?: string;\n\t/**\n\t * Full 40-character git commit SHA of a deployed CDN build. When set, the\n\t * loader skips the rollout bootstrap and imports that build's immutable\n\t * entry (`<cdnBase>/<sha>/sdk/index.js`) directly. Pass the package's\n\t * `CDN_BUILD_SHA` export to pin the build associated with this npm\n\t * version. NOT recommended for production: a pinned runtime misses Ada's\n\t * fixes and rollouts, and may predate server contract changes. Composes\n\t * with `cdnBase` for staged validation.\n\t */\n\tpinBuildSha?: string;\n\t/**\n\t * Import seam for unit tests. Defaults to a native dynamic `import()` of\n\t * the resolved bootstrap URL.\n\t */\n\timportModule?: ImportSdkModule;\n}\n\n// Both annotations are load-bearing: bundlers rewrite bare dynamic imports,\n// and this specifier must reach the browser untouched so the runtime always\n// resolves from Ada's CDN. The npm build for this entry is unminified so the\n// annotations survive into the published output.\nconst defaultImportModule: ImportSdkModule = (url) =>\n\timport(/* webpackIgnore: true */ /* @vite-ignore */ url);\n\n// Keyed by import seam so production memoizes on the module-level default\n// while each injected test seam gets an isolated cache.\nconst inFlightByImporter = new WeakMap<\n\tImportSdkModule,\n\tMap<string, Promise<MessagingSdkModule>>\n>();\n\n// Mirrors the loopback semantics of the shared `isLoopbackHost` /\n// `validateMessagingEndpoint` (shared/utils/src/url.ts) and the bridge\n// loader's copy (packages/bridge/src/npm/loader.ts). Inlined because the\n// published loader must stay dependency-free; keep the host set in sync.\nconst LOOPBACK_HOSTS = new Set([\"localhost\", \"127.0.0.1\", \"[::1]\", \"::1\"]);\n\nfunction isLoopbackHostname(hostname: string): boolean {\n\tconst normalized = hostname.trim().toLowerCase();\n\t// `*.localhost` resolves to the loopback interface and is \"potentially\n\t// trustworthy\" per the Secure Contexts spec; no production host ends in\n\t// `.localhost`.\n\treturn LOOPBACK_HOSTS.has(normalized) || normalized.endsWith(\".localhost\");\n}\n\nfunction resolvePinnedBuildSha(pinBuildSha: string): string {\n\tconst sha = pinBuildSha.trim().toLowerCase();\n\tif (!PINNED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The pinBuildSha option must be a full 40-character hex git SHA: \"${pinBuildSha}\".`,\n\t\t\t\"invalid_build_sha\",\n\t\t);\n\t}\n\tif (UNSTAMPED_BUILD_SHA_PATTERN.test(sha)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t\"The pinBuildSha option is the unstamped CDN_BUILD_SHA placeholder. Only the published npm package carries a stamped SHA; a repository or local build cannot pin a CDN build.\",\n\t\t\t\"unstamped_build_sha\",\n\t\t);\n\t}\n\treturn sha;\n}\n\n/**\n * The runtime this URL serves carries the SDK's security logic (endpoint\n * validation, session handling, the origin checks), so a cleartext transport\n * would let an on-path attacker replace it wholesale: `https:` is required,\n * with a loopback-only `http:` exception for local development.\n */\nfunction resolveSdkBootstrapUrl(\n\tcdnBase: string | undefined,\n\tpinBuildSha: string | undefined,\n): string {\n\tconst base = (cdnBase ?? DEFAULT_MESSAGING_CDN_BASE).trim();\n\t// Resolving against a base without a trailing slash would drop its last\n\t// path segment, so normalize before building the bootstrap URL.\n\tconst normalizedBase = base.endsWith(\"/\") ? base : `${base}/`;\n\tconst entryPath =\n\t\tpinBuildSha === undefined\n\t\t\t? SDK_BOOTSTRAP_FILENAME\n\t\t\t: `${resolvePinnedBuildSha(pinBuildSha)}/${PINNED_SDK_ENTRY_PATH}`;\n\tlet url: URL;\n\ttry {\n\t\turl = new URL(entryPath, normalizedBase);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option is not a valid URL: \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t\tcause,\n\t\t);\n\t}\n\tconst isHttps = url.protocol === \"https:\";\n\tconst isLoopbackHttp =\n\t\turl.protocol === \"http:\" && isLoopbackHostname(url.hostname);\n\tif (!isHttps && !isLoopbackHttp) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The cdnBase option must use https (plain http is allowed only for loopback hosts): \"${base}\".`,\n\t\t\t\"invalid_cdn_base\",\n\t\t);\n\t}\n\treturn url.toString();\n}\n\n// A partial CDN module (mid-deploy, or a rolled-back build missing a newer\n// export) must fail here as a typed sdk_module_invalid error, not later as a\n// TypeError on the first missing call — so every export is checked. The\n// `satisfies` clause rejects names that are not interface members; the\n// exhaustiveness type below is what forces this list to grow with it.\nconst REQUIRED_SDK_EXPORTS = [\n\t\"AdaMessagingClient\",\n\t\"createAdaEmbedInterface\",\n\t\"dispatchEmbedLoaderInitialActionQueue\",\n\t\"dispatchEmbedLoaderInitialActionQueueForAtomicHandoff\",\n\t\"mountAdaWebViewRuntime\",\n\t\"resolveAdaWebViewRuntimeAssets\",\n] as const satisfies readonly (keyof MessagingSdkModule)[];\n\n// Compile-time exhaustiveness: adding a MessagingSdkModule member without\n// listing it in REQUIRED_SDK_EXPORTS turns this type into `never`, failing\n// the `true` assignment below.\ntype AllSdkExportsListed =\n\tExclude<\n\t\tkeyof MessagingSdkModule,\n\t\t(typeof REQUIRED_SDK_EXPORTS)[number]\n\t> extends never\n\t\t? true\n\t\t: never;\nconst _allSdkExportsListed: AllSdkExportsListed = true;\nvoid _allSdkExportsListed;\n\nfunction isMessagingSdkModule(value: unknown): value is MessagingSdkModule {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst candidate = value as Record<string, unknown>;\n\treturn REQUIRED_SDK_EXPORTS.every(\n\t\t(key) => typeof candidate[key] === \"function\",\n\t);\n}\n\nasync function importSdkModule(\n\turl: string,\n\timportModule: ImportSdkModule,\n): Promise<MessagingSdkModule> {\n\tlet loaded: unknown;\n\ttry {\n\t\tloaded = await importModule(url);\n\t} catch (cause) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The Ada Messaging SDK failed to load from ${url}.`,\n\t\t\t\"sdk_import_failed\",\n\t\t\tcause,\n\t\t);\n\t}\n\tif (!isMessagingSdkModule(loaded)) {\n\t\tthrow new MessagingSdkLoadError(\n\t\t\t`The module at ${url} does not expose the Ada Messaging SDK interface.`,\n\t\t\t\"sdk_module_invalid\",\n\t\t);\n\t}\n\treturn loaded;\n}\n\n/**\n * Load the Ada Messaging SDK module from Ada's CDN.\n *\n * The returned promise is memoized per resolved URL: concurrent and repeated\n * calls share one in-flight import. A failed load clears the memo so a later\n * call retries. Rejections are always {@link MessagingSdkLoadError}.\n *\n * @example\n * const { createAdaEmbedInterface } = await loadMessagingSdk();\n */\nexport function loadMessagingSdk(\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<MessagingSdkModule> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tlet url: string;\n\ttry {\n\t\turl = resolveSdkBootstrapUrl(options.cdnBase, options.pinBuildSha);\n\t} catch (error) {\n\t\treturn Promise.reject(error);\n\t}\n\n\tlet inFlight = inFlightByImporter.get(importModule);\n\tif (!inFlight) {\n\t\tinFlight = new Map();\n\t\tinFlightByImporter.set(importModule, inFlight);\n\t}\n\tconst existing = inFlight.get(url);\n\tif (existing) {\n\t\treturn existing;\n\t}\n\n\tconst cache = inFlight;\n\tconst pending = importSdkModule(url, importModule);\n\tcache.set(url, pending);\n\tpending.catch(() => {\n\t\tif (cache.get(url) === pending) {\n\t\t\tcache.delete(url);\n\t\t}\n\t});\n\treturn pending;\n}\n\n// Keyed by import seam like the module memo above: production memoizes on\n// the module-level default while each injected test seam gets an isolated\n// cache. `ada` is set when the promise resolves, so the stopped-interface\n// diagnostic can consult the resolved facade without wrapping it.\ninterface StartedInterfaceMemo {\n\tpromise: Promise<AdaMessagingInterface>;\n\tsettings: AdaMessagingStartOptions;\n\tada?: AdaMessagingInterface;\n}\n\nconst startedInterfaceByImporter = new WeakMap<\n\tImportSdkModule,\n\tStartedInterfaceMemo\n>();\n\n// Ungated like the runtime logger's dropped-host-value channel: both call\n// sites fire only on host misuse, never on an ordinary page load.\nfunction warnHost(message: string): void {\n\ttry {\n\t\t// biome-ignore lint/suspicious/noConsole: the loader's sanctioned host-page diagnostic sink\n\t\tconsole.warn(`[Ada] ${message}`);\n\t} catch {\n\t\t// A host may replace console methods; diagnostics must not throw.\n\t}\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null) {\n\t\treturn false;\n\t}\n\tconst prototype = Object.getPrototypeOf(value);\n\treturn prototype === Object.prototype || prototype === null;\n}\n\n// Structural equality over the settings surface. Two functions compare equal:\n// fresh inline callbacks on a re-run (React Strict Mode effects) are not a\n// settings change worth a warning. Exotic objects compare by reference.\nfunction settingsEqual(a: unknown, b: unknown): boolean {\n\tif (Object.is(a, b)) {\n\t\treturn true;\n\t}\n\tif (typeof a === \"function\" && typeof b === \"function\") {\n\t\treturn true;\n\t}\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\treturn (\n\t\t\ta.length === b.length &&\n\t\t\ta.every((item, index) => settingsEqual(item, b[index]))\n\t\t);\n\t}\n\tif (isPlainObject(a) && isPlainObject(b)) {\n\t\tconst aKeys = Object.keys(a);\n\t\tconst bKeys = new Set(Object.keys(b));\n\t\treturn (\n\t\t\taKeys.length === bKeys.size &&\n\t\t\taKeys.every((key) => bKeys.has(key) && settingsEqual(a[key], b[key]))\n\t\t);\n\t}\n\treturn false;\n}\n\n// Observes the facade's symbol-keyed lifecycle getter (embed-lifecycle.ts)\n// instead of wrapping stop/start, so the customer's interface is never\n// mutated; a pinned runtime that predates the getter yields no flag and the\n// stopped-interface diagnostic is skipped rather than guessed.\nfunction isInterfaceStopped(ada: AdaMessagingInterface): boolean {\n\treturn readEmbedStoppedFlag(ada) === true;\n}\n\nasync function startAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions,\n): Promise<AdaMessagingInterface> {\n\tconst sdkModule = await loadMessagingSdk(options);\n\tconst ada = sdkModule.createAdaEmbedInterface();\n\tawait ada.start(settings);\n\treturn ada;\n}\n\n/**\n * Load the SDK, create the messaging interface, and start the widget —\n * the npm-first equivalent of the script-tag + `window.adaSettings` setup.\n *\n * One widget per page: the returned promise is memoized, so repeated and\n * concurrent calls resolve to the same started interface and the first\n * call's settings win. A failed load or start clears the memo so a later\n * call retries. Manage the running widget through the returned interface:\n * `stop()` tears it down and a later `start(settings)` on the same interface\n * runs again. Calling {@link loadAdaMessaging} after `stop()` returns the\n * same stopped interface without restarting it. Both memo outcomes log a\n * console warning: ignored differing settings, and a resolved interface\n * whose widget was stopped.\n *\n * Never assigns the interface to `window` or any other global: the returned\n * reference is the only handle, so hold it in module scope. A reference in\n * module scope is reachable only by your own code, never by other scripts on\n * the page.\n *\n * @example\n * // module scope: your code keeps the only reference\n * const ada = await loadAdaMessaging({\n * handle: \"my-bot\",\n * enableProgrammaticControl: true,\n * });\n * await ada.sendMessage(\"Hello\");\n */\nexport function loadAdaMessaging(\n\tsettings: AdaMessagingStartOptions,\n\toptions: LoadMessagingSdkOptions = {},\n): Promise<AdaMessagingInterface> {\n\tconst importModule = options.importModule ?? defaultImportModule;\n\tconst existing = startedInterfaceByImporter.get(importModule);\n\tif (existing) {\n\t\tif (!settingsEqual(settings, existing.settings)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() was already called: the new settings are ignored and the existing interface is returned. To apply new settings, call stop() then start(settings) on it.\",\n\t\t\t);\n\t\t}\n\t\tif (existing.ada && isInterfaceStopped(existing.ada)) {\n\t\t\twarnHost(\n\t\t\t\t\"loadAdaMessaging() resolved an interface whose widget was stopped and never restarts it. Call start(settings) on it to run the widget again.\",\n\t\t\t);\n\t\t}\n\t\treturn existing.promise;\n\t}\n\tconst memo: StartedInterfaceMemo = {\n\t\tpromise: startAdaMessaging(settings, options),\n\t\tsettings,\n\t};\n\tstartedInterfaceByImporter.set(importModule, memo);\n\tmemo.promise.then(\n\t\t(ada) => {\n\t\t\tmemo.ada = ada;\n\t\t},\n\t\t() => {\n\t\t\tif (startedInterfaceByImporter.get(importModule) === memo) {\n\t\t\t\tstartedInterfaceByImporter.delete(importModule);\n\t\t\t}\n\t\t},\n\t);\n\treturn memo.promise;\n}\n"],"mappings":"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"}
@@ -102,6 +102,8 @@ function createMockAdaMessaging(overrides = {}) {
102
102
  setLanguage: async () => void 0,
103
103
  setTheme: async () => void 0,
104
104
  setDeviceToken: async () => void 0,
105
+ acknowledgeSessionMirrorClear: async () => void 0,
106
+ deliverSessionMirrorSeed: async () => void 0,
105
107
  sendMessage: async (body) => {
106
108
  const id = `mock-message-${nextMessageId++}`;
107
109
  conversationId = conversationId ?? "mock-conversation";
@@ -1 +1 @@
1
- {"version":3,"file":"testing.cjs","names":[],"sources":["../../src/npm/testing.ts"],"sourcesContent":["import type {\n\tAdaSdkEventCallback,\n\tMetaFieldValue,\n\tPublicMessage,\n} from \"../client/client-types\";\nimport type { AdaMessagingInterface } from \"./loader\";\n\n/** One recorded method invocation on a {@link MockAdaMessaging}. */\nexport interface RecordedAdaMessagingCall {\n\tmethod: string;\n\targs: unknown[];\n}\n\n/**\n * The mock returned by {@link createMockAdaMessaging}: the full\n * {@link AdaMessagingInterface} plus test hooks for inspecting calls and\n * simulating SDK events.\n */\nexport interface MockAdaMessaging extends AdaMessagingInterface {\n\t/** Every method invocation, in call order. */\n\tcalls: RecordedAdaMessagingCall[];\n\t/**\n\t * Deliver an event to `subscribeEvent` and `subscribeAll` callbacks, as if\n\t * the SDK emitted it (e.g. `$emit(\"ada:ready\", { isRolledOut: true })`).\n\t * Routing matches the real SDK: keyed subscriptions match by prefix\n\t * (`\"ada:campaigns\"` catches `\"ada:campaigns:shown\"`), except the\n\t * transcript events `ada:message:sent`/`ada:message:received`, which reach\n\t * exact-key subscribers only and never `subscribeAll`.\n\t */\n\t$emit: (key: string, data?: unknown) => void;\n}\n\ninterface KeyedSubscription {\n\tkey: string;\n\tcallback: AdaSdkEventCallback;\n}\n\n// Mirrors EXACT_MATCH_EVENT_KEYS in client/client-host-runtime.ts: transcript\n// events reach exact-key subscribers only, never prefix or subscribeAll\n// subscribers. Duplicated (not imported) so the published testing entry stays\n// free of runtime modules; a drift test pins the mock against the real matcher.\nconst EXACT_MATCH_EVENT_KEYS = new Set([\n\t\"ada:message:sent\",\n\t\"ada:message:received\",\n]);\n\nfunction keyedSubscriptionMatches(\n\tsubscriptionKey: string,\n\teventKey: string,\n): boolean {\n\tif (EXACT_MATCH_EVENT_KEYS.has(eventKey)) {\n\t\treturn subscriptionKey === eventKey;\n\t}\n\t// Raw prefix match, same as the real SDK: \"ada:campaigns\" also catches\n\t// \"ada:campaigns:shown\" and underscore-suffixed keys.\n\treturn subscriptionKey === eventKey || eventKey.startsWith(subscriptionKey);\n}\n\nfunction toMetaFieldRecord(\n\tfields: Record<string, unknown>,\n): Record<string, MetaFieldValue> {\n\tconst record: Record<string, MetaFieldValue> = {};\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (\n\t\t\ttypeof value === \"string\" ||\n\t\t\ttypeof value === \"number\" ||\n\t\t\ttypeof value === \"boolean\" ||\n\t\t\tvalue === null\n\t\t) {\n\t\t\trecord[key] = value;\n\t\t}\n\t}\n\treturn record;\n}\n\n/**\n * Create an in-memory messaging-interface stand-in for unit-testing an\n * integration without loading the SDK from the CDN.\n *\n * Every method resolves, records itself in `calls`, and keeps just enough\n * state to behave like the real interface: `sendMessage` appends a\n * programmatic user message that `getMessages` returns, `open`/`close`/\n * `toggle` drive `isOpen` and `getInfo`, `setMetaFields` feeds\n * `getMetaFields` (sensitive fields are never returned, matching the real\n * SDK), and `$emit` fans out to `subscribeEvent`/`subscribeAll` callbacks.\n *\n * Pass `overrides` to replace any method (e.g. with `vi.fn()`); overridden\n * methods are still recorded in `calls`.\n */\nexport function createMockAdaMessaging(\n\toverrides: Partial<AdaMessagingInterface> = {},\n): MockAdaMessaging {\n\tconst calls: RecordedAdaMessagingCall[] = [];\n\tlet nextSubscriptionId = 1;\n\tlet nextMessageId = 1;\n\tlet chatOpen = false;\n\tlet conversationId: string | null = null;\n\tconst metaFields: Record<string, MetaFieldValue> = {};\n\tconst messages: PublicMessage[] = [];\n\tconst keyedSubscriptions = new Map<number, KeyedSubscription>();\n\tconst wildcardSubscriptions = new Map<\n\t\tnumber,\n\t\t(key: string, data: unknown) => void\n\t>();\n\n\tconst emit = (key: string, data?: unknown): void => {\n\t\tfor (const subscription of keyedSubscriptions.values()) {\n\t\t\tif (keyedSubscriptionMatches(subscription.key, key)) {\n\t\t\t\tsubscription.callback(data, { eventKey: key });\n\t\t\t}\n\t\t}\n\t\tif (EXACT_MATCH_EVENT_KEYS.has(key)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const callback of wildcardSubscriptions.values()) {\n\t\t\tcallback(key, data);\n\t\t}\n\t};\n\n\tconst clearTranscript = (): void => {\n\t\tmessages.length = 0;\n\t\tconversationId = null;\n\t};\n\n\tconst base: AdaMessagingInterface = {\n\t\tstart: async () => undefined,\n\t\tstop: async () => undefined,\n\t\ttoggle: async () => {\n\t\t\tchatOpen = !chatOpen;\n\t\t},\n\t\tdeleteHistory: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\treset: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\tsetMetaFields: async (fields) => {\n\t\t\tObject.assign(metaFields, toMetaFieldRecord(fields));\n\t\t},\n\t\t// Sensitive fields are accepted but never readable back, matching the\n\t\t// real SDK's getMetaFields contract.\n\t\tsetSensitiveMetaFields: async () => undefined,\n\t\tsetTestModeBanner: async () => undefined,\n\t\tgetInfo: async () => ({\n\t\t\tisChatOpen: chatOpen,\n\t\t\tisDrawerOpen: chatOpen,\n\t\t\thasActiveChatter: messages.length > 0,\n\t\t\thasClosedChat: false,\n\t\t}),\n\t\tgetMetaFields: async () => ({ ...metaFields }),\n\t\tisOpen: async () => chatOpen,\n\t\tgetConversation: async () => ({\n\t\t\tid: conversationId,\n\t\t\thandoff: { inLiveChat: false },\n\t\t}),\n\t\tgetMessages: async () => [...messages],\n\t\tsetComposerText: async () => undefined,\n\t\tsetDelegate: () => undefined,\n\t\ttrackEvent: async () => undefined,\n\t\tsubscribeEvent: async (key, callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\tkeyedSubscriptions.set(id, { key, callback });\n\t\t\treturn id;\n\t\t},\n\t\tunsubscribeEvent: async (id) => {\n\t\t\tkeyedSubscriptions.delete(id);\n\t\t\twildcardSubscriptions.delete(id);\n\t\t},\n\t\tsubscribeAll: async (callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\twildcardSubscriptions.set(id, callback);\n\t\t\treturn id;\n\t\t},\n\t\topen: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\tclose: async () => {\n\t\t\tchatOpen = false;\n\t\t},\n\t\tsetLanguage: async () => undefined,\n\t\tsetTheme: async () => undefined,\n\t\tsetDeviceToken: async () => undefined,\n\t\tsendMessage: async (body) => {\n\t\t\tconst id = `mock-message-${nextMessageId++}`;\n\t\t\tconversationId = conversationId ?? \"mock-conversation\";\n\t\t\tmessages.push({\n\t\t\t\tid,\n\t\t\t\trole: \"user\",\n\t\t\t\ttype: \"text\",\n\t\t\t\tbody,\n\t\t\t\tcreatedAt: Math.floor(Date.now() / 1000),\n\t\t\t\torigin: \"programmatic\",\n\t\t\t});\n\t\t\treturn { id };\n\t\t},\n\t\thandleNotification: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\trequestNotifications: async () => \"granted\",\n\t\tcreateProactive: async () => undefined,\n\t\ttriggerCampaign: async () => undefined,\n\t\tevaluateCampaignConditions: async () => undefined,\n\t\tcloseCampaign: async () => undefined,\n\t\ttriggerAnswer: async () => undefined,\n\t\ttriggerProactive: async () => undefined,\n\t\ttriggerGreeting: async () => undefined,\n\t\tdestroy: async () => {\n\t\t\tkeyedSubscriptions.clear();\n\t\t\twildcardSubscriptions.clear();\n\t\t\tchatOpen = false;\n\t\t},\n\t};\n\n\tconst definedOverrides = Object.fromEntries(\n\t\tObject.entries(overrides).filter(([, value]) => value !== undefined),\n\t);\n\tconst implementation = { ...base, ...definedOverrides };\n\tconst recorded = Object.fromEntries(\n\t\tObject.entries(implementation).map(([method, fn]) => [\n\t\t\tmethod,\n\t\t\t(...args: unknown[]) => {\n\t\t\t\tcalls.push({ method, args });\n\t\t\t\treturn (fn as (...callArgs: unknown[]) => unknown)(...args);\n\t\t\t},\n\t\t]),\n\t) as unknown as AdaMessagingInterface;\n\n\treturn { ...recorded, calls, $emit: emit };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"testing.cjs","names":[],"sources":["../../src/npm/testing.ts"],"sourcesContent":["import type {\n\tAdaSdkEventCallback,\n\tMetaFieldValue,\n\tPublicMessage,\n} from \"../client/client-types\";\nimport type { AdaMessagingInterface } from \"./loader\";\n\n/** One recorded method invocation on a {@link MockAdaMessaging}. */\nexport interface RecordedAdaMessagingCall {\n\tmethod: string;\n\targs: unknown[];\n}\n\n/**\n * The mock returned by {@link createMockAdaMessaging}: the full\n * {@link AdaMessagingInterface} plus test hooks for inspecting calls and\n * simulating SDK events.\n */\nexport interface MockAdaMessaging extends AdaMessagingInterface {\n\t/** Every method invocation, in call order. */\n\tcalls: RecordedAdaMessagingCall[];\n\t/**\n\t * Deliver an event to `subscribeEvent` and `subscribeAll` callbacks, as if\n\t * the SDK emitted it (e.g. `$emit(\"ada:ready\", { isRolledOut: true })`).\n\t * Routing matches the real SDK: keyed subscriptions match by prefix\n\t * (`\"ada:campaigns\"` catches `\"ada:campaigns:shown\"`), except the\n\t * transcript events `ada:message:sent`/`ada:message:received`, which reach\n\t * exact-key subscribers only and never `subscribeAll`.\n\t */\n\t$emit: (key: string, data?: unknown) => void;\n}\n\ninterface KeyedSubscription {\n\tkey: string;\n\tcallback: AdaSdkEventCallback;\n}\n\n// Mirrors EXACT_MATCH_EVENT_KEYS in client/client-host-runtime.ts: transcript\n// events reach exact-key subscribers only, never prefix or subscribeAll\n// subscribers. Duplicated (not imported) so the published testing entry stays\n// free of runtime modules; a drift test pins the mock against the real matcher.\nconst EXACT_MATCH_EVENT_KEYS = new Set([\n\t\"ada:message:sent\",\n\t\"ada:message:received\",\n]);\n\nfunction keyedSubscriptionMatches(\n\tsubscriptionKey: string,\n\teventKey: string,\n): boolean {\n\tif (EXACT_MATCH_EVENT_KEYS.has(eventKey)) {\n\t\treturn subscriptionKey === eventKey;\n\t}\n\t// Raw prefix match, same as the real SDK: \"ada:campaigns\" also catches\n\t// \"ada:campaigns:shown\" and underscore-suffixed keys.\n\treturn subscriptionKey === eventKey || eventKey.startsWith(subscriptionKey);\n}\n\nfunction toMetaFieldRecord(\n\tfields: Record<string, unknown>,\n): Record<string, MetaFieldValue> {\n\tconst record: Record<string, MetaFieldValue> = {};\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (\n\t\t\ttypeof value === \"string\" ||\n\t\t\ttypeof value === \"number\" ||\n\t\t\ttypeof value === \"boolean\" ||\n\t\t\tvalue === null\n\t\t) {\n\t\t\trecord[key] = value;\n\t\t}\n\t}\n\treturn record;\n}\n\n/**\n * Create an in-memory messaging-interface stand-in for unit-testing an\n * integration without loading the SDK from the CDN.\n *\n * Every method resolves, records itself in `calls`, and keeps just enough\n * state to behave like the real interface: `sendMessage` appends a\n * programmatic user message that `getMessages` returns, `open`/`close`/\n * `toggle` drive `isOpen` and `getInfo`, `setMetaFields` feeds\n * `getMetaFields` (sensitive fields are never returned, matching the real\n * SDK), and `$emit` fans out to `subscribeEvent`/`subscribeAll` callbacks.\n *\n * Pass `overrides` to replace any method (e.g. with `vi.fn()`); overridden\n * methods are still recorded in `calls`.\n */\nexport function createMockAdaMessaging(\n\toverrides: Partial<AdaMessagingInterface> = {},\n): MockAdaMessaging {\n\tconst calls: RecordedAdaMessagingCall[] = [];\n\tlet nextSubscriptionId = 1;\n\tlet nextMessageId = 1;\n\tlet chatOpen = false;\n\tlet conversationId: string | null = null;\n\tconst metaFields: Record<string, MetaFieldValue> = {};\n\tconst messages: PublicMessage[] = [];\n\tconst keyedSubscriptions = new Map<number, KeyedSubscription>();\n\tconst wildcardSubscriptions = new Map<\n\t\tnumber,\n\t\t(key: string, data: unknown) => void\n\t>();\n\n\tconst emit = (key: string, data?: unknown): void => {\n\t\tfor (const subscription of keyedSubscriptions.values()) {\n\t\t\tif (keyedSubscriptionMatches(subscription.key, key)) {\n\t\t\t\tsubscription.callback(data, { eventKey: key });\n\t\t\t}\n\t\t}\n\t\tif (EXACT_MATCH_EVENT_KEYS.has(key)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const callback of wildcardSubscriptions.values()) {\n\t\t\tcallback(key, data);\n\t\t}\n\t};\n\n\tconst clearTranscript = (): void => {\n\t\tmessages.length = 0;\n\t\tconversationId = null;\n\t};\n\n\tconst base: AdaMessagingInterface = {\n\t\tstart: async () => undefined,\n\t\tstop: async () => undefined,\n\t\ttoggle: async () => {\n\t\t\tchatOpen = !chatOpen;\n\t\t},\n\t\tdeleteHistory: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\treset: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\tsetMetaFields: async (fields) => {\n\t\t\tObject.assign(metaFields, toMetaFieldRecord(fields));\n\t\t},\n\t\t// Sensitive fields are accepted but never readable back, matching the\n\t\t// real SDK's getMetaFields contract.\n\t\tsetSensitiveMetaFields: async () => undefined,\n\t\tsetTestModeBanner: async () => undefined,\n\t\tgetInfo: async () => ({\n\t\t\tisChatOpen: chatOpen,\n\t\t\tisDrawerOpen: chatOpen,\n\t\t\thasActiveChatter: messages.length > 0,\n\t\t\thasClosedChat: false,\n\t\t}),\n\t\tgetMetaFields: async () => ({ ...metaFields }),\n\t\tisOpen: async () => chatOpen,\n\t\tgetConversation: async () => ({\n\t\t\tid: conversationId,\n\t\t\thandoff: { inLiveChat: false },\n\t\t}),\n\t\tgetMessages: async () => [...messages],\n\t\tsetComposerText: async () => undefined,\n\t\tsetDelegate: () => undefined,\n\t\ttrackEvent: async () => undefined,\n\t\tsubscribeEvent: async (key, callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\tkeyedSubscriptions.set(id, { key, callback });\n\t\t\treturn id;\n\t\t},\n\t\tunsubscribeEvent: async (id) => {\n\t\t\tkeyedSubscriptions.delete(id);\n\t\t\twildcardSubscriptions.delete(id);\n\t\t},\n\t\tsubscribeAll: async (callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\twildcardSubscriptions.set(id, callback);\n\t\t\treturn id;\n\t\t},\n\t\topen: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\tclose: async () => {\n\t\t\tchatOpen = false;\n\t\t},\n\t\tsetLanguage: async () => undefined,\n\t\tsetTheme: async () => undefined,\n\t\tsetDeviceToken: async () => undefined,\n\t\t// Optional on AdaEmbedInterface (internal mobile-webview plumbing), but\n\t\t// Ada's own maintained test double still implements the full surface — the\n\t\t// types-drift guard requires the shipped mock to cover every interface\n\t\t// member so a customer's double never lags it.\n\t\tacknowledgeSessionMirrorClear: async () => undefined,\n\t\tdeliverSessionMirrorSeed: async () => undefined,\n\t\tsendMessage: async (body) => {\n\t\t\tconst id = `mock-message-${nextMessageId++}`;\n\t\t\tconversationId = conversationId ?? \"mock-conversation\";\n\t\t\tmessages.push({\n\t\t\t\tid,\n\t\t\t\trole: \"user\",\n\t\t\t\ttype: \"text\",\n\t\t\t\tbody,\n\t\t\t\tcreatedAt: Math.floor(Date.now() / 1000),\n\t\t\t\torigin: \"programmatic\",\n\t\t\t});\n\t\t\treturn { id };\n\t\t},\n\t\thandleNotification: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\trequestNotifications: async () => \"granted\",\n\t\tcreateProactive: async () => undefined,\n\t\ttriggerCampaign: async () => undefined,\n\t\tevaluateCampaignConditions: async () => undefined,\n\t\tcloseCampaign: async () => undefined,\n\t\ttriggerAnswer: async () => undefined,\n\t\ttriggerProactive: async () => undefined,\n\t\ttriggerGreeting: async () => undefined,\n\t\tdestroy: async () => {\n\t\t\tkeyedSubscriptions.clear();\n\t\t\twildcardSubscriptions.clear();\n\t\t\tchatOpen = false;\n\t\t},\n\t};\n\n\tconst definedOverrides = Object.fromEntries(\n\t\tObject.entries(overrides).filter(([, value]) => value !== undefined),\n\t);\n\tconst implementation = { ...base, ...definedOverrides };\n\tconst recorded = Object.fromEntries(\n\t\tObject.entries(implementation).map(([method, fn]) => [\n\t\t\tmethod,\n\t\t\t(...args: unknown[]) => {\n\t\t\t\tcalls.push({ method, args });\n\t\t\t\treturn (fn as (...callArgs: unknown[]) => unknown)(...args);\n\t\t\t},\n\t\t]),\n\t) as unknown as AdaMessagingInterface;\n\n\treturn { ...recorded, calls, $emit: emit };\n}\n"],"mappings":"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"}
@@ -101,6 +101,8 @@ function createMockAdaMessaging(overrides = {}) {
101
101
  setLanguage: async () => void 0,
102
102
  setTheme: async () => void 0,
103
103
  setDeviceToken: async () => void 0,
104
+ acknowledgeSessionMirrorClear: async () => void 0,
105
+ deliverSessionMirrorSeed: async () => void 0,
104
106
  sendMessage: async (body) => {
105
107
  const id = `mock-message-${nextMessageId++}`;
106
108
  conversationId = conversationId ?? "mock-conversation";
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","names":[],"sources":["../../src/npm/testing.ts"],"sourcesContent":["import type {\n\tAdaSdkEventCallback,\n\tMetaFieldValue,\n\tPublicMessage,\n} from \"../client/client-types\";\nimport type { AdaMessagingInterface } from \"./loader\";\n\n/** One recorded method invocation on a {@link MockAdaMessaging}. */\nexport interface RecordedAdaMessagingCall {\n\tmethod: string;\n\targs: unknown[];\n}\n\n/**\n * The mock returned by {@link createMockAdaMessaging}: the full\n * {@link AdaMessagingInterface} plus test hooks for inspecting calls and\n * simulating SDK events.\n */\nexport interface MockAdaMessaging extends AdaMessagingInterface {\n\t/** Every method invocation, in call order. */\n\tcalls: RecordedAdaMessagingCall[];\n\t/**\n\t * Deliver an event to `subscribeEvent` and `subscribeAll` callbacks, as if\n\t * the SDK emitted it (e.g. `$emit(\"ada:ready\", { isRolledOut: true })`).\n\t * Routing matches the real SDK: keyed subscriptions match by prefix\n\t * (`\"ada:campaigns\"` catches `\"ada:campaigns:shown\"`), except the\n\t * transcript events `ada:message:sent`/`ada:message:received`, which reach\n\t * exact-key subscribers only and never `subscribeAll`.\n\t */\n\t$emit: (key: string, data?: unknown) => void;\n}\n\ninterface KeyedSubscription {\n\tkey: string;\n\tcallback: AdaSdkEventCallback;\n}\n\n// Mirrors EXACT_MATCH_EVENT_KEYS in client/client-host-runtime.ts: transcript\n// events reach exact-key subscribers only, never prefix or subscribeAll\n// subscribers. Duplicated (not imported) so the published testing entry stays\n// free of runtime modules; a drift test pins the mock against the real matcher.\nconst EXACT_MATCH_EVENT_KEYS = new Set([\n\t\"ada:message:sent\",\n\t\"ada:message:received\",\n]);\n\nfunction keyedSubscriptionMatches(\n\tsubscriptionKey: string,\n\teventKey: string,\n): boolean {\n\tif (EXACT_MATCH_EVENT_KEYS.has(eventKey)) {\n\t\treturn subscriptionKey === eventKey;\n\t}\n\t// Raw prefix match, same as the real SDK: \"ada:campaigns\" also catches\n\t// \"ada:campaigns:shown\" and underscore-suffixed keys.\n\treturn subscriptionKey === eventKey || eventKey.startsWith(subscriptionKey);\n}\n\nfunction toMetaFieldRecord(\n\tfields: Record<string, unknown>,\n): Record<string, MetaFieldValue> {\n\tconst record: Record<string, MetaFieldValue> = {};\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (\n\t\t\ttypeof value === \"string\" ||\n\t\t\ttypeof value === \"number\" ||\n\t\t\ttypeof value === \"boolean\" ||\n\t\t\tvalue === null\n\t\t) {\n\t\t\trecord[key] = value;\n\t\t}\n\t}\n\treturn record;\n}\n\n/**\n * Create an in-memory messaging-interface stand-in for unit-testing an\n * integration without loading the SDK from the CDN.\n *\n * Every method resolves, records itself in `calls`, and keeps just enough\n * state to behave like the real interface: `sendMessage` appends a\n * programmatic user message that `getMessages` returns, `open`/`close`/\n * `toggle` drive `isOpen` and `getInfo`, `setMetaFields` feeds\n * `getMetaFields` (sensitive fields are never returned, matching the real\n * SDK), and `$emit` fans out to `subscribeEvent`/`subscribeAll` callbacks.\n *\n * Pass `overrides` to replace any method (e.g. with `vi.fn()`); overridden\n * methods are still recorded in `calls`.\n */\nexport function createMockAdaMessaging(\n\toverrides: Partial<AdaMessagingInterface> = {},\n): MockAdaMessaging {\n\tconst calls: RecordedAdaMessagingCall[] = [];\n\tlet nextSubscriptionId = 1;\n\tlet nextMessageId = 1;\n\tlet chatOpen = false;\n\tlet conversationId: string | null = null;\n\tconst metaFields: Record<string, MetaFieldValue> = {};\n\tconst messages: PublicMessage[] = [];\n\tconst keyedSubscriptions = new Map<number, KeyedSubscription>();\n\tconst wildcardSubscriptions = new Map<\n\t\tnumber,\n\t\t(key: string, data: unknown) => void\n\t>();\n\n\tconst emit = (key: string, data?: unknown): void => {\n\t\tfor (const subscription of keyedSubscriptions.values()) {\n\t\t\tif (keyedSubscriptionMatches(subscription.key, key)) {\n\t\t\t\tsubscription.callback(data, { eventKey: key });\n\t\t\t}\n\t\t}\n\t\tif (EXACT_MATCH_EVENT_KEYS.has(key)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const callback of wildcardSubscriptions.values()) {\n\t\t\tcallback(key, data);\n\t\t}\n\t};\n\n\tconst clearTranscript = (): void => {\n\t\tmessages.length = 0;\n\t\tconversationId = null;\n\t};\n\n\tconst base: AdaMessagingInterface = {\n\t\tstart: async () => undefined,\n\t\tstop: async () => undefined,\n\t\ttoggle: async () => {\n\t\t\tchatOpen = !chatOpen;\n\t\t},\n\t\tdeleteHistory: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\treset: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\tsetMetaFields: async (fields) => {\n\t\t\tObject.assign(metaFields, toMetaFieldRecord(fields));\n\t\t},\n\t\t// Sensitive fields are accepted but never readable back, matching the\n\t\t// real SDK's getMetaFields contract.\n\t\tsetSensitiveMetaFields: async () => undefined,\n\t\tsetTestModeBanner: async () => undefined,\n\t\tgetInfo: async () => ({\n\t\t\tisChatOpen: chatOpen,\n\t\t\tisDrawerOpen: chatOpen,\n\t\t\thasActiveChatter: messages.length > 0,\n\t\t\thasClosedChat: false,\n\t\t}),\n\t\tgetMetaFields: async () => ({ ...metaFields }),\n\t\tisOpen: async () => chatOpen,\n\t\tgetConversation: async () => ({\n\t\t\tid: conversationId,\n\t\t\thandoff: { inLiveChat: false },\n\t\t}),\n\t\tgetMessages: async () => [...messages],\n\t\tsetComposerText: async () => undefined,\n\t\tsetDelegate: () => undefined,\n\t\ttrackEvent: async () => undefined,\n\t\tsubscribeEvent: async (key, callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\tkeyedSubscriptions.set(id, { key, callback });\n\t\t\treturn id;\n\t\t},\n\t\tunsubscribeEvent: async (id) => {\n\t\t\tkeyedSubscriptions.delete(id);\n\t\t\twildcardSubscriptions.delete(id);\n\t\t},\n\t\tsubscribeAll: async (callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\twildcardSubscriptions.set(id, callback);\n\t\t\treturn id;\n\t\t},\n\t\topen: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\tclose: async () => {\n\t\t\tchatOpen = false;\n\t\t},\n\t\tsetLanguage: async () => undefined,\n\t\tsetTheme: async () => undefined,\n\t\tsetDeviceToken: async () => undefined,\n\t\tsendMessage: async (body) => {\n\t\t\tconst id = `mock-message-${nextMessageId++}`;\n\t\t\tconversationId = conversationId ?? \"mock-conversation\";\n\t\t\tmessages.push({\n\t\t\t\tid,\n\t\t\t\trole: \"user\",\n\t\t\t\ttype: \"text\",\n\t\t\t\tbody,\n\t\t\t\tcreatedAt: Math.floor(Date.now() / 1000),\n\t\t\t\torigin: \"programmatic\",\n\t\t\t});\n\t\t\treturn { id };\n\t\t},\n\t\thandleNotification: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\trequestNotifications: async () => \"granted\",\n\t\tcreateProactive: async () => undefined,\n\t\ttriggerCampaign: async () => undefined,\n\t\tevaluateCampaignConditions: async () => undefined,\n\t\tcloseCampaign: async () => undefined,\n\t\ttriggerAnswer: async () => undefined,\n\t\ttriggerProactive: async () => undefined,\n\t\ttriggerGreeting: async () => undefined,\n\t\tdestroy: async () => {\n\t\t\tkeyedSubscriptions.clear();\n\t\t\twildcardSubscriptions.clear();\n\t\t\tchatOpen = false;\n\t\t},\n\t};\n\n\tconst definedOverrides = Object.fromEntries(\n\t\tObject.entries(overrides).filter(([, value]) => value !== undefined),\n\t);\n\tconst implementation = { ...base, ...definedOverrides };\n\tconst recorded = Object.fromEntries(\n\t\tObject.entries(implementation).map(([method, fn]) => [\n\t\t\tmethod,\n\t\t\t(...args: unknown[]) => {\n\t\t\t\tcalls.push({ method, args });\n\t\t\t\treturn (fn as (...callArgs: unknown[]) => unknown)(...args);\n\t\t\t},\n\t\t]),\n\t) as unknown as AdaMessagingInterface;\n\n\treturn { ...recorded, calls, $emit: emit };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"testing.js","names":[],"sources":["../../src/npm/testing.ts"],"sourcesContent":["import type {\n\tAdaSdkEventCallback,\n\tMetaFieldValue,\n\tPublicMessage,\n} from \"../client/client-types\";\nimport type { AdaMessagingInterface } from \"./loader\";\n\n/** One recorded method invocation on a {@link MockAdaMessaging}. */\nexport interface RecordedAdaMessagingCall {\n\tmethod: string;\n\targs: unknown[];\n}\n\n/**\n * The mock returned by {@link createMockAdaMessaging}: the full\n * {@link AdaMessagingInterface} plus test hooks for inspecting calls and\n * simulating SDK events.\n */\nexport interface MockAdaMessaging extends AdaMessagingInterface {\n\t/** Every method invocation, in call order. */\n\tcalls: RecordedAdaMessagingCall[];\n\t/**\n\t * Deliver an event to `subscribeEvent` and `subscribeAll` callbacks, as if\n\t * the SDK emitted it (e.g. `$emit(\"ada:ready\", { isRolledOut: true })`).\n\t * Routing matches the real SDK: keyed subscriptions match by prefix\n\t * (`\"ada:campaigns\"` catches `\"ada:campaigns:shown\"`), except the\n\t * transcript events `ada:message:sent`/`ada:message:received`, which reach\n\t * exact-key subscribers only and never `subscribeAll`.\n\t */\n\t$emit: (key: string, data?: unknown) => void;\n}\n\ninterface KeyedSubscription {\n\tkey: string;\n\tcallback: AdaSdkEventCallback;\n}\n\n// Mirrors EXACT_MATCH_EVENT_KEYS in client/client-host-runtime.ts: transcript\n// events reach exact-key subscribers only, never prefix or subscribeAll\n// subscribers. Duplicated (not imported) so the published testing entry stays\n// free of runtime modules; a drift test pins the mock against the real matcher.\nconst EXACT_MATCH_EVENT_KEYS = new Set([\n\t\"ada:message:sent\",\n\t\"ada:message:received\",\n]);\n\nfunction keyedSubscriptionMatches(\n\tsubscriptionKey: string,\n\teventKey: string,\n): boolean {\n\tif (EXACT_MATCH_EVENT_KEYS.has(eventKey)) {\n\t\treturn subscriptionKey === eventKey;\n\t}\n\t// Raw prefix match, same as the real SDK: \"ada:campaigns\" also catches\n\t// \"ada:campaigns:shown\" and underscore-suffixed keys.\n\treturn subscriptionKey === eventKey || eventKey.startsWith(subscriptionKey);\n}\n\nfunction toMetaFieldRecord(\n\tfields: Record<string, unknown>,\n): Record<string, MetaFieldValue> {\n\tconst record: Record<string, MetaFieldValue> = {};\n\tfor (const [key, value] of Object.entries(fields)) {\n\t\tif (\n\t\t\ttypeof value === \"string\" ||\n\t\t\ttypeof value === \"number\" ||\n\t\t\ttypeof value === \"boolean\" ||\n\t\t\tvalue === null\n\t\t) {\n\t\t\trecord[key] = value;\n\t\t}\n\t}\n\treturn record;\n}\n\n/**\n * Create an in-memory messaging-interface stand-in for unit-testing an\n * integration without loading the SDK from the CDN.\n *\n * Every method resolves, records itself in `calls`, and keeps just enough\n * state to behave like the real interface: `sendMessage` appends a\n * programmatic user message that `getMessages` returns, `open`/`close`/\n * `toggle` drive `isOpen` and `getInfo`, `setMetaFields` feeds\n * `getMetaFields` (sensitive fields are never returned, matching the real\n * SDK), and `$emit` fans out to `subscribeEvent`/`subscribeAll` callbacks.\n *\n * Pass `overrides` to replace any method (e.g. with `vi.fn()`); overridden\n * methods are still recorded in `calls`.\n */\nexport function createMockAdaMessaging(\n\toverrides: Partial<AdaMessagingInterface> = {},\n): MockAdaMessaging {\n\tconst calls: RecordedAdaMessagingCall[] = [];\n\tlet nextSubscriptionId = 1;\n\tlet nextMessageId = 1;\n\tlet chatOpen = false;\n\tlet conversationId: string | null = null;\n\tconst metaFields: Record<string, MetaFieldValue> = {};\n\tconst messages: PublicMessage[] = [];\n\tconst keyedSubscriptions = new Map<number, KeyedSubscription>();\n\tconst wildcardSubscriptions = new Map<\n\t\tnumber,\n\t\t(key: string, data: unknown) => void\n\t>();\n\n\tconst emit = (key: string, data?: unknown): void => {\n\t\tfor (const subscription of keyedSubscriptions.values()) {\n\t\t\tif (keyedSubscriptionMatches(subscription.key, key)) {\n\t\t\t\tsubscription.callback(data, { eventKey: key });\n\t\t\t}\n\t\t}\n\t\tif (EXACT_MATCH_EVENT_KEYS.has(key)) {\n\t\t\treturn;\n\t\t}\n\t\tfor (const callback of wildcardSubscriptions.values()) {\n\t\t\tcallback(key, data);\n\t\t}\n\t};\n\n\tconst clearTranscript = (): void => {\n\t\tmessages.length = 0;\n\t\tconversationId = null;\n\t};\n\n\tconst base: AdaMessagingInterface = {\n\t\tstart: async () => undefined,\n\t\tstop: async () => undefined,\n\t\ttoggle: async () => {\n\t\t\tchatOpen = !chatOpen;\n\t\t},\n\t\tdeleteHistory: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\treset: async () => {\n\t\t\tclearTranscript();\n\t\t},\n\t\tsetMetaFields: async (fields) => {\n\t\t\tObject.assign(metaFields, toMetaFieldRecord(fields));\n\t\t},\n\t\t// Sensitive fields are accepted but never readable back, matching the\n\t\t// real SDK's getMetaFields contract.\n\t\tsetSensitiveMetaFields: async () => undefined,\n\t\tsetTestModeBanner: async () => undefined,\n\t\tgetInfo: async () => ({\n\t\t\tisChatOpen: chatOpen,\n\t\t\tisDrawerOpen: chatOpen,\n\t\t\thasActiveChatter: messages.length > 0,\n\t\t\thasClosedChat: false,\n\t\t}),\n\t\tgetMetaFields: async () => ({ ...metaFields }),\n\t\tisOpen: async () => chatOpen,\n\t\tgetConversation: async () => ({\n\t\t\tid: conversationId,\n\t\t\thandoff: { inLiveChat: false },\n\t\t}),\n\t\tgetMessages: async () => [...messages],\n\t\tsetComposerText: async () => undefined,\n\t\tsetDelegate: () => undefined,\n\t\ttrackEvent: async () => undefined,\n\t\tsubscribeEvent: async (key, callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\tkeyedSubscriptions.set(id, { key, callback });\n\t\t\treturn id;\n\t\t},\n\t\tunsubscribeEvent: async (id) => {\n\t\t\tkeyedSubscriptions.delete(id);\n\t\t\twildcardSubscriptions.delete(id);\n\t\t},\n\t\tsubscribeAll: async (callback) => {\n\t\t\tconst id = nextSubscriptionId++;\n\t\t\twildcardSubscriptions.set(id, callback);\n\t\t\treturn id;\n\t\t},\n\t\topen: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\tclose: async () => {\n\t\t\tchatOpen = false;\n\t\t},\n\t\tsetLanguage: async () => undefined,\n\t\tsetTheme: async () => undefined,\n\t\tsetDeviceToken: async () => undefined,\n\t\t// Optional on AdaEmbedInterface (internal mobile-webview plumbing), but\n\t\t// Ada's own maintained test double still implements the full surface — the\n\t\t// types-drift guard requires the shipped mock to cover every interface\n\t\t// member so a customer's double never lags it.\n\t\tacknowledgeSessionMirrorClear: async () => undefined,\n\t\tdeliverSessionMirrorSeed: async () => undefined,\n\t\tsendMessage: async (body) => {\n\t\t\tconst id = `mock-message-${nextMessageId++}`;\n\t\t\tconversationId = conversationId ?? \"mock-conversation\";\n\t\t\tmessages.push({\n\t\t\t\tid,\n\t\t\t\trole: \"user\",\n\t\t\t\ttype: \"text\",\n\t\t\t\tbody,\n\t\t\t\tcreatedAt: Math.floor(Date.now() / 1000),\n\t\t\t\torigin: \"programmatic\",\n\t\t\t});\n\t\t\treturn { id };\n\t\t},\n\t\thandleNotification: async () => {\n\t\t\tchatOpen = true;\n\t\t},\n\t\trequestNotifications: async () => \"granted\",\n\t\tcreateProactive: async () => undefined,\n\t\ttriggerCampaign: async () => undefined,\n\t\tevaluateCampaignConditions: async () => undefined,\n\t\tcloseCampaign: async () => undefined,\n\t\ttriggerAnswer: async () => undefined,\n\t\ttriggerProactive: async () => undefined,\n\t\ttriggerGreeting: async () => undefined,\n\t\tdestroy: async () => {\n\t\t\tkeyedSubscriptions.clear();\n\t\t\twildcardSubscriptions.clear();\n\t\t\tchatOpen = false;\n\t\t},\n\t};\n\n\tconst definedOverrides = Object.fromEntries(\n\t\tObject.entries(overrides).filter(([, value]) => value !== undefined),\n\t);\n\tconst implementation = { ...base, ...definedOverrides };\n\tconst recorded = Object.fromEntries(\n\t\tObject.entries(implementation).map(([method, fn]) => [\n\t\t\tmethod,\n\t\t\t(...args: unknown[]) => {\n\t\t\t\tcalls.push({ method, args });\n\t\t\t\treturn (fn as (...callArgs: unknown[]) => unknown)(...args);\n\t\t\t},\n\t\t]),\n\t) as unknown as AdaMessagingInterface;\n\n\treturn { ...recorded, calls, $emit: emit };\n}\n"],"mappings":"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"}
@@ -3,3 +3,43 @@
3
3
  * Both sdk and core reference this set for cluster validation.
4
4
  */
5
5
  export declare const VALID_ADA_CLUSTERS: Set<string>;
6
+ /**
7
+ * Cluster domains whose bots serve their web assets from the global preprod
8
+ * CDN (`messaging-assets.ada-dev2.support`) rather than the production one.
9
+ *
10
+ * Every TypeScript consumer imports this set rather than restating it. The
11
+ * native wrapper and the web runtime it loads must reach the SAME verdict for
12
+ * a given cluster: `sdk.js` derives its base from `import.meta.url`, so a
13
+ * production `webview.html` hosting a preprod `sdk.js` (or the reverse) mounts
14
+ * `core.html` cross-origin to its parent, core's `isNativeWebViewMount` check
15
+ * fails closed, and the session mirror silently never arms. Android's Kotlin
16
+ * copy cannot import this file; it is pinned against this set by
17
+ * `packages/sdk/src/preprod-cluster-contract.test.ts`.
18
+ */
19
+ export declare const PREPROD_ADA_CLUSTER_DOMAINS: Set<string>;
20
+ /**
21
+ * Reduces a host-supplied cluster value to the canonical bot domain used by
22
+ * {@link VALID_ADA_CLUSTERS}, {@link PREPROD_ADA_CLUSTER_DOMAINS} and the core
23
+ * rollout/API host builders (e.g. `my-bot.maple.ada.support`).
24
+ *
25
+ * Accepts the shapes hosts actually pass: a scheme prefix, trailing slashes,
26
+ * mixed case, and the bare shorthand `maple` (expanded to `maple.ada.support`).
27
+ * `localhost` passes through. Returns undefined for anything that is not a
28
+ * valid domain-shaped identifier, so callers fail back to their safe default
29
+ * rather than propagating an unusable value.
30
+ *
31
+ * This is the single implementation: two normalizers that merely agree by
32
+ * inspection are how a cluster reaches opposite verdicts on the two sides of
33
+ * the same mount.
34
+ */
35
+ export declare function normalizeAdaClusterDomain(
36
+ cluster: string | undefined,
37
+ ): string | undefined;
38
+ /**
39
+ * Whether a host-supplied cluster value names a preprod cluster, normalized
40
+ * first. Prefer this over a bare set lookup so a caller cannot reach the
41
+ * opposite verdict by skipping normalization.
42
+ */
43
+ export declare function isPreprodAdaCluster(
44
+ cluster: string | undefined,
45
+ ): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ada-cx/messaging-sdk",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
4
4
  "description": "TypeScript types, test mocks, and a thin CDN loader for the Ada Messaging SDK. The runtime always resolves from Ada's CDN and is never bundled into this package.",
5
5
  "homepage": "https://docs.ada.cx",
6
6
  "keywords": [