@babelforce/babelconnect-sdk 0.10.0 → 0.12.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 CHANGED
@@ -74,6 +74,11 @@ bc.calls.dial("+49301234567"); // click-to-dial from your CRM
74
74
  bc.session.set({ number: "+49301234567" }); // correlate / prefill
75
75
  ```
76
76
 
77
+ No bundler? Load `<script src="https://agent.example.com/embed/babelconnect-embed.iife.js">` instead — it
78
+ defines `window.BabelconnectEmbed` with the same API, no `import` needed. See the
79
+ **[embedding guide](https://babelforce.github.io/babelconnect-sdk/docs/typescript/embedding)**, "Script tag
80
+ (no bundler)".
81
+
77
82
  ## API overview
78
83
 
79
84
  | Area | Exports |
package/dist/client.d.ts CHANGED
@@ -88,10 +88,18 @@ export declare class BabelconnectClient {
88
88
  }): void;
89
89
  /** Open a conference around the current call. `hold` parks that call while you add members and consult. */
90
90
  startConference(hold?: boolean): void;
91
- /** Invite a participant — pass exactly one of `agentId` or `number`. Starts a conference first if none is active. */
91
+ /**
92
+ * Invite a participant — pass exactly one of `agentId` or `number`. Starts a
93
+ * conference first if none is active. `holdOthers` parks every other live
94
+ * member of the CURRENT conference before the invite goes out (CA4's "hold
95
+ * others" checkbox, default off) so they don't overhear the invitee
96
+ * ringing; the server unholds them again once the invitee joins or the
97
+ * invite fails.
98
+ */
92
99
  addConferenceMember(opts: {
93
100
  agentId?: string;
94
101
  number?: string;
102
+ holdOthers?: boolean;
95
103
  }): void;
96
104
  /** Remove a member from the conference (moderator only). */
97
105
  kickConferenceMember(memberId: string): void;
@@ -103,6 +111,37 @@ export declare class BabelconnectClient {
103
111
  endConference(): void;
104
112
  /** Drop only the agent's own leg; the other members stay connected. */
105
113
  leaveConference(): void;
114
+ /**
115
+ * Enter a campaign (the picker's "Enter"). `paused` false (default) starts
116
+ * the agent `waiting` for a lead right away; true enters already idle.
117
+ */
118
+ joinCampaign(campaignId: string, paused?: boolean): void;
119
+ /** Exit the current campaign (selector row or in-call). */
120
+ leaveCampaign(): void;
121
+ /** Pause (`on` true) or resume (`on` false) the campaign. */
122
+ pauseCampaign(on: boolean): void;
123
+ /**
124
+ * List the active outbound-dialer campaigns the agent may join (the
125
+ * Outbound tab's campaign picker, OBDA1).
126
+ */
127
+ listCampaigns(): Promise<import("./gen/babelconnect/v1/babelconnect_pb.js").Campaign[]>;
128
+ /**
129
+ * End the agent's live campaign call (OBDB2) — distinct from `hangup`: the
130
+ * backend both releases the Vicidial-side channel and drives the campaign
131
+ * member state to DISPOSITION.
132
+ */
133
+ endCampaignCall(): void;
134
+ /**
135
+ * Record the outcome of a campaign call (OBDC1). `callbackDate` is ISO
136
+ * `YYYY-MM-DDTHH:mmZZ` and should be left empty except for the "callback"
137
+ * disposition (OBDC2).
138
+ */
139
+ disposeCall(code: string, callbackDate?: string): void;
140
+ /**
141
+ * List the available call-outcome codes for the disposition picker
142
+ * (OBDC1) — a static global list, not campaign-scoped.
143
+ */
144
+ listDispositions(): Promise<string[]>;
106
145
  /** Add seconds to the after-call-work countdown (default 30). Show only when `wrapUp.canExtend`. */
107
146
  wrapUpExtend(seconds?: number): void;
108
147
  /** End after-call work early. Show only when `wrapUp.canCancel`. */
@@ -140,6 +179,12 @@ export declare class BabelconnectClient {
140
179
  * `query` filters by label/number.
141
180
  */
142
181
  getPhonebook(max?: number, page?: number, query?: string): Promise<import("./gen/babelconnect/v1/babelconnect_pb.js").PhonebookEntry[]>;
182
+ /**
183
+ * Autocomplete blind-transfer / add-member targets — agents, raw numbers,
184
+ * and IVR applications — as the caller types. `query` filters server-side;
185
+ * empty returns the default result set.
186
+ */
187
+ getTransferTargets(query?: string): Promise<import("./gen/babelconnect/v1/babelconnect_pb.js").TransferTarget[]>;
143
188
  /** Tear down media legs and the connection. */
144
189
  close(): Promise<void>;
145
190
  private runSubscribe;
@@ -150,3 +195,8 @@ export declare class BabelconnectClient {
150
195
  private rawSend;
151
196
  private closeAllMedia;
152
197
  }
198
+ /**
199
+ * Stamp the SDK identity (name + build version) onto a request's headers — sent
200
+ * on every unary + streaming call so the server can identify the client (APP-A5).
201
+ */
202
+ export declare function applySdkHeaders(header: Headers): void;
package/dist/client.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { createClient, } from "@connectrpc/connect";
2
2
  import { createGrpcWebTransport } from "@connectrpc/connect-web";
3
+ import { SDK_VERSION } from "./version.js";
3
4
  import { Agent } from "./gen/babelconnect/v1/babelconnect_connect.js";
4
- import { AddConferenceMember, AnswerCall, CallDirection, CallLifecycle, CallSource, Command, EndConference, Error as BcError, FlagRecording, Hangup, HistoryRequest, SmsThreadRequest, PhonebookRequest, HoldConferenceMember, Hold, KickConferenceMember, LeaveConference, MarkConversationRead, Mute, MuteConferenceMember, PlaceCall, Register, ResetLineStatus, SendDigits, SendSmsRequest, SetAgentNumber, SetConversationOpen, SetDisplayAs, SetPresence, SetWebrtc, StartConference, StartRecording, StopRecording, SetRecordingTags, SubscribeRequest, Transfer, WrapUpCancel, WrapUpExtend, } from "./gen/babelconnect/v1/babelconnect_pb.js";
5
+ import { AddConferenceMember, AnswerCall, CallDirection, CallLifecycle, CallSource, Command, DisposeCall, EndCampaignCall, EndConference, Error as BcError, FlagRecording, Hangup, HistoryRequest, SmsThreadRequest, PhonebookRequest, HoldConferenceMember, Hold, JoinCampaign, KickConferenceMember, LeaveCampaign, LeaveConference, ListCampaignsRequest, ListDispositionsRequest, MarkConversationRead, Mute, MuteConferenceMember, PauseCampaign, PlaceCall, Pong, Register, ResetLineStatus, SendDigits, SendSmsRequest, SetAgentNumber, SetConversationOpen, SetDisplayAs, SetPresence, SetWebrtc, StartConference, StartRecording, StopRecording, SetRecordingTags, SubscribeRequest, Transfer, TransferTargetsRequest, WrapUpCancel, WrapUpExtend, } from "./gen/babelconnect/v1/babelconnect_pb.js";
5
6
  import { browserMediaFactory, MediaError, toRTCIceServers, } from "./media.js";
6
7
  import { StateCache } from "./state-cache.js";
7
8
  /**
@@ -150,7 +151,14 @@ export class BabelconnectClient {
150
151
  },
151
152
  }));
152
153
  }
153
- /** Invite a participant — pass exactly one of `agentId` or `number`. Starts a conference first if none is active. */
154
+ /**
155
+ * Invite a participant — pass exactly one of `agentId` or `number`. Starts a
156
+ * conference first if none is active. `holdOthers` parks every other live
157
+ * member of the CURRENT conference before the invite goes out (CA4's "hold
158
+ * others" checkbox, default off) so they don't overhear the invitee
159
+ * ringing; the server unholds them again once the invitee joins or the
160
+ * invite fails.
161
+ */
154
162
  addConferenceMember(opts) {
155
163
  this.send(new Command({
156
164
  command: {
@@ -158,6 +166,7 @@ export class BabelconnectClient {
158
166
  value: new AddConferenceMember({
159
167
  agentId: opts.agentId ?? "",
160
168
  number: opts.number ?? "",
169
+ holdOthers: opts.holdOthers ?? false,
161
170
  }),
162
171
  },
163
172
  }));
@@ -201,6 +210,73 @@ export class BabelconnectClient {
201
210
  command: { case: "leaveConference", value: new LeaveConference({}) },
202
211
  }));
203
212
  }
213
+ // --- Outbound dialer (campaign mode, OBDA1–3) ---
214
+ /**
215
+ * Enter a campaign (the picker's "Enter"). `paused` false (default) starts
216
+ * the agent `waiting` for a lead right away; true enters already idle.
217
+ */
218
+ joinCampaign(campaignId, paused = false) {
219
+ this.send(new Command({
220
+ command: {
221
+ case: "joinCampaign",
222
+ value: new JoinCampaign({ campaignId, paused }),
223
+ },
224
+ }));
225
+ }
226
+ /** Exit the current campaign (selector row or in-call). */
227
+ leaveCampaign() {
228
+ this.send(new Command({
229
+ command: { case: "leaveCampaign", value: new LeaveCampaign({}) },
230
+ }));
231
+ }
232
+ /** Pause (`on` true) or resume (`on` false) the campaign. */
233
+ pauseCampaign(on) {
234
+ this.send(new Command({
235
+ command: { case: "pauseCampaign", value: new PauseCampaign({ on }) },
236
+ }));
237
+ }
238
+ /**
239
+ * List the active outbound-dialer campaigns the agent may join (the
240
+ * Outbound tab's campaign picker, OBDA1).
241
+ */
242
+ async listCampaigns() {
243
+ const resp = await this.rpc.listCampaigns(new ListCampaignsRequest({}));
244
+ return resp.campaigns;
245
+ }
246
+ /**
247
+ * End the agent's live campaign call (OBDB2) — distinct from `hangup`: the
248
+ * backend both releases the Vicidial-side channel and drives the campaign
249
+ * member state to DISPOSITION.
250
+ */
251
+ endCampaignCall() {
252
+ this.send(new Command({
253
+ command: {
254
+ case: "endCampaignCall",
255
+ value: new EndCampaignCall({}),
256
+ },
257
+ }));
258
+ }
259
+ /**
260
+ * Record the outcome of a campaign call (OBDC1). `callbackDate` is ISO
261
+ * `YYYY-MM-DDTHH:mmZZ` and should be left empty except for the "callback"
262
+ * disposition (OBDC2).
263
+ */
264
+ disposeCall(code, callbackDate = "") {
265
+ this.send(new Command({
266
+ command: {
267
+ case: "disposeCall",
268
+ value: new DisposeCall({ code, callbackDate }),
269
+ },
270
+ }));
271
+ }
272
+ /**
273
+ * List the available call-outcome codes for the disposition picker
274
+ * (OBDC1) — a static global list, not campaign-scoped.
275
+ */
276
+ async listDispositions() {
277
+ const resp = await this.rpc.listDispositions(new ListDispositionsRequest({}));
278
+ return resp.reasons;
279
+ }
204
280
  /** Add seconds to the after-call-work countdown (default 30). Show only when `wrapUp.canExtend`. */
205
281
  wrapUpExtend(seconds = 30) {
206
282
  this.send(new Command({
@@ -321,6 +397,15 @@ export class BabelconnectClient {
321
397
  const resp = await this.rpc.getPhonebook(new PhonebookRequest({ max, page, query }));
322
398
  return resp.entries;
323
399
  }
400
+ /**
401
+ * Autocomplete blind-transfer / add-member targets — agents, raw numbers,
402
+ * and IVR applications — as the caller types. `query` filters server-side;
403
+ * empty returns the default result set.
404
+ */
405
+ async getTransferTargets(query = "") {
406
+ const resp = await this.rpc.getTransferTargets(new TransferTargetsRequest({ query }));
407
+ return resp.targets;
408
+ }
324
409
  /** Tear down media legs and the connection. */
325
410
  async close() {
326
411
  this.closed = true;
@@ -346,6 +431,15 @@ export class BabelconnectClient {
346
431
  // pass so a 15s heartbeat doesn't trigger a no-op reconcile every tick.
347
432
  if (u.update.case === "keepalive")
348
433
  return;
434
+ // Ping: reply with a Pong echoing the seq so the server can measure RTT +
435
+ // liveness (CALL-M6). Control-only, like keepalive — short-circuited before the
436
+ // cache and the ready/flush logic so it is never mistaken for the snapshot.
437
+ if (u.update.case === "ping") {
438
+ void this.rawSend(new Command({
439
+ command: { case: "pong", value: new Pong({ seq: u.update.value.seq }) },
440
+ }));
441
+ return;
442
+ }
349
443
  // First message = the snapshot ⇒ the session is registered server-side, so
350
444
  // flush intents queued during connect() (register(), an early dial, …).
351
445
  if (!this.ready) {
@@ -464,10 +558,22 @@ export class BabelconnectClient {
464
558
  await Promise.all(all.map((m) => m.close()));
465
559
  }
466
560
  }
467
- /** An interceptor that attaches the bearer token to every gRPC-web call. */
561
+ /**
562
+ * Stamp the SDK identity (name + build version) onto a request's headers — sent
563
+ * on every unary + streaming call so the server can identify the client (APP-A5).
564
+ */
565
+ export function applySdkHeaders(header) {
566
+ header.set("x-babelconnect-sdk", "ts");
567
+ header.set("x-babelconnect-sdk-version", SDK_VERSION);
568
+ }
569
+ /**
570
+ * An interceptor that attaches the bearer token and the SDK identity headers to
571
+ * every gRPC-web call (unary + the long-lived Subscribe stream).
572
+ */
468
573
  function authInterceptor(token) {
469
574
  return (next) => (req) => {
470
575
  req.header.set("Authorization", `Bearer ${token}`);
576
+ applySdkHeaders(req.header);
471
577
  return next(req);
472
578
  };
473
579
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Script-tag build entry (EMB21) — bundled by esbuild (not `tsc`, which only emits
3
+ * the ESM `dist/embed/index.js`) into a self-contained IIFE at
4
+ * `dist/web/babelconnect-embed.iife.js`, for CRM hosts with no bundler/ESM support:
5
+ *
6
+ * ```html
7
+ * <script src="https://agent.example.com/embed/babelconnect-embed.iife.js"></script>
8
+ * <script>
9
+ * const bc = BabelconnectEmbed.mount({ container, serverUrl, token });
10
+ * </script>
11
+ * ```
12
+ *
13
+ * Loading this file defines a single global, `window.BabelconnectEmbed` — the same
14
+ * class exported as `BabelconnectEmbed` from `@babelforce/babelconnect-sdk/embed`,
15
+ * so `BabelconnectEmbed.mount({...})` here is identical to the ESM
16
+ * `BabelconnectEmbed.mount({...})`. The name is deliberately **distinct** from the
17
+ * legacy `@babelforce/bc-connect` global (`BabelconnectConnect`) — the two APIs
18
+ * differ and a compatibility shim is out of scope; see EMBEDDING.md for the
19
+ * migration.
20
+ *
21
+ * This file has no `export` statement on purpose: esbuild's `--format=iife` wraps
22
+ * an entry's *exports* into a namespace object under `--global-name`, which would
23
+ * put the class at `window.BabelconnectEmbed.BabelconnectEmbed` instead of
24
+ * `window.BabelconnectEmbed` itself. Assigning to `globalThis` directly here keeps
25
+ * the global exactly the class.
26
+ */
27
+ import { BabelconnectEmbed } from "./index.js";
28
+ declare global {
29
+ interface Window {
30
+ BabelconnectEmbed: typeof BabelconnectEmbed;
31
+ }
32
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Script-tag build entry (EMB21) — bundled by esbuild (not `tsc`, which only emits
3
+ * the ESM `dist/embed/index.js`) into a self-contained IIFE at
4
+ * `dist/web/babelconnect-embed.iife.js`, for CRM hosts with no bundler/ESM support:
5
+ *
6
+ * ```html
7
+ * <script src="https://agent.example.com/embed/babelconnect-embed.iife.js"></script>
8
+ * <script>
9
+ * const bc = BabelconnectEmbed.mount({ container, serverUrl, token });
10
+ * </script>
11
+ * ```
12
+ *
13
+ * Loading this file defines a single global, `window.BabelconnectEmbed` — the same
14
+ * class exported as `BabelconnectEmbed` from `@babelforce/babelconnect-sdk/embed`,
15
+ * so `BabelconnectEmbed.mount({...})` here is identical to the ESM
16
+ * `BabelconnectEmbed.mount({...})`. The name is deliberately **distinct** from the
17
+ * legacy `@babelforce/bc-connect` global (`BabelconnectConnect`) — the two APIs
18
+ * differ and a compatibility shim is out of scope; see EMBEDDING.md for the
19
+ * migration.
20
+ *
21
+ * This file has no `export` statement on purpose: esbuild's `--format=iife` wraps
22
+ * an entry's *exports* into a namespace object under `--global-name`, which would
23
+ * put the class at `window.BabelconnectEmbed.BabelconnectEmbed` instead of
24
+ * `window.BabelconnectEmbed` itself. Assigning to `globalThis` directly here keeps
25
+ * the global exactly the class.
26
+ */
27
+ import { BabelconnectEmbed } from "./index.js";
28
+ globalThis.BabelconnectEmbed = BabelconnectEmbed;
@@ -29,6 +29,17 @@ export interface EmbedOptions {
29
29
  session?: Record<string, unknown>;
30
30
  /** Optional initial shared context (also settable later via {@link context}). */
31
31
  context?: Record<string, unknown>;
32
+ /**
33
+ * The `agent.loaded`/`user.loaded` event-payload schema version (EMB20).
34
+ * Omit for the app's current default (`"v3"` — just the top-level
35
+ * `agentId`, no legacy struct nesting). Pass `"v1"` to opt into the
36
+ * bc-connect-compatible shape (`agent.loaded` → `{agentId,
37
+ * agent:{id,name,email,number}}`, `user.loaded` → `{agentId,
38
+ * user:{email}}`) for a host still doing bc-v2-style CRM matching by email
39
+ * or routing by number. See the Embedding guide, "Loaded event schema
40
+ * version".
41
+ */
42
+ eventsVersion?: string;
32
43
  /** Path within the app (default `/`). */
33
44
  path?: string;
34
45
  /** Optional className for the iframe. */
@@ -36,21 +47,49 @@ export interface EmbedOptions {
36
47
  }
37
48
  /** A handler for an app→host event's `data` payload. */
38
49
  export type EmbedEventHandler = (data: unknown) => void;
50
+ /** Args for {@link BabelconnectEmbed.auth}'s `set`. */
51
+ export interface AuthSetArgs {
52
+ /** New bearer token. */
53
+ token: string;
54
+ /** Updated session correlation. Omit to leave the currently-tracked value unchanged. */
55
+ session?: Record<string, unknown>;
56
+ /** Updated shared context. Omit to leave the currently-tracked value unchanged. */
57
+ context?: Record<string, unknown>;
58
+ /** Updated events-schema version (EMB20, see {@link EmbedOptions.eventsVersion}). Only takes
59
+ * effect on the NEXT `ready` handshake (an iframe reload) — a live session's already-emitted
60
+ * `agent.loaded`/`user.loaded` don't refire. Omit to leave the currently-tracked value unchanged. */
61
+ eventsVersion?: string;
62
+ }
39
63
  /**
40
- * A mounted embed. Use the verb groups ({@link calls}, {@link session},
41
- * {@link context}, {@link app}) to drive the app, and {@link on} to receive
42
- * app→host events (`agent.loaded`, `cti.call`, `cti.error`, …).
64
+ * A mounted embed. Use the verb groups ({@link auth}, {@link calls},
65
+ * {@link session}, {@link context}, {@link app}) to drive the app, and
66
+ * {@link on} to receive app→host events (`agent.loaded`, `cti.call`,
67
+ * `cti.error`, …).
43
68
  */
44
69
  export declare class BabelconnectEmbed {
45
- private readonly opts;
46
70
  private readonly iframe;
47
71
  private readonly serverOrigin;
48
72
  private readonly handlers;
49
73
  private readonly onMessage;
50
74
  private disposed;
75
+ private currentToken;
76
+ private currentSession;
77
+ private currentContext;
78
+ private currentEventsVersion;
51
79
  private constructor();
52
80
  /** Mount the embed: inject the iframe and start the bridge. */
53
81
  static mount(opts: EmbedOptions): BabelconnectEmbed;
82
+ /**
83
+ * Refresh the bearer token (and optionally session/context) mid-session —
84
+ * e.g. ahead of expiry on a long shift. Posts `auth.set` immediately; the
85
+ * live app applies it in place without interrupting the session or any
86
+ * active call (EMB15). Also remembers the values as *current*, so if the
87
+ * iframe later reloads and re-emits `ready`, the handshake hands off this
88
+ * refreshed token rather than the one passed to {@link BabelconnectEmbed.mount}.
89
+ */
90
+ readonly auth: {
91
+ set: (args: AuthSetArgs) => void;
92
+ };
54
93
  /** Click-to-dial: place a call (or `dial=false` to only pre-fill the dialer). */
55
94
  readonly calls: {
56
95
  dial: (number: string, dial?: boolean) => void;
@@ -74,6 +113,7 @@ export declare class BabelconnectEmbed {
74
113
  /** Remove the iframe and stop listening. */
75
114
  dispose(): void;
76
115
  private post;
116
+ private postAuthSet;
77
117
  private handleMessage;
78
118
  private emit;
79
119
  }
@@ -18,20 +18,31 @@
18
18
  * ```
19
19
  */
20
20
  /**
21
- * A mounted embed. Use the verb groups ({@link calls}, {@link session},
22
- * {@link context}, {@link app}) to drive the app, and {@link on} to receive
23
- * app→host events (`agent.loaded`, `cti.call`, `cti.error`, …).
21
+ * A mounted embed. Use the verb groups ({@link auth}, {@link calls},
22
+ * {@link session}, {@link context}, {@link app}) to drive the app, and
23
+ * {@link on} to receive app→host events (`agent.loaded`, `cti.call`,
24
+ * `cti.error`, …).
24
25
  */
25
26
  export class BabelconnectEmbed {
26
- opts;
27
27
  iframe;
28
28
  serverOrigin;
29
29
  handlers = new Map();
30
30
  onMessage;
31
31
  disposed = false;
32
+ // The token/session/context/eventsVersion currently in effect — seeded from
33
+ // `opts` at mount() and updated in place by `auth.set()`, so a later `ready`
34
+ // (an iframe reload) re-sends whatever is *current*, not the mount()-time
35
+ // value.
36
+ currentToken;
37
+ currentSession;
38
+ currentContext;
39
+ currentEventsVersion;
32
40
  constructor(opts) {
33
- this.opts = opts;
34
41
  this.serverOrigin = new URL(opts.serverUrl).origin;
42
+ this.currentToken = opts.token;
43
+ this.currentSession = opts.session;
44
+ this.currentContext = opts.context;
45
+ this.currentEventsVersion = opts.eventsVersion;
35
46
  const iframe = document.createElement("iframe");
36
47
  iframe.src = opts.serverUrl.replace(/\/+$/, "") + (opts.path ?? "/");
37
48
  iframe.allow = "microphone; autoplay";
@@ -49,6 +60,26 @@ export class BabelconnectEmbed {
49
60
  static mount(opts) {
50
61
  return new BabelconnectEmbed(opts);
51
62
  }
63
+ /**
64
+ * Refresh the bearer token (and optionally session/context) mid-session —
65
+ * e.g. ahead of expiry on a long shift. Posts `auth.set` immediately; the
66
+ * live app applies it in place without interrupting the session or any
67
+ * active call (EMB15). Also remembers the values as *current*, so if the
68
+ * iframe later reloads and re-emits `ready`, the handshake hands off this
69
+ * refreshed token rather than the one passed to {@link BabelconnectEmbed.mount}.
70
+ */
71
+ auth = {
72
+ set: (args) => {
73
+ this.currentToken = args.token;
74
+ if (args.session !== undefined)
75
+ this.currentSession = args.session;
76
+ if (args.context !== undefined)
77
+ this.currentContext = args.context;
78
+ if (args.eventsVersion !== undefined)
79
+ this.currentEventsVersion = args.eventsVersion;
80
+ this.postAuthSet();
81
+ },
82
+ };
52
83
  /** Click-to-dial: place a call (or `dial=false` to only pre-fill the dialer). */
53
84
  calls = {
54
85
  dial: (number, dial = true) => this.post("calls", "calls.dial", { number, dial }),
@@ -91,25 +122,46 @@ export class BabelconnectEmbed {
91
122
  post(module, name, args) {
92
123
  this.iframe.contentWindow?.postMessage({ type: "connect", module, name, args }, this.serverOrigin);
93
124
  }
125
+ // Posts the current token/session/context (+ eventsVersion, EMB20) as an
126
+ // auth.set envelope. `eventsVersion` is only included when the host set
127
+ // one — an absent key (rather than an explicit `undefined`) keeps the args
128
+ // shape unchanged for hosts that never opted in, and lets the app apply its
129
+ // own default unambiguously.
130
+ postAuthSet() {
131
+ this.post("auth", "auth.set", {
132
+ token: this.currentToken,
133
+ session: this.currentSession,
134
+ context: this.currentContext,
135
+ ...(this.currentEventsVersion !== undefined ? { eventsVersion: this.currentEventsVersion } : {}),
136
+ });
137
+ }
94
138
  handleMessage(e) {
95
139
  if (e.origin !== this.serverOrigin)
96
140
  return; // only trust the app's origin
97
141
  const msg = e.data;
98
142
  if (!msg || msg.type !== "bcConnect" || typeof msg.name !== "string")
99
143
  return;
100
- // Token handoff: on the app's `ready`, hand off the bearer token (+ optional
101
- // initial session/context) over postMessage — never via the iframe URL.
102
- if (msg.name === "ready") {
103
- this.post("auth", "auth.set", {
104
- token: this.opts.token,
105
- session: this.opts.session,
106
- context: this.opts.context,
107
- });
108
- }
144
+ // Token handoff: on the app's `ready`, hand off the current bearer token
145
+ // (+ session/context/eventsVersion) over postMessage — never via the
146
+ // iframe URL. `ready` also fires on an iframe reload, so this re-sends
147
+ // whatever is current (see `auth.set`), not necessarily the value passed
148
+ // to `mount()`.
149
+ if (msg.name === "ready")
150
+ this.postAuthSet();
109
151
  this.emit(msg.name, msg.data);
110
152
  }
111
153
  emit(name, data) {
112
- for (const fn of this.handlers.get(name) ?? [])
113
- fn(data);
154
+ for (const fn of this.handlers.get(name) ?? []) {
155
+ try {
156
+ fn(data);
157
+ }
158
+ catch (err) {
159
+ // A subscriber's handler is host code, not ours — one throwing
160
+ // handler must not stop sibling handlers from receiving the event,
161
+ // nor escape into the window `message` dispatch (which would break
162
+ // unrelated listeners on the host page).
163
+ console.error("[babelconnect-embed] on() handler threw", err);
164
+ }
165
+ }
114
166
  }
115
167
  }
@@ -1,4 +1,4 @@
1
- import { Ack, AgentView, AuthenticateRequest, Command, GetStateRequest, HistoryRequest, HistoryResponse, Identity, PhonebookRequest, PhonebookResponse, SendSmsRequest, SmsConversation, SmsThreadRequest, SmsThreadResponse, StateUpdate, SubscribeRequest } from "./babelconnect_pb.js";
1
+ import { Ack, AgentView, AuthenticateRequest, Command, GetStateRequest, HistoryRequest, HistoryResponse, Identity, ListCampaignsRequest, ListCampaignsResponse, ListDispositionsRequest, ListDispositionsResponse, PhonebookRequest, PhonebookResponse, SendSmsRequest, SmsConversation, SmsThreadRequest, SmsThreadResponse, StateUpdate, SubscribeRequest, TransferTargetsRequest, TransferTargetsResponse } from "./babelconnect_pb.js";
2
2
  import { MethodKind } from "@bufbuild/protobuf";
3
3
  /**
4
4
  * @generated from service babelconnect.v1.Agent
@@ -101,6 +101,24 @@ export declare const Agent: {
101
101
  readonly O: typeof PhonebookResponse;
102
102
  readonly kind: MethodKind.Unary;
103
103
  };
104
+ /**
105
+ * GetTransferTargets autocompletes blind-transfer / add-member targets as the
106
+ * agent types — agents, raw numbers, and IVR applications (GET
107
+ * /agent/calls/transfer/targets?query=, bc-standalone-2
108
+ * src/components/Common/NumberListSelect.vue:59). `query` filters server-side
109
+ * (substring across name/email/number for agents, name for applications); the
110
+ * backend has no call-context param, so eligibility by call direction is a
111
+ * separate, already-server-computed flag the client applies itself
112
+ * (CallState.can_transfer_to_application hides APPLICATION results — CALL-I3).
113
+ *
114
+ * @generated from rpc babelconnect.v1.Agent.GetTransferTargets
115
+ */
116
+ readonly getTransferTargets: {
117
+ readonly name: "GetTransferTargets";
118
+ readonly I: typeof TransferTargetsRequest;
119
+ readonly O: typeof TransferTargetsResponse;
120
+ readonly kind: MethodKind.Unary;
121
+ };
104
122
  /**
105
123
  * GetState returns the agent's current AgentView as a single unary call — the
106
124
  * REST/web "get current state" (the Subscribe stream is the realtime twin). When
@@ -128,5 +146,35 @@ export declare const Agent: {
128
146
  readonly O: typeof SmsConversation;
129
147
  readonly kind: MethodKind.Unary;
130
148
  };
149
+ /**
150
+ * ListCampaigns lists the outbound-dialer campaigns the agent may join (the
151
+ * Outbound tab's campaign picker, OBDA1) — on-demand reference data like
152
+ * GetPhonebook/GetTransferTargets, backed by GET /api/v2/agent/outbound/
153
+ * campaigns?active=true (sbf/services ObdController#listCampaigns).
154
+ *
155
+ * @generated from rpc babelconnect.v1.Agent.ListCampaigns
156
+ */
157
+ readonly listCampaigns: {
158
+ readonly name: "ListCampaigns";
159
+ readonly I: typeof ListCampaignsRequest;
160
+ readonly O: typeof ListCampaignsResponse;
161
+ readonly kind: MethodKind.Unary;
162
+ };
163
+ /**
164
+ * ListDispositions lists the available call-outcome codes for the disposition
165
+ * picker (OBDC1) — on-demand reference data like ListCampaigns, backed by GET
166
+ * /api/v2/agent/outbound/campaigns/{id}/dispositions (sbf/services
167
+ * ObdController#listDispositions). Grounded: the `{id}` path segment is never
168
+ * read server-side — the list is a static global, not campaign-scoped — so the
169
+ * request carries no campaign id either.
170
+ *
171
+ * @generated from rpc babelconnect.v1.Agent.ListDispositions
172
+ */
173
+ readonly listDispositions: {
174
+ readonly name: "ListDispositions";
175
+ readonly I: typeof ListDispositionsRequest;
176
+ readonly O: typeof ListDispositionsResponse;
177
+ readonly kind: MethodKind.Unary;
178
+ };
131
179
  };
132
180
  };
@@ -27,7 +27,7 @@
27
27
  // In addition to gRPC/gRPC-web, the unary RPCs are exposed as a REST+JSON surface
28
28
  // via grpc-gateway (the google.api.http annotations below) with an OpenAPI v2 spec
29
29
  // — one proto source of truth, two wire surfaces. Streaming stays gRPC/gRPC-web.
30
- import { Ack, AgentView, AuthenticateRequest, Command, GetStateRequest, HistoryRequest, HistoryResponse, Identity, PhonebookRequest, PhonebookResponse, SendSmsRequest, SmsConversation, SmsThreadRequest, SmsThreadResponse, StateUpdate, SubscribeRequest } from "./babelconnect_pb.js";
30
+ import { Ack, AgentView, AuthenticateRequest, Command, GetStateRequest, HistoryRequest, HistoryResponse, Identity, ListCampaignsRequest, ListCampaignsResponse, ListDispositionsRequest, ListDispositionsResponse, PhonebookRequest, PhonebookResponse, SendSmsRequest, SmsConversation, SmsThreadRequest, SmsThreadResponse, StateUpdate, SubscribeRequest, TransferTargetsRequest, TransferTargetsResponse } from "./babelconnect_pb.js";
31
31
  import { MethodKind } from "@bufbuild/protobuf";
32
32
  /**
33
33
  * @generated from service babelconnect.v1.Agent
@@ -130,6 +130,24 @@ export const Agent = {
130
130
  O: PhonebookResponse,
131
131
  kind: MethodKind.Unary,
132
132
  },
133
+ /**
134
+ * GetTransferTargets autocompletes blind-transfer / add-member targets as the
135
+ * agent types — agents, raw numbers, and IVR applications (GET
136
+ * /agent/calls/transfer/targets?query=, bc-standalone-2
137
+ * src/components/Common/NumberListSelect.vue:59). `query` filters server-side
138
+ * (substring across name/email/number for agents, name for applications); the
139
+ * backend has no call-context param, so eligibility by call direction is a
140
+ * separate, already-server-computed flag the client applies itself
141
+ * (CallState.can_transfer_to_application hides APPLICATION results — CALL-I3).
142
+ *
143
+ * @generated from rpc babelconnect.v1.Agent.GetTransferTargets
144
+ */
145
+ getTransferTargets: {
146
+ name: "GetTransferTargets",
147
+ I: TransferTargetsRequest,
148
+ O: TransferTargetsResponse,
149
+ kind: MethodKind.Unary,
150
+ },
133
151
  /**
134
152
  * GetState returns the agent's current AgentView as a single unary call — the
135
153
  * REST/web "get current state" (the Subscribe stream is the realtime twin). When
@@ -157,5 +175,35 @@ export const Agent = {
157
175
  O: SmsConversation,
158
176
  kind: MethodKind.Unary,
159
177
  },
178
+ /**
179
+ * ListCampaigns lists the outbound-dialer campaigns the agent may join (the
180
+ * Outbound tab's campaign picker, OBDA1) — on-demand reference data like
181
+ * GetPhonebook/GetTransferTargets, backed by GET /api/v2/agent/outbound/
182
+ * campaigns?active=true (sbf/services ObdController#listCampaigns).
183
+ *
184
+ * @generated from rpc babelconnect.v1.Agent.ListCampaigns
185
+ */
186
+ listCampaigns: {
187
+ name: "ListCampaigns",
188
+ I: ListCampaignsRequest,
189
+ O: ListCampaignsResponse,
190
+ kind: MethodKind.Unary,
191
+ },
192
+ /**
193
+ * ListDispositions lists the available call-outcome codes for the disposition
194
+ * picker (OBDC1) — on-demand reference data like ListCampaigns, backed by GET
195
+ * /api/v2/agent/outbound/campaigns/{id}/dispositions (sbf/services
196
+ * ObdController#listDispositions). Grounded: the `{id}` path segment is never
197
+ * read server-side — the list is a static global, not campaign-scoped — so the
198
+ * request carries no campaign id either.
199
+ *
200
+ * @generated from rpc babelconnect.v1.Agent.ListDispositions
201
+ */
202
+ listDispositions: {
203
+ name: "ListDispositions",
204
+ I: ListDispositionsRequest,
205
+ O: ListDispositionsResponse,
206
+ kind: MethodKind.Unary,
207
+ },
160
208
  }
161
209
  };