@ada-cx/messaging-bridge 1.0.0-setup.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.
- package/README.md +328 -0
- package/dist/bridge-client.d.ts +69 -0
- package/dist/bridge-react.d.ts +63 -0
- package/dist/build-info.d.ts +17 -0
- package/dist/derive.d.ts +115 -0
- package/dist/loader-CVynp73o.js +160 -0
- package/dist/loader-CVynp73o.js.map +1 -0
- package/dist/loader.d.ts +125 -0
- package/dist/npm-index.d.ts +17 -0
- package/dist/npm-index.js +28 -0
- package/dist/npm-index.js.map +1 -0
- package/dist/npm-react.d.ts +13 -0
- package/dist/npm-react.js +113 -0
- package/dist/npm-react.js.map +1 -0
- package/dist/operations.d.ts +323 -0
- package/dist/shared-utils/csat-settings.d.ts +61 -0
- package/dist/shared-utils/file-upload.d.ts +36 -0
- package/dist/shared-utils/index.d.ts +2 -0
- package/dist/state-keys.d.ts +105 -0
- package/dist/testing.d.ts +53 -0
- package/dist/testing.js +471 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +849 -0
- package/package.json +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
# @ada-cx/messaging-bridge
|
|
2
|
+
|
|
3
|
+
Build your own chat UI (a "custom app") on Ada Messaging's bridge contract.
|
|
4
|
+
|
|
5
|
+
This package carries TypeScript types, test mocks, and a thin loader. The
|
|
6
|
+
bridge runtime always loads from Ada's CDN at
|
|
7
|
+
`https://messaging-assets.ada.support/bridge.js`. Ada patches the runtime
|
|
8
|
+
continuously, so your installed package version never pins security logic.
|
|
9
|
+
|
|
10
|
+
## Requirements
|
|
11
|
+
|
|
12
|
+
- Your custom app runs inside an iframe that Ada's core frame mounts. The
|
|
13
|
+
core frame is served from Ada's asset host, and it sits inside the page
|
|
14
|
+
that embeds the widget. Both origins are in your app's ancestor chain.
|
|
15
|
+
- You point the Web SDK at your app with its `appUrl` setting. Custom apps
|
|
16
|
+
are experimental. Contact your Ada team before you build on this feature.
|
|
17
|
+
- If your app's responses send no `X-Frame-Options` header and no CSP
|
|
18
|
+
`frame-ancestors` directive, browsers permit framing, and no server change
|
|
19
|
+
is needed. If your app restricts framing, `frame-ancestors` must allow
|
|
20
|
+
Ada's asset hosts and every site origin that embeds the widget:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
Content-Security-Policy: frame-ancestors https://messaging-assets.ada.support https://static.ada.support https://your-site.com
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Replace `https://your-site.com` with the origins of the pages that embed
|
|
27
|
+
the Ada widget. Browsers check `frame-ancestors` against every ancestor
|
|
28
|
+
frame, so a policy that lists only Ada's hosts blocks your app.
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
Install the package:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @ada-cx/messaging-bridge
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Load the runtime, complete the handshake, then render from state:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { loadMessagingBridge } from "@ada-cx/messaging-bridge";
|
|
42
|
+
|
|
43
|
+
const bridge = await loadMessagingBridge();
|
|
44
|
+
const client = bridge.createBridgeClient();
|
|
45
|
+
|
|
46
|
+
// REQUIRED: send app.initialize within 15 seconds of your frame loading.
|
|
47
|
+
// If the handshake does not arrive, core unmounts your frame.
|
|
48
|
+
client.sendEvent("app.initialize");
|
|
49
|
+
|
|
50
|
+
// Subscribe to display-state updates from core.
|
|
51
|
+
const unsubscribe = client.subscribe(() => {
|
|
52
|
+
const state = client.getState();
|
|
53
|
+
renderMessages(state?.["chat.messages"] ?? []);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Send a user message through the operations layer.
|
|
57
|
+
const handle = client.operations.sendMessage("Hello");
|
|
58
|
+
const message = await handle.settled();
|
|
59
|
+
|
|
60
|
+
// Tear down when your app unmounts.
|
|
61
|
+
unsubscribe();
|
|
62
|
+
client.destroy();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`loadMessagingBridge()` memoizes the load. Repeated calls return the same
|
|
66
|
+
promise. A failed load is forgotten, so a later call retries.
|
|
67
|
+
|
|
68
|
+
Use typed state keys through the loaded module:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const botName = client.getState()?.[bridge.STATE.CONFIG_BOT_NAME];
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Operations
|
|
75
|
+
|
|
76
|
+
`client.operations` provides typed helpers over the raw event contract. Each
|
|
77
|
+
helper carries the guards, debounces, and correlation logic that Ada's own
|
|
78
|
+
app uses. Prefer them over hand-built `sendEvent` calls.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// Correlated send: the handle resolves with the message row.
|
|
82
|
+
const handle = client.operations.sendMessage("Hello");
|
|
83
|
+
const message = await handle.settled({ timeoutMs: 10_000 });
|
|
84
|
+
|
|
85
|
+
// Read tracking: debounced, monotonic, reset per conversation.
|
|
86
|
+
client.operations.markRead(message.cursor ?? "");
|
|
87
|
+
|
|
88
|
+
// Correlated survey submit.
|
|
89
|
+
const result = await client.operations
|
|
90
|
+
.submitCsat({ score: 5 }, { surveyType: "bot", conversationId })
|
|
91
|
+
.settled();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Highlights:
|
|
95
|
+
|
|
96
|
+
- `sendMessage(body, { secret? })` returns a handle. `settled()` resolves
|
|
97
|
+
with the message once it appears in `chat.messages`, and rejects when core
|
|
98
|
+
reports a send error first (rate limit, over-length body, session not
|
|
99
|
+
ready, dispatch failure) — detected on the `chat.error.seq` advance, so a
|
|
100
|
+
repeat of the identical error text still rejects. A secret send writes
|
|
101
|
+
no transcript row, so its handle never resolves. Pass `timeoutMs` or skip
|
|
102
|
+
`settled()` for secret sends.
|
|
103
|
+
- `submitCapture(value).settled()` reports the server verdict for the
|
|
104
|
+
active capture field.
|
|
105
|
+
- `checkEndChatEligibility()` resolves with the End Chat survey decision.
|
|
106
|
+
- `markRead(cursor)` debounces 400 ms and keeps a monotonic watermark. Do
|
|
107
|
+
not reimplement read tracking.
|
|
108
|
+
- `startNewConversation()` applies a 5 second cooldown and returns `false`
|
|
109
|
+
while cooling down.
|
|
110
|
+
- `startFileUpload(file)` retains the `{ file, uploadId }` pair so
|
|
111
|
+
`retry()` replays exactly it.
|
|
112
|
+
- `notifyComposerChanged()` is payload-free and suppressed in secret mode.
|
|
113
|
+
Composer text never crosses the frame boundary.
|
|
114
|
+
- Chrome and settings helpers: `close`, `minimize`, `dismissError`,
|
|
115
|
+
`setLanguage`, `setTheme`, `emailTranscript`, and more. See the
|
|
116
|
+
`BridgeOperations` type for the full surface.
|
|
117
|
+
|
|
118
|
+
Use `outage.connectivityLost` as the connectivity signal. Do not substitute
|
|
119
|
+
`navigator.onLine`. It reports false positives on VPN and virtual-adapter
|
|
120
|
+
transitions.
|
|
121
|
+
|
|
122
|
+
## Observation
|
|
123
|
+
|
|
124
|
+
`client.subscribeKey(key, callback)` fires only when one key's value
|
|
125
|
+
changes. `client.select(selector, callback)` does the same for a derived
|
|
126
|
+
projection. Both default to `Object.is` equality and accept a custom
|
|
127
|
+
`equals`. Core reuses references for unchanged keys, so identity equality
|
|
128
|
+
is sound for every key except `chat.messages`.
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
client.subscribeKey("chat.isGenerating", (generating) => {
|
|
132
|
+
toggleTypingIndicator(generating === true);
|
|
133
|
+
});
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Derivation helpers
|
|
137
|
+
|
|
138
|
+
The loaded module exports pure helpers mined from Ada's reference app:
|
|
139
|
+
`messageKey`, `isHistoricalRow`, `findFirstUnread`, `selectUnread`,
|
|
140
|
+
`groupMessages`, `filterDisplayable`, `resolveBotName`, `isAgentTyping`,
|
|
141
|
+
and `isConnectivityLost`.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const { messageKey, filterDisplayable, groupMessages } = bridge;
|
|
145
|
+
const rows = filterDisplayable(messages, state?.["chat.isConversationActive"] ?? false);
|
|
146
|
+
const grouping = groupMessages(rows);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Use `messageKey` as your render key. Raw message ids change twice: a
|
|
150
|
+
streaming bubble swaps to its final id, and an optimistic send swaps to its
|
|
151
|
+
durable id. Keying on the raw id replays animations and breaks unread
|
|
152
|
+
anchors.
|
|
153
|
+
|
|
154
|
+
### Options
|
|
155
|
+
|
|
156
|
+
| Option | Default | Purpose |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| `cdnBase` | `https://messaging-assets.ada.support` | Asset origin for `bridge.js`. Must be an `https` URL. Plain `http` works only for loopback hosts such as `localhost` during local development. Override only for staging validation. |
|
|
159
|
+
| `pinBuildSha` | none | Full 40-character git SHA of a deployed CDN build. Loads that build's immutable copy instead of the current root asset. Not recommended for production. See [Pin the CDN build](#pin-the-cdn-build). |
|
|
160
|
+
|
|
161
|
+
Errors: `loadMessagingBridge` rejects with a `MessagingBridgeLoadError`. Its
|
|
162
|
+
`code` property identifies the failure:
|
|
163
|
+
|
|
164
|
+
| Code | Meaning |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `invalid_cdn_base` | The `cdnBase` option is not a valid `https` URL. |
|
|
167
|
+
| `invalid_build_sha` | The `pinBuildSha` option is not a full 40-character hex git SHA. |
|
|
168
|
+
| `unstamped_build_sha` | The `pinBuildSha` option is the unstamped placeholder from a repository build. |
|
|
169
|
+
| `bridge_import_failed` | The asset failed to load (network, CSP, 404). |
|
|
170
|
+
| `bridge_module_invalid` | The loaded module is not an Ada bridge build. |
|
|
171
|
+
|
|
172
|
+
### Pin the CDN build
|
|
173
|
+
|
|
174
|
+
The package exports `CDN_BUILD_SHA`. The value is the git commit SHA of the
|
|
175
|
+
monorepo commit this npm version was published from. Ada deploys each
|
|
176
|
+
commit's bridge runtime as an immutable SHA-rooted copy on the CDN. The
|
|
177
|
+
value therefore names the CDN build associated with this npm version.
|
|
178
|
+
|
|
179
|
+
Pass the SHA as the `pinBuildSha` loader option. The loader then skips the
|
|
180
|
+
root `bridge.js` asset and imports that build's immutable copy,
|
|
181
|
+
`<cdnBase>/<sha>/bridge/bridge.js`, directly:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import {
|
|
185
|
+
CDN_BUILD_SHA,
|
|
186
|
+
isCdnBuildShaStamped,
|
|
187
|
+
loadMessagingBridge,
|
|
188
|
+
} from "@ada-cx/messaging-bridge";
|
|
189
|
+
|
|
190
|
+
if (isCdnBuildShaStamped()) {
|
|
191
|
+
await loadMessagingBridge({ pinBuildSha: CDN_BUILD_SHA });
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Pinning is not recommended for production.** A pinned runtime misses Ada's
|
|
196
|
+
fixes and the loader-fence rollout. A pinned runtime can also predate later
|
|
197
|
+
core or server contract changes and stop working. Use a pin only to debug an
|
|
198
|
+
issue, to validate a staged build, or to reproduce a report against a known
|
|
199
|
+
runtime.
|
|
200
|
+
|
|
201
|
+
The option accepts any full 40-character hex git SHA of a deployed main
|
|
202
|
+
build. The loader rejects any other value with the `invalid_build_sha` error
|
|
203
|
+
code. Only published packages carry a real SHA. In the repository, and in a
|
|
204
|
+
locally built copy, `CDN_BUILD_SHA` is a 40-zero placeholder and
|
|
205
|
+
`isCdnBuildShaStamped()` returns `false`. The loader rejects the placeholder
|
|
206
|
+
with the `unstamped_build_sha` error code.
|
|
207
|
+
|
|
208
|
+
The npm publish and the CDN deploy of the same commit run in parallel. In
|
|
209
|
+
the first minutes after a release, a pin can fail with
|
|
210
|
+
`bridge_import_failed` until the deploy completes. If the deploy of that
|
|
211
|
+
commit failed, the pinned build never exists. The unpinned default is
|
|
212
|
+
unaffected in both cases.
|
|
213
|
+
|
|
214
|
+
`pinBuildSha` composes with `cdnBase`. The pinned copy resolves under the
|
|
215
|
+
asset host you pass.
|
|
216
|
+
|
|
217
|
+
## React
|
|
218
|
+
|
|
219
|
+
The `./react` entry provides a provider and hooks. They delegate to a
|
|
220
|
+
`BridgeClient` instance. They contain no runtime logic of their own.
|
|
221
|
+
|
|
222
|
+
The simplest path handles loading, the `app.initialize` handshake, and
|
|
223
|
+
teardown for you:
|
|
224
|
+
|
|
225
|
+
```tsx
|
|
226
|
+
import { createBridgeProvider, useBridgeState } from "@ada-cx/messaging-bridge/react";
|
|
227
|
+
|
|
228
|
+
const AdaBridgeProvider = createBridgeProvider();
|
|
229
|
+
|
|
230
|
+
function Root() {
|
|
231
|
+
return (
|
|
232
|
+
<AdaBridgeProvider fallback={<Spinner />}>
|
|
233
|
+
<MyChatUi />
|
|
234
|
+
</AdaBridgeProvider>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function MyChatUi() {
|
|
239
|
+
const state = useBridgeState();
|
|
240
|
+
return <MessageList messages={state?.["chat.messages"] ?? []} />;
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The provider accepts two more props for load failures. `errorFallback`
|
|
245
|
+
renders when the runtime fails to load. `onError` receives the
|
|
246
|
+
`MessagingBridgeLoadError`. Without them, the provider logs the error and
|
|
247
|
+
keeps rendering `fallback`.
|
|
248
|
+
|
|
249
|
+
To own the lifecycle yourself, pass a client you created:
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
import { BridgeProvider } from "@ada-cx/messaging-bridge/react";
|
|
253
|
+
|
|
254
|
+
<BridgeProvider client={client}>
|
|
255
|
+
<MyChatUi />
|
|
256
|
+
</BridgeProvider>
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
With an injected client, you send `app.initialize` and call `destroy()`
|
|
260
|
+
yourself.
|
|
261
|
+
|
|
262
|
+
Hooks: `useBridgeClient()`, `useBridgeState()`, and `useBridgeStateKey(key)`.
|
|
263
|
+
`useBridgeStateKey` re-renders only when its key's value changes.
|
|
264
|
+
`useBridgeState` re-renders on every state update. Reach the operations
|
|
265
|
+
layer through `useBridgeClient().operations`.
|
|
266
|
+
|
|
267
|
+
## Testing
|
|
268
|
+
|
|
269
|
+
The `./testing` entry provides a scriptable in-memory client for unit tests.
|
|
270
|
+
It performs no postMessage and no network access.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
import { createMockBridgeClient } from "@ada-cx/messaging-bridge/testing";
|
|
274
|
+
|
|
275
|
+
const mock = createMockBridgeClient();
|
|
276
|
+
render(<BridgeProvider client={mock}><MyChatUi /></BridgeProvider>);
|
|
277
|
+
|
|
278
|
+
mock.updateState({ "chat.isSending": true });
|
|
279
|
+
expect(mock.sentEvents).toContainEqual({
|
|
280
|
+
event: "chat.message.send",
|
|
281
|
+
payload: expect.objectContaining({ body: "Hello" }),
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Mock helpers: `setState`, `updateState`, `sentEvents`, `clearSentEvents`,
|
|
286
|
+
`subscriberCount`, and `destroyed`.
|
|
287
|
+
|
|
288
|
+
The mock implements the full client surface, `operations` included. Every
|
|
289
|
+
operation records its events in `sentEvents`. A correlated handle resolves
|
|
290
|
+
when your test scripts the answering state change:
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const handle = mock.operations.sendMessage("Hello");
|
|
294
|
+
mock.updateState({
|
|
295
|
+
"chat.messages": [
|
|
296
|
+
{ type: "text", id: "m1", sender: "user", timestamp: 1,
|
|
297
|
+
body: "Hello", clientKey: handle.tempMessageUuid },
|
|
298
|
+
],
|
|
299
|
+
});
|
|
300
|
+
await handle.settled();
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Contract notes
|
|
304
|
+
|
|
305
|
+
- `AppDisplayState` is the full state shape core publishes. `AppEvents` maps
|
|
306
|
+
each sendable event to its payload type.
|
|
307
|
+
- `app.initialize` is mandatory. Send it within 15 seconds of frame load.
|
|
308
|
+
- The runtime derives the core origin from `document.referrer`, or locks it
|
|
309
|
+
on the first valid state message. It rejects state from other origins.
|
|
310
|
+
- In Ada's custom-app frame, the first `app.initialize` hands core a
|
|
311
|
+
`MessagePort`. All later state and events ride that port. The port is
|
|
312
|
+
pinned to your first document, so your app must not navigate or reload its
|
|
313
|
+
own frame. A navigation disconnects the bridge permanently.
|
|
314
|
+
- The runtime detects the custom-app frame by the frame name core sets
|
|
315
|
+
(`ada-custom-app`), or by an opaque origin under older core builds. Do not
|
|
316
|
+
change `window.name` inside your app document.
|
|
317
|
+
- The custom-app frame keeps your real origin, so your own cookies and
|
|
318
|
+
storage work inside it. Browsers partition third-party storage by the
|
|
319
|
+
embedding site. Cookies need `Partitioned; Secure; SameSite=None`.
|
|
320
|
+
- The custom-app frame needs `document.referrer` to target the handshake.
|
|
321
|
+
Core provides the referrer by default. If a browser extension or policy
|
|
322
|
+
strips the Referer header, the handshake cannot send. The runtime logs an
|
|
323
|
+
error, and core falls back to Ada's default app after 15 seconds.
|
|
324
|
+
|
|
325
|
+
## Support
|
|
326
|
+
|
|
327
|
+
This package supports Ada's custom app program. Open issues through your Ada
|
|
328
|
+
support contact.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { BridgeOperations, ObserveOptions } from "./operations.js";
|
|
2
|
+
import type { AppDisplayState, AppEvents } from "./types.js";
|
|
3
|
+
type Subscriber = () => void;
|
|
4
|
+
/**
|
|
5
|
+
* Framework-agnostic client connecting a display layer (Ada's app, or a
|
|
6
|
+
* customer-built custom app) to the core business-logic frame via typed
|
|
7
|
+
* postMessage. Create one with {@link createBridgeClient}.
|
|
8
|
+
*/
|
|
9
|
+
export interface BridgeClient {
|
|
10
|
+
/** Current {@link AppDisplayState} snapshot, or `null` before first update. */
|
|
11
|
+
getState(): AppDisplayState | null;
|
|
12
|
+
/** Subscribe to state changes. Returns an unsubscribe function. */
|
|
13
|
+
subscribe(callback: Subscriber): () => void;
|
|
14
|
+
/**
|
|
15
|
+
* Dispatch a typed event to core business logic. Send `app.initialize`
|
|
16
|
+
* within 15 seconds of the frame loading, or core unmounts the app frame.
|
|
17
|
+
*/
|
|
18
|
+
sendEvent<K extends keyof AppEvents>(event: K, ...args: AppEvents[K] extends undefined ? [] : [payload: AppEvents[K]]): void;
|
|
19
|
+
/**
|
|
20
|
+
* Subscribe to ONE state key. The callback fires only when that key's
|
|
21
|
+
* value changes (`Object.is` by default) — unlike {@link subscribe}, which
|
|
22
|
+
* fires on every state update. State arrives over postMessage (structured
|
|
23
|
+
* clone), which would mint a fresh identity for every object-valued key
|
|
24
|
+
* on every update, so the client restores the PREVIOUS snapshot's
|
|
25
|
+
* reference for each key that is structurally unchanged before notifying.
|
|
26
|
+
* Identity equality is therefore sound for every key: an object key's
|
|
27
|
+
* identity changes exactly when its content does (for `chat.messages`,
|
|
28
|
+
* that is every streaming delta). Returns an unsubscribe function.
|
|
29
|
+
*/
|
|
30
|
+
subscribeKey<K extends keyof AppDisplayState>(key: K, callback: (value: AppDisplayState[K] | undefined, previous: AppDisplayState[K] | undefined) => void, opts?: ObserveOptions<AppDisplayState[K] | undefined>): () => void;
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to a derived projection of the state. The callback fires only
|
|
33
|
+
* when the selected value changes (`Object.is` by default; pass a shallow
|
|
34
|
+
* or structural `equals` for object/array selectors). Returns an
|
|
35
|
+
* unsubscribe function.
|
|
36
|
+
*/
|
|
37
|
+
select<T>(selector: (state: AppDisplayState | null) => T, callback: (value: T, previous: T | undefined) => void, opts?: ObserveOptions<T>): () => void;
|
|
38
|
+
/**
|
|
39
|
+
* Typed helpers over the event/state contract — sends with their guards,
|
|
40
|
+
* debounces, and state-edge correlation mined from Ada's reference app.
|
|
41
|
+
*/
|
|
42
|
+
readonly operations: BridgeOperations;
|
|
43
|
+
/**
|
|
44
|
+
* Remove all listeners and postMessage handlers, and reject every
|
|
45
|
+
* in-flight `operations` `settled()` promise with a
|
|
46
|
+
* `BridgeClientDestroyedError` (detect it by `error.name`).
|
|
47
|
+
*/
|
|
48
|
+
destroy(): void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a {@link BridgeClient} bound to the parent core frame.
|
|
52
|
+
*
|
|
53
|
+
* Declaration only (D21 dependency inversion): the implementation lives in
|
|
54
|
+
* the private `packages/bridge-runtime` workspace and ships exclusively as
|
|
55
|
+
* the CDN asset `bridge.js` — obtain it through `loadMessagingBridge()`.
|
|
56
|
+
* The runtime compiles against this signature (`MessagingBridgeModule` is
|
|
57
|
+
* pinned both ways by bridge-runtime's `cdn-contract.ts`), so the published
|
|
58
|
+
* type can never drift from the CDN implementation.
|
|
59
|
+
*
|
|
60
|
+
* Security model: the core origin is derived from `document.referrer` when
|
|
61
|
+
* available, otherwise locked on the first valid `core.state.update` message
|
|
62
|
+
* (trust-on-first-use); state from any other origin is rejected, and events
|
|
63
|
+
* only ever target the locked origin — never `"*"`. In a custom-app document
|
|
64
|
+
* the first `app.initialize` transfers a MessagePort to core and all later
|
|
65
|
+
* traffic rides that port. A native-injected `window.__ADA_INITIAL_STATE__`
|
|
66
|
+
* snapshot seeds the state when its timestamp is within a 10-minute TTL.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createBridgeClient(): BridgeClient;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { BridgeClient } from "./bridge-client.js";
|
|
2
|
+
import type { MessagingBridgeModule } from "./loader.js";
|
|
3
|
+
import type { AppDisplayState } from "./types.js";
|
|
4
|
+
export declare const BridgeContext: import("react").Context<BridgeClient | null>;
|
|
5
|
+
/**
|
|
6
|
+
* Provide a {@link BridgeClient} to the React tree.
|
|
7
|
+
*
|
|
8
|
+
* Pure delegation: the caller owns the client lifecycle. The provider does not
|
|
9
|
+
* create, initialize, or destroy the client — obtain one from the CDN module
|
|
10
|
+
* returned by `loadMessagingBridge()` (or use {@link createBridgeProvider} to
|
|
11
|
+
* have loading, `app.initialize`, and teardown handled for you).
|
|
12
|
+
*/
|
|
13
|
+
export declare function BridgeProvider({ client, children, }: {
|
|
14
|
+
client: BridgeClient;
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}): React.JSX.Element;
|
|
17
|
+
/** The active {@link BridgeClient}. Throws outside a `BridgeProvider`. */
|
|
18
|
+
export declare function useBridgeClient(): BridgeClient;
|
|
19
|
+
/**
|
|
20
|
+
* The full display-state snapshot, or `null` before the first
|
|
21
|
+
* `core.state.update`. Re-renders on every state change.
|
|
22
|
+
*/
|
|
23
|
+
export declare function useBridgeState(): AppDisplayState | null;
|
|
24
|
+
/**
|
|
25
|
+
* Returns a single typed value from the bridge state, or `null` when the state
|
|
26
|
+
* is not yet available. Provides full TypeScript autocomplete for the key names
|
|
27
|
+
* and returns the exact type declared in AppDisplayState.
|
|
28
|
+
*
|
|
29
|
+
* Re-renders only when THIS key's value changes (delegating to the client's
|
|
30
|
+
* `subscribeKey`), not on every state update the way `useBridgeState` does.
|
|
31
|
+
*/
|
|
32
|
+
export declare function useBridgeStateKey<K extends keyof AppDisplayState>(key: K): AppDisplayState[K] | null;
|
|
33
|
+
export interface LoadedBridgeProviderProps {
|
|
34
|
+
children: React.ReactNode;
|
|
35
|
+
/** Rendered while the bridge runtime is loading. Defaults to nothing. */
|
|
36
|
+
fallback?: React.ReactNode;
|
|
37
|
+
/**
|
|
38
|
+
* Rendered when the runtime fails to load (network, CSP, 404). Defaults
|
|
39
|
+
* to `fallback`, so without it a failure looks like loading forever.
|
|
40
|
+
*/
|
|
41
|
+
errorFallback?: React.ReactNode;
|
|
42
|
+
/**
|
|
43
|
+
* Called when the runtime fails to load, with the rejection — a
|
|
44
|
+
* {@link MessagingBridgeLoadError} for every loader-detected failure.
|
|
45
|
+
* When omitted, the failure is logged with `console.error`.
|
|
46
|
+
*/
|
|
47
|
+
onError?: (error: unknown) => void;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Build a provider component that loads the bridge runtime from the CDN,
|
|
51
|
+
* creates a client, sends the required `app.initialize` handshake, and
|
|
52
|
+
* destroys the client on unmount.
|
|
53
|
+
*
|
|
54
|
+
* The returned component renders `fallback` (default: nothing) until the
|
|
55
|
+
* runtime has loaded, then renders `children` inside a {@link BridgeProvider}.
|
|
56
|
+
* When the load fails it renders `errorFallback` (default: `fallback`) and
|
|
57
|
+
* reports the error through `onError` (default: `console.error`).
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* const AdaBridgeProvider = createBridgeProvider();
|
|
61
|
+
* root.render(<AdaBridgeProvider><MyChatUi /></AdaBridgeProvider>);
|
|
62
|
+
*/
|
|
63
|
+
export declare function createBridgeProvider(load?: () => Promise<MessagingBridgeModule>): (props: LoadedBridgeProviderProps) => React.JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git commit SHA of the monorepo commit this npm version was published from.
|
|
3
|
+
* The CDN pipeline deploys each main commit's bridge runtime under an
|
|
4
|
+
* immutable SHA root, so this value names the CDN build associated with this
|
|
5
|
+
* npm version. Pass it as the `pinBuildSha` loader option to load exactly
|
|
6
|
+
* that runtime build — not recommended for production (see the README).
|
|
7
|
+
*
|
|
8
|
+
* In the repository, and in any locally built copy, the value is the
|
|
9
|
+
* unstamped 40-zero placeholder ({@link isCdnBuildShaStamped} returns
|
|
10
|
+
* `false`); only published tarballs carry a real SHA.
|
|
11
|
+
*/
|
|
12
|
+
export declare const CDN_BUILD_SHA: string;
|
|
13
|
+
/**
|
|
14
|
+
* Whether `sha` (default {@link CDN_BUILD_SHA}) is a stamped full git commit
|
|
15
|
+
* SHA rather than the unstamped repo placeholder.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isCdnBuildShaStamped(sha?: string): boolean;
|
package/dist/derive.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { AppDisplayState, Message } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Pure derivation helpers mined from Ada's reference app (`packages/app`).
|
|
4
|
+
* Each function is a faithful port of logic the app validates in production,
|
|
5
|
+
* so a custom app does not have to rediscover the edge cases the hard way.
|
|
6
|
+
* No DOM, no client: every helper is a pure function of state or messages.
|
|
7
|
+
*
|
|
8
|
+
* Declarations only (D21 dependency inversion): the implementations live in
|
|
9
|
+
* the private `packages/bridge-runtime` workspace and ship exclusively on
|
|
10
|
+
* the CDN runtime, typed on `MessagingBridgeModule`. The runtime compiles
|
|
11
|
+
* against these signatures (pinned both ways by bridge-runtime's
|
|
12
|
+
* `cdn-contract.ts`), so the published types cannot drift from the CDN
|
|
13
|
+
* implementation.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Stable identity for a message across both id swaps. A live stream bubble
|
|
17
|
+
* (`id: "stream:<correlationId>"`) and its authoritative final (`id: <server
|
|
18
|
+
* _id>`) are the same logical reply, and an optimistic user message and its
|
|
19
|
+
* server echo are the same logical send — keying "new message arrived"
|
|
20
|
+
* effects (alert sound, scroll, unread) on raw `id` treats each swap as a
|
|
21
|
+
* fresh arrival. `streamId` is identical across the first swap, `clientKey`
|
|
22
|
+
* across the second, so prefer them in that order and fall back to `id`.
|
|
23
|
+
* Use this as your render key and as the anchor for unread boundaries.
|
|
24
|
+
*/
|
|
25
|
+
export declare function messageKey(message: {
|
|
26
|
+
id: string;
|
|
27
|
+
streamId?: string;
|
|
28
|
+
clientKey?: string;
|
|
29
|
+
}): string;
|
|
30
|
+
/**
|
|
31
|
+
* Whether a rendered row belongs to a conversation the chatter has left.
|
|
32
|
+
*
|
|
33
|
+
* The transcript is deliberately preserved across a conversation change, so a
|
|
34
|
+
* row from the previous conversation stays on screen but must not stay
|
|
35
|
+
* actionable: answering an old interactive row sends its target into the
|
|
36
|
+
* current conversation. The fail-safe directions are asymmetric on purpose:
|
|
37
|
+
* an unstamped message (predates stamping, or optimistic) is treated as
|
|
38
|
+
* current, while a stamped row with no active conversation pin is historical
|
|
39
|
+
* (a light reset clears the pin but keeps the transcript, and a stamped row
|
|
40
|
+
* cannot belong to "no conversation"). Consult this before enabling any
|
|
41
|
+
* interactive row (quick replies, options, retry).
|
|
42
|
+
*/
|
|
43
|
+
export declare function isHistoricalRow(messageConversationId: string | null | undefined, state: AppDisplayState | null | undefined): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* The first message a returning user has not read yet — the first bot/agent
|
|
46
|
+
* message whose durable `_id` cursor sorts (lexically) past the persisted
|
|
47
|
+
* read watermark (`chat.lastReadCursor`). Skips the user's own messages and
|
|
48
|
+
* presence markers, so the unread divider sits before incoming content.
|
|
49
|
+
* Returns `null` when the watermark is unset or everything has been read.
|
|
50
|
+
* Returns the message (not an id) so callers pick the right key: anchor
|
|
51
|
+
* unread boundaries on {@link messageKey} (stable across id swaps); use the
|
|
52
|
+
* raw `id` for DOM lookup.
|
|
53
|
+
*/
|
|
54
|
+
export declare function findFirstUnread(messages: Message[], lastReadCursor: string): Message | null;
|
|
55
|
+
export interface UnreadSelection {
|
|
56
|
+
/**
|
|
57
|
+
* {@link messageKey} of the first unread message (the unread-divider
|
|
58
|
+
* anchor), or `null` when nothing is unread.
|
|
59
|
+
*/
|
|
60
|
+
firstUnreadId: string | null;
|
|
61
|
+
/**
|
|
62
|
+
* Unread messages from the boundary on: `sender !== "user"`, excluding
|
|
63
|
+
* rows that render nothing (presence events without a dedicated row and
|
|
64
|
+
* videos with an unrenderable source).
|
|
65
|
+
*/
|
|
66
|
+
count: number;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Unread boundary and count derived from the transcript and
|
|
70
|
+
* `chat.lastReadCursor`. Pass the output of {@link filterDisplayable} (plus
|
|
71
|
+
* your own `chat.answeredInteractiveIds` filtering) as `messages` so the
|
|
72
|
+
* boundary anchors on a row your transcript actually renders and the count
|
|
73
|
+
* matches visible rows — the reference app derives from its filtered list.
|
|
74
|
+
* Defaults to the raw `chat.messages`; rows that render nothing (non-row
|
|
75
|
+
* presence events, unrenderable videos) are excluded from the count either
|
|
76
|
+
* way. Pass `boundaryId` (a {@link messageKey}) to keep an already-shown
|
|
77
|
+
* divider anchored while newer messages arrive; when the anchor is no longer
|
|
78
|
+
* in the list (a baseline resync replaced the transcript) the boundary falls
|
|
79
|
+
* back to the persisted watermark — the same recovery the reference app
|
|
80
|
+
* performs.
|
|
81
|
+
*/
|
|
82
|
+
export declare function selectUnread(state: AppDisplayState | null, opts?: {
|
|
83
|
+
messages?: Message[];
|
|
84
|
+
boundaryId?: string;
|
|
85
|
+
}): UnreadSelection;
|
|
86
|
+
export interface MessageGroupingEntry {
|
|
87
|
+
/** Render a date divider above this message (first message of its day). */
|
|
88
|
+
showDivider: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* This message continues the previous sender's run — hide the avatar and
|
|
91
|
+
* sender attribution and tighten the spacing.
|
|
92
|
+
*/
|
|
93
|
+
isGroupedWithPrev: boolean;
|
|
94
|
+
}
|
|
95
|
+
/** Per-index date-divider and sender-run grouping decisions. */
|
|
96
|
+
export declare function groupMessages(messages: Message[]): MessageGroupingEntry[];
|
|
97
|
+
/**
|
|
98
|
+
* The messages a transcript should actually render, in order. Drops:
|
|
99
|
+
* non-rendering presence events (see the event allowlist), `video` rows with
|
|
100
|
+
* an unrenderable source, superseded CSAT rows (only the latest survey per
|
|
101
|
+
* `(conversationId, surveyType)` renders), unsubmitted/unscored CSAT rows of
|
|
102
|
+
* an inactive conversation, and the positional Sign-In "Never mind" pair once
|
|
103
|
+
* it is stale. Pass `chat.isConversationActive` (default it to `false` when
|
|
104
|
+
* absent) as `isConversationActive`. Apply your own
|
|
105
|
+
* `chat.answeredInteractiveIds` filtering on quick-reply rows afterwards,
|
|
106
|
+
* then feed the result to {@link selectUnread} via its `messages` option so
|
|
107
|
+
* the unread boundary and count describe the rows you render.
|
|
108
|
+
*/
|
|
109
|
+
export declare function filterDisplayable(messages: Message[], isConversationActive: boolean): Message[];
|
|
110
|
+
/** Display name for the bot: `config.botName`, else `config.handle`, else "Ada". */
|
|
111
|
+
export declare function resolveBotName(state: AppDisplayState | null): string;
|
|
112
|
+
/** Someone is composing a reply (core's typing verdict + active agent). */
|
|
113
|
+
export declare function isAgentTyping(state: AppDisplayState | null): boolean;
|
|
114
|
+
/** Core's connectivity verdict — never substitute `navigator.onLine`. */
|
|
115
|
+
export declare function isConnectivityLost(state: AppDisplayState | null): boolean;
|