@babelforce/babelconnect-sdk 0.6.1 → 0.7.1

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/dist/auth.d.ts CHANGED
@@ -1,3 +1,59 @@
1
+ /** The default OAuth public client id for the babelconnect agent app (no secret; PKCE). */
2
+ export declare const DEFAULT_CLIENT_ID = "babelconnect";
3
+ /** A parsed OAuth token response: the bearer plus, when the grant returns them, the rotating refresh token and the access-token lifetime (seconds). */
4
+ export interface TokenResponse {
5
+ access_token: string;
6
+ refresh_token?: string;
7
+ expires_in?: number;
8
+ token_type?: string;
9
+ }
10
+ /** A PKCE code verifier + S256 challenge (RFC 7636). */
11
+ export interface PkceChallenge {
12
+ /** The high-entropy secret to keep client-side and send with {@link authorizationCodeGrant}. */
13
+ codeVerifier: string;
14
+ /** The base64url(SHA-256(codeVerifier)) value to send with {@link buildAuthorizeUrl}. */
15
+ codeChallenge: string;
16
+ codeChallengeMethod: "S256";
17
+ }
18
+ /**
19
+ * Generate a PKCE code verifier + S256 challenge (RFC 7636). Pass `codeChallenge` to
20
+ * {@link buildAuthorizeUrl} and keep `codeVerifier` to later exchange the returned code via
21
+ * {@link authorizationCodeGrant}. Uses the Web Crypto API (Node 18+ / browsers).
22
+ */
23
+ export declare function pkceChallenge(): Promise<PkceChallenge>;
24
+ export interface AuthorizeUrlOptions {
25
+ /** babelconnect-server origin (the same one used for gRPC-web); the authorize endpoint lives on the backend behind the same origin. */
26
+ serverUrl: string;
27
+ /** OAuth client id (defaults to {@link DEFAULT_CLIENT_ID} = `"babelconnect"`). */
28
+ clientId?: string;
29
+ /** The registered callback URL the backend redirects to with the code. */
30
+ redirectUri: string;
31
+ /** OAuth scope (e.g. `"*"`). */
32
+ scope: string;
33
+ codeChallenge: string;
34
+ /** Opaque CSRF token echoed back by the backend; verify it on return. */
35
+ state?: string;
36
+ codeChallengeMethod?: "S256" | "plain";
37
+ }
38
+ /**
39
+ * Build the `GET {serverUrl}/oauth/authorize` URL that starts the Authorization Code + PKCE flow.
40
+ * Redirect the user to it; the backend redirects back to `redirectUri` with a `code` (and `state`).
41
+ */
42
+ export declare function buildAuthorizeUrl(opts: AuthorizeUrlOptions): string;
43
+ /**
44
+ * Exchange an authorization `code` (from the PKCE consent redirect) for tokens via the
45
+ * authorization_code grant against `POST {serverUrl}/oauth/token`. Public clients pass only
46
+ * `codeVerifier` (no secret). Returns the access token (and a rotating refresh token when the
47
+ * backend grants offline access) — pass `access_token` to {@link BabelconnectClient.connect}.
48
+ */
49
+ export declare function authorizationCodeGrant(opts: {
50
+ serverUrl: string;
51
+ code: string;
52
+ redirectUri: string;
53
+ clientId?: string;
54
+ codeVerifier: string;
55
+ clientSecret?: string;
56
+ }): Promise<TokenResponse>;
1
57
  /**
2
58
  * Exchange an agent email/password for a bearer token via babelconnect-server's
3
59
  * OAuth password-grant endpoint (`POST {serverUrl}/oauth/token`).
@@ -19,3 +75,23 @@ export declare function passwordGrant(opts: {
19
75
  pass: string;
20
76
  clientId?: string;
21
77
  }): Promise<string>;
78
+ /**
79
+ * Revoke an OAuth token on sign-out (RFC 7009) via babelconnect-server's
80
+ * `POST {serverUrl}/oauth/revoke` (proxied same-origin like `/oauth/token`).
81
+ *
82
+ * Form-urlencoded `token` (+ `token_type_hint` and optional `clientId`); the
83
+ * bearer is also carried for backends that authenticate the revoke. Best-effort:
84
+ * revocation returns 200 even for an unknown token, so a non-2xx is ignored (the
85
+ * session is ending regardless) — only a transport error rejects.
86
+ *
87
+ * @param opts.serverUrl babelconnect-server origin, e.g. `https://agent.example.com`
88
+ * @param opts.token the token to revoke (also sent as the bearer)
89
+ * @param opts.tokenTypeHint RFC 7009 hint (defaults to `"access_token"`)
90
+ * @param opts.clientId optional OAuth client id
91
+ */
92
+ export declare function revokeToken(opts: {
93
+ serverUrl: string;
94
+ token: string;
95
+ tokenTypeHint?: string;
96
+ clientId?: string;
97
+ }): Promise<void>;
package/dist/auth.js CHANGED
@@ -1,3 +1,68 @@
1
+ /** The default OAuth public client id for the babelconnect agent app (no secret; PKCE). */
2
+ export const DEFAULT_CLIENT_ID = "babelconnect";
3
+ /** base64url-encode bytes without padding (RFC 4648 §5) — the encoding PKCE requires. */
4
+ function base64url(bytes) {
5
+ let bin = "";
6
+ for (const b of bytes)
7
+ bin += String.fromCharCode(b);
8
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
9
+ }
10
+ /**
11
+ * Generate a PKCE code verifier + S256 challenge (RFC 7636). Pass `codeChallenge` to
12
+ * {@link buildAuthorizeUrl} and keep `codeVerifier` to later exchange the returned code via
13
+ * {@link authorizationCodeGrant}. Uses the Web Crypto API (Node 18+ / browsers).
14
+ */
15
+ export async function pkceChallenge() {
16
+ const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
17
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
18
+ return { codeVerifier, codeChallenge: base64url(new Uint8Array(digest)), codeChallengeMethod: "S256" };
19
+ }
20
+ /**
21
+ * Build the `GET {serverUrl}/oauth/authorize` URL that starts the Authorization Code + PKCE flow.
22
+ * Redirect the user to it; the backend redirects back to `redirectUri` with a `code` (and `state`).
23
+ */
24
+ export function buildAuthorizeUrl(opts) {
25
+ const base = opts.serverUrl.replace(/\/+$/, "");
26
+ const params = new URLSearchParams({
27
+ response_type: "code",
28
+ client_id: opts.clientId ?? DEFAULT_CLIENT_ID,
29
+ redirect_uri: opts.redirectUri,
30
+ scope: opts.scope,
31
+ code_challenge: opts.codeChallenge,
32
+ code_challenge_method: opts.codeChallengeMethod ?? "S256",
33
+ });
34
+ if (opts.state !== undefined)
35
+ params.set("state", opts.state);
36
+ return `${base}/oauth/authorize?${params.toString()}`;
37
+ }
38
+ /**
39
+ * Exchange an authorization `code` (from the PKCE consent redirect) for tokens via the
40
+ * authorization_code grant against `POST {serverUrl}/oauth/token`. Public clients pass only
41
+ * `codeVerifier` (no secret). Returns the access token (and a rotating refresh token when the
42
+ * backend grants offline access) — pass `access_token` to {@link BabelconnectClient.connect}.
43
+ */
44
+ export async function authorizationCodeGrant(opts) {
45
+ const base = opts.serverUrl.replace(/\/+$/, "");
46
+ const body = new URLSearchParams({
47
+ grant_type: "authorization_code",
48
+ code: opts.code,
49
+ redirect_uri: opts.redirectUri,
50
+ client_id: opts.clientId ?? DEFAULT_CLIENT_ID,
51
+ code_verifier: opts.codeVerifier,
52
+ });
53
+ if (opts.clientSecret !== undefined)
54
+ body.set("client_secret", opts.clientSecret);
55
+ const resp = await fetch(`${base}/oauth/token`, {
56
+ method: "POST",
57
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
58
+ body,
59
+ });
60
+ const json = (await resp.json().catch(() => ({})));
61
+ if (!resp.ok || !json.access_token) {
62
+ throw new Error(`authorization_code grant failed (status ${resp.status})`);
63
+ }
64
+ return json;
65
+ }
1
66
  /**
2
67
  * Exchange an agent email/password for a bearer token via babelconnect-server's
3
68
  * OAuth password-grant endpoint (`POST {serverUrl}/oauth/token`).
@@ -32,3 +97,34 @@ export async function passwordGrant(opts) {
32
97
  }
33
98
  return json.access_token;
34
99
  }
100
+ /**
101
+ * Revoke an OAuth token on sign-out (RFC 7009) via babelconnect-server's
102
+ * `POST {serverUrl}/oauth/revoke` (proxied same-origin like `/oauth/token`).
103
+ *
104
+ * Form-urlencoded `token` (+ `token_type_hint` and optional `clientId`); the
105
+ * bearer is also carried for backends that authenticate the revoke. Best-effort:
106
+ * revocation returns 200 even for an unknown token, so a non-2xx is ignored (the
107
+ * session is ending regardless) — only a transport error rejects.
108
+ *
109
+ * @param opts.serverUrl babelconnect-server origin, e.g. `https://agent.example.com`
110
+ * @param opts.token the token to revoke (also sent as the bearer)
111
+ * @param opts.tokenTypeHint RFC 7009 hint (defaults to `"access_token"`)
112
+ * @param opts.clientId optional OAuth client id
113
+ */
114
+ export async function revokeToken(opts) {
115
+ const base = opts.serverUrl.replace(/\/+$/, "");
116
+ const body = new URLSearchParams({
117
+ token: opts.token,
118
+ token_type_hint: opts.tokenTypeHint ?? "access_token",
119
+ });
120
+ if (opts.clientId)
121
+ body.set("client_id", opts.clientId);
122
+ await fetch(`${base}/oauth/revoke`, {
123
+ method: "POST",
124
+ headers: {
125
+ "Content-Type": "application/x-www-form-urlencoded",
126
+ Authorization: `Bearer ${opts.token}`,
127
+ },
128
+ body,
129
+ });
130
+ }
package/dist/client.js CHANGED
@@ -1,8 +1,8 @@
1
- import { createClient } from "@connectrpc/connect";
1
+ import { createClient, } from "@connectrpc/connect";
2
2
  import { createGrpcWebTransport } from "@connectrpc/connect-web";
3
3
  import { Agent } from "./gen/babelconnect/v1/babelconnect_connect.js";
4
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 { browserMediaFactory, toRTCIceServers } from "./media.js";
5
+ import { browserMediaFactory, toRTCIceServers, } from "./media.js";
6
6
  import { StateCache } from "./state-cache.js";
7
7
  /**
8
8
  * The TypeScript "dumb renderer" client: opens the `Subscribe`/`Send` gRPC-web
@@ -65,7 +65,9 @@ export class BabelconnectClient {
65
65
  * {@link BabelconnectClient.setWebrtc}(false) + {@link BabelconnectClient.setAgentNumber}.
66
66
  */
67
67
  register(capabilities = ["webrtc"]) {
68
- this.send(new Command({ command: { case: "register", value: new Register({ capabilities }) } }));
68
+ this.send(new Command({
69
+ command: { case: "register", value: new Register({ capabilities }) },
70
+ }));
69
71
  }
70
72
  placeCall(to, opts = {}) {
71
73
  this.send(new Command({
@@ -89,27 +91,39 @@ export class BabelconnectClient {
89
91
  }
90
92
  /** End (or reject) a call by id. */
91
93
  hangup(callId) {
92
- this.send(new Command({ command: { case: "hangup", value: new Hangup({ callId }) } }));
94
+ this.send(new Command({
95
+ command: { case: "hangup", value: new Hangup({ callId }) },
96
+ }));
93
97
  }
94
98
  /** Mute or unmute the agent's own leg of a call. */
95
99
  mute(callId, on) {
96
- this.send(new Command({ command: { case: "mute", value: new Mute({ callId, on }) } }));
100
+ this.send(new Command({
101
+ command: { case: "mute", value: new Mute({ callId, on }) },
102
+ }));
97
103
  }
98
104
  /** Put a call on hold or retrieve it. */
99
105
  hold(callId, on) {
100
- this.send(new Command({ command: { case: "hold", value: new Hold({ callId, on }) } }));
106
+ this.send(new Command({
107
+ command: { case: "hold", value: new Hold({ callId, on }) },
108
+ }));
101
109
  }
102
110
  /** Send DTMF tones into the call (e.g. an IVR menu choice). Valid characters: `0`–`9`, `*`, `#`, `A`–`D`. */
103
111
  sendDigits(callId, digits) {
104
- this.send(new Command({ command: { case: "dtmf", value: new SendDigits({ callId, digits }) } }));
112
+ this.send(new Command({
113
+ command: { case: "dtmf", value: new SendDigits({ callId, digits }) },
114
+ }));
105
115
  }
106
116
  /** Set the outbound caller ID the consumer sees (choose from `agent.availableNumbers`). */
107
117
  setDisplayAs(number) {
108
- this.send(new Command({ command: { case: "setDisplayAs", value: new SetDisplayAs({ number }) } }));
118
+ this.send(new Command({
119
+ command: { case: "setDisplayAs", value: new SetDisplayAs({ number }) },
120
+ }));
109
121
  }
110
122
  /** Switch presence (the selector): "available" or a configured pause reason (see `AgentInfo.presenceOptions`). */
111
123
  setPresence(name) {
112
- this.send(new Command({ command: { case: "setPresence", value: new SetPresence({ name }) } }));
124
+ this.send(new Command({
125
+ command: { case: "setPresence", value: new SetPresence({ name }) },
126
+ }));
113
127
  }
114
128
  /** Transfer to exactly one of `to` (number), `agentId`, or `applicationId`; `warm` = attended. */
115
129
  transfer(callId, to, opts = {}) {
@@ -129,92 +143,163 @@ export class BabelconnectClient {
129
143
  // --- Conferences ---
130
144
  /** Open a conference around the current call. `hold` parks that call while you add members and consult. */
131
145
  startConference(hold = false) {
132
- this.send(new Command({ command: { case: "startConference", value: new StartConference({ hold }) } }));
146
+ this.send(new Command({
147
+ command: {
148
+ case: "startConference",
149
+ value: new StartConference({ hold }),
150
+ },
151
+ }));
133
152
  }
134
153
  /** Invite a participant — pass exactly one of `agentId` or `number`. Starts a conference first if none is active. */
135
154
  addConferenceMember(opts) {
136
155
  this.send(new Command({
137
156
  command: {
138
157
  case: "addConferenceMember",
139
- value: new AddConferenceMember({ agentId: opts.agentId ?? "", number: opts.number ?? "" }),
158
+ value: new AddConferenceMember({
159
+ agentId: opts.agentId ?? "",
160
+ number: opts.number ?? "",
161
+ }),
140
162
  },
141
163
  }));
142
164
  }
143
165
  /** Remove a member from the conference (moderator only). */
144
166
  kickConferenceMember(memberId) {
145
- this.send(new Command({ command: { case: "kickConferenceMember", value: new KickConferenceMember({ memberId }) } }));
167
+ this.send(new Command({
168
+ command: {
169
+ case: "kickConferenceMember",
170
+ value: new KickConferenceMember({ memberId }),
171
+ },
172
+ }));
146
173
  }
147
174
  /** Hold or unhold an individual conference member (moderator only). */
148
175
  holdConferenceMember(memberId, on) {
149
- this.send(new Command({ command: { case: "holdConferenceMember", value: new HoldConferenceMember({ memberId, on }) } }));
176
+ this.send(new Command({
177
+ command: {
178
+ case: "holdConferenceMember",
179
+ value: new HoldConferenceMember({ memberId, on }),
180
+ },
181
+ }));
150
182
  }
151
183
  /** Mute or unmute an individual conference member (moderator only). */
152
184
  muteConferenceMember(memberId, on) {
153
- this.send(new Command({ command: { case: "muteConferenceMember", value: new MuteConferenceMember({ memberId, on }) } }));
185
+ this.send(new Command({
186
+ command: {
187
+ case: "muteConferenceMember",
188
+ value: new MuteConferenceMember({ memberId, on }),
189
+ },
190
+ }));
154
191
  }
155
192
  /** End the whole conference for everyone (moderator only). */
156
193
  endConference() {
157
- this.send(new Command({ command: { case: "endConference", value: new EndConference({}) } }));
194
+ this.send(new Command({
195
+ command: { case: "endConference", value: new EndConference({}) },
196
+ }));
158
197
  }
159
198
  /** Drop only the agent's own leg; the other members stay connected. */
160
199
  leaveConference() {
161
- this.send(new Command({ command: { case: "leaveConference", value: new LeaveConference({}) } }));
200
+ this.send(new Command({
201
+ command: { case: "leaveConference", value: new LeaveConference({}) },
202
+ }));
162
203
  }
163
204
  /** Add seconds to the after-call-work countdown (default 30). Show only when `wrapUp.canExtend`. */
164
205
  wrapUpExtend(seconds = 30) {
165
- this.send(new Command({ command: { case: "wrapUpExtend", value: new WrapUpExtend({ seconds }) } }));
206
+ this.send(new Command({
207
+ command: { case: "wrapUpExtend", value: new WrapUpExtend({ seconds }) },
208
+ }));
166
209
  }
167
210
  /** End after-call work early. Show only when `wrapUp.canCancel`. */
168
211
  wrapUpCancel() {
169
- this.send(new Command({ command: { case: "wrapUpCancel", value: new WrapUpCancel({}) } }));
212
+ this.send(new Command({
213
+ command: { case: "wrapUpCancel", value: new WrapUpCancel({}) },
214
+ }));
170
215
  }
171
216
  /** Clear a blocked line (busy / unreachable / declined) so new calls reach the agent again. */
172
217
  resetLineStatus() {
173
- this.send(new Command({ command: { case: "resetLineStatus", value: new ResetLineStatus({}) } }));
218
+ this.send(new Command({
219
+ command: { case: "resetLineStatus", value: new ResetLineStatus({}) },
220
+ }));
174
221
  }
175
222
  /** Begin recording the current call. Gated on `agent.canRecord`. */
176
223
  startRecording(callId) {
177
- this.send(new Command({ command: { case: "startRecording", value: new StartRecording({ callId }) } }));
224
+ this.send(new Command({
225
+ command: {
226
+ case: "startRecording",
227
+ value: new StartRecording({ callId }),
228
+ },
229
+ }));
178
230
  }
179
231
  /** Stop the call's active recording. */
180
232
  stopRecording(callId) {
181
- this.send(new Command({ command: { case: "stopRecording", value: new StopRecording({ callId }) } }));
233
+ this.send(new Command({
234
+ command: {
235
+ case: "stopRecording",
236
+ value: new StopRecording({ callId }),
237
+ },
238
+ }));
182
239
  }
183
240
  /** Toggle the "flagged" mark on the call's active recording. */
184
241
  flagRecording(callId) {
185
- this.send(new Command({ command: { case: "flagRecording", value: new FlagRecording({ callId }) } }));
242
+ this.send(new Command({
243
+ command: {
244
+ case: "flagRecording",
245
+ value: new FlagRecording({ callId }),
246
+ },
247
+ }));
186
248
  }
187
249
  /** Replace the tags on the call's active recording (choose from `agent.availableTags`). */
188
250
  setRecordingTags(callId, tags) {
189
- this.send(new Command({ command: { case: "setRecordingTags", value: new SetRecordingTags({ callId, tags }) } }));
251
+ this.send(new Command({
252
+ command: {
253
+ case: "setRecordingTags",
254
+ value: new SetRecordingTags({ callId, tags }),
255
+ },
256
+ }));
190
257
  }
191
258
  /** Turn the in-browser WebRTC phone on or off. With it off, set an agent number so calls bridge there. */
192
259
  setWebrtc(on) {
193
- this.send(new Command({ command: { case: "setWebrtc", value: new SetWebrtc({ on }) } }));
260
+ this.send(new Command({
261
+ command: { case: "setWebrtc", value: new SetWebrtc({ on }) },
262
+ }));
194
263
  }
195
264
  /** Set the agent's external phone number; the backend bridges calls there when WebRTC is off. */
196
265
  setAgentNumber(number) {
197
- this.send(new Command({ command: { case: "setAgentNumber", value: new SetAgentNumber({ number }) } }));
266
+ this.send(new Command({
267
+ command: {
268
+ case: "setAgentNumber",
269
+ value: new SetAgentNumber({ number }),
270
+ },
271
+ }));
198
272
  }
199
273
  /** Send an SMS. `from` may be empty (server picks the default); `session` carries CTI/embedding correlation. */
200
274
  sendSms(to, text, opts = {}) {
201
275
  this.send(new Command({
202
276
  command: {
203
277
  case: "sendSms",
204
- value: new SendSmsRequest({ to, text, from: opts.from ?? "", session: opts.session ?? {} }),
278
+ value: new SendSmsRequest({
279
+ to,
280
+ text,
281
+ from: opts.from ?? "",
282
+ session: opts.session ?? {},
283
+ }),
205
284
  },
206
285
  }));
207
286
  }
208
287
  /** Open (reopen) or close (resolve) an SMS conversation. */
209
288
  setConversationOpen(conversationId, open) {
210
289
  this.send(new Command({
211
- command: { case: "setConversationOpen", value: new SetConversationOpen({ conversationId, open }) },
290
+ command: {
291
+ case: "setConversationOpen",
292
+ value: new SetConversationOpen({ conversationId, open }),
293
+ },
212
294
  }));
213
295
  }
214
296
  /** Clear the unread count on an SMS conversation (the agent opened its thread). */
215
297
  markConversationRead(conversationId) {
216
298
  this.send(new Command({
217
- command: { case: "markConversationRead", value: new MarkConversationRead({ conversationId }) },
299
+ command: {
300
+ case: "markConversationRead",
301
+ value: new MarkConversationRead({ conversationId }),
302
+ },
218
303
  }));
219
304
  }
220
305
  /** Fetch a page of the agent's call history (the History tab). */
@@ -245,7 +330,9 @@ export class BabelconnectClient {
245
330
  // --- internals ---
246
331
  async runSubscribe() {
247
332
  try {
248
- for await (const u of this.rpc.subscribe(new SubscribeRequest({}), { signal: this.abort.signal })) {
333
+ for await (const u of this.rpc.subscribe(new SubscribeRequest({}), {
334
+ signal: this.abort.signal,
335
+ })) {
249
336
  this.onUpdate(u);
250
337
  }
251
338
  }
@@ -255,6 +342,10 @@ export class BabelconnectClient {
255
342
  }
256
343
  }
257
344
  onUpdate(u) {
345
+ // Keepalive heartbeat: transport-only, no state — skip the cache/reconcile
346
+ // pass so a 15s heartbeat doesn't trigger a no-op reconcile every tick.
347
+ if (u.update.case === "keepalive")
348
+ return;
258
349
  // First message = the snapshot ⇒ the session is registered server-side, so
259
350
  // flush intents queued during connect() (register(), an early dial, …).
260
351
  if (!this.ready) {
@@ -267,7 +358,8 @@ export class BabelconnectClient {
267
358
  this.opts.onError?.(u.update.value);
268
359
  return;
269
360
  }
270
- if (u.update.case === "patch" && u.update.value.change.case === "notification") {
361
+ if (u.update.case === "patch" &&
362
+ u.update.value.change.case === "notification") {
271
363
  this.opts.onNotification?.(u.update.value.change.value);
272
364
  return;
273
365
  }
@@ -301,9 +393,15 @@ export class BabelconnectClient {
301
393
  async doAnswer(call) {
302
394
  if (this.answering.has(call.id) || this.media.has(call.id))
303
395
  return;
304
- const factory = this.opts.mediaFactory === undefined ? browserMediaFactory : this.opts.mediaFactory;
396
+ const factory = this.opts.mediaFactory === undefined
397
+ ? browserMediaFactory
398
+ : this.opts.mediaFactory;
305
399
  if (!factory) {
306
- this.opts.onError?.(new BcError({ code: "no_media", message: "no media factory configured", callId: call.id }));
400
+ this.opts.onError?.(new BcError({
401
+ code: "no_media",
402
+ message: "no media factory configured",
403
+ callId: call.id,
404
+ }));
307
405
  return;
308
406
  }
309
407
  this.answering.add(call.id);
@@ -314,10 +412,19 @@ export class BabelconnectClient {
314
412
  // undefined so the Media's own fallback applies.
315
413
  const answerSdp = await media.answer(call.webrtcOffer, toRTCIceServers(call.iceServers));
316
414
  this.media.set(call.id, media);
317
- this.send(new Command({ command: { case: "answer", value: new AnswerCall({ callId: call.id, sdp: answerSdp }) } }));
415
+ this.send(new Command({
416
+ command: {
417
+ case: "answer",
418
+ value: new AnswerCall({ callId: call.id, sdp: answerSdp }),
419
+ },
420
+ }));
318
421
  }
319
422
  catch (e) {
320
- this.opts.onError?.(new BcError({ code: "media_answer_failed", message: String(e), callId: call.id }));
423
+ this.opts.onError?.(new BcError({
424
+ code: "media_answer_failed",
425
+ message: String(e),
426
+ callId: call.id,
427
+ }));
321
428
  }
322
429
  finally {
323
430
  this.answering.delete(call.id);
@@ -707,6 +707,15 @@ export declare class AgentInfo extends Message<AgentInfo> {
707
707
  * @generated from field: string account_name = 17;
708
708
  */
709
709
  accountName: string;
710
+ /**
711
+ * Involuntary line block: the ACD/platform marked the agent busy / unreachable /
712
+ * declined (a recoverable state cleared via ResetLineStatus) — distinct from a
713
+ * chosen "busy" presence the agent (or sign-out-as-busy) selected. Drives the
714
+ * line-blocked banner + Reset; a voluntary busy presence leaves this false.
715
+ *
716
+ * @generated from field: bool line_blocked = 18;
717
+ */
718
+ lineBlocked: boolean;
710
719
  constructor(data?: PartialMessage<AgentInfo>);
711
720
  static readonly runtime: typeof proto3;
712
721
  static readonly typeName = "babelconnect.v1.AgentInfo";
@@ -990,7 +999,9 @@ export declare class WrapUpStatus extends Message<WrapUpStatus> {
990
999
  * open the client receives a snapshot; thereafter partial patches. `seq` is
991
1000
  * monotonic across snapshot+patch — a gap means the client should resubscribe
992
1001
  * for a fresh snapshot. `error` is an out-of-band notice (command rejection /
993
- * server warning) and does NOT advance `seq`.
1002
+ * server warning) and does NOT advance `seq`. `keepalive` is an empty transport
1003
+ * heartbeat (see Keepalive) that likewise does NOT advance `seq` and carries no
1004
+ * state — client caches must ignore it.
994
1005
  *
995
1006
  * @generated from message babelconnect.v1.StateUpdate
996
1007
  */
@@ -1020,6 +1031,12 @@ export declare class StateUpdate extends Message<StateUpdate> {
1020
1031
  */
1021
1032
  value: Error;
1022
1033
  case: "error";
1034
+ } | {
1035
+ /**
1036
+ * @generated from field: babelconnect.v1.Keepalive keepalive = 5;
1037
+ */
1038
+ value: Keepalive;
1039
+ case: "keepalive";
1023
1040
  } | {
1024
1041
  case: undefined;
1025
1042
  value?: undefined;
@@ -1033,6 +1050,26 @@ export declare class StateUpdate extends Message<StateUpdate> {
1033
1050
  static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): StateUpdate;
1034
1051
  static equals(a: StateUpdate | PlainMessage<StateUpdate> | undefined, b: StateUpdate | PlainMessage<StateUpdate> | undefined): boolean;
1035
1052
  }
1053
+ /**
1054
+ * Keepalive is an empty heartbeat frame sent on the Session/Subscribe streams so
1055
+ * idle connections through proxies/LBs are not closed as idle. Notably the AWS
1056
+ * Classic ELB in front of ingress-nginx (dev/prod EKS) has a 300s connection
1057
+ * idle timeout; without these frames a quiet agent's gRPC-web server-stream is
1058
+ * killed after 5 min. The server sends one every ~15s. It carries no state and
1059
+ * does NOT advance `seq`; client caches treat a keepalive StateUpdate as a no-op.
1060
+ *
1061
+ * @generated from message babelconnect.v1.Keepalive
1062
+ */
1063
+ export declare class Keepalive extends Message<Keepalive> {
1064
+ constructor(data?: PartialMessage<Keepalive>);
1065
+ static readonly runtime: typeof proto3;
1066
+ static readonly typeName = "babelconnect.v1.Keepalive";
1067
+ static readonly fields: FieldList;
1068
+ static fromBinary(bytes: Uint8Array, options?: Partial<BinaryReadOptions>): Keepalive;
1069
+ static fromJson(jsonValue: JsonValue, options?: Partial<JsonReadOptions>): Keepalive;
1070
+ static fromJsonString(jsonString: string, options?: Partial<JsonReadOptions>): Keepalive;
1071
+ static equals(a: Keepalive | PlainMessage<Keepalive> | undefined, b: Keepalive | PlainMessage<Keepalive> | undefined): boolean;
1072
+ }
1036
1073
  /**
1037
1074
  * Patch is an entity-level delta applied mechanically by the client cache:
1038
1075
  * replace the agent block, upsert/remove a call by id, or set wrap-up.
@@ -998,6 +998,15 @@ export class AgentInfo extends Message {
998
998
  * @generated from field: string account_name = 17;
999
999
  */
1000
1000
  accountName = "";
1001
+ /**
1002
+ * Involuntary line block: the ACD/platform marked the agent busy / unreachable /
1003
+ * declined (a recoverable state cleared via ResetLineStatus) — distinct from a
1004
+ * chosen "busy" presence the agent (or sign-out-as-busy) selected. Drives the
1005
+ * line-blocked banner + Reset; a voluntary busy presence leaves this false.
1006
+ *
1007
+ * @generated from field: bool line_blocked = 18;
1008
+ */
1009
+ lineBlocked = false;
1001
1010
  constructor(data) {
1002
1011
  super();
1003
1012
  proto3.util.initPartial(data, this);
@@ -1022,6 +1031,7 @@ export class AgentInfo extends Message {
1022
1031
  { no: 15, name: "username", kind: "scalar", T: 9 /* ScalarType.STRING */ },
1023
1032
  { no: 16, name: "account_id", kind: "scalar", T: 9 /* ScalarType.STRING */ },
1024
1033
  { no: 17, name: "account_name", kind: "scalar", T: 9 /* ScalarType.STRING */ },
1034
+ { no: 18, name: "line_blocked", kind: "scalar", T: 8 /* ScalarType.BOOL */ },
1025
1035
  ]);
1026
1036
  static fromBinary(bytes, options) {
1027
1037
  return new AgentInfo().fromBinary(bytes, options);
@@ -1428,7 +1438,9 @@ export class WrapUpStatus extends Message {
1428
1438
  * open the client receives a snapshot; thereafter partial patches. `seq` is
1429
1439
  * monotonic across snapshot+patch — a gap means the client should resubscribe
1430
1440
  * for a fresh snapshot. `error` is an out-of-band notice (command rejection /
1431
- * server warning) and does NOT advance `seq`.
1441
+ * server warning) and does NOT advance `seq`. `keepalive` is an empty transport
1442
+ * heartbeat (see Keepalive) that likewise does NOT advance `seq` and carries no
1443
+ * state — client caches must ignore it.
1432
1444
  *
1433
1445
  * @generated from message babelconnect.v1.StateUpdate
1434
1446
  */
@@ -1452,6 +1464,7 @@ export class StateUpdate extends Message {
1452
1464
  { no: 2, name: "snapshot", kind: "message", T: AgentView, oneof: "update" },
1453
1465
  { no: 3, name: "patch", kind: "message", T: Patch, oneof: "update" },
1454
1466
  { no: 4, name: "error", kind: "message", T: Error, oneof: "update" },
1467
+ { no: 5, name: "keepalive", kind: "message", T: Keepalive, oneof: "update" },
1455
1468
  ]);
1456
1469
  static fromBinary(bytes, options) {
1457
1470
  return new StateUpdate().fromBinary(bytes, options);
@@ -1466,6 +1479,37 @@ export class StateUpdate extends Message {
1466
1479
  return proto3.util.equals(StateUpdate, a, b);
1467
1480
  }
1468
1481
  }
1482
+ /**
1483
+ * Keepalive is an empty heartbeat frame sent on the Session/Subscribe streams so
1484
+ * idle connections through proxies/LBs are not closed as idle. Notably the AWS
1485
+ * Classic ELB in front of ingress-nginx (dev/prod EKS) has a 300s connection
1486
+ * idle timeout; without these frames a quiet agent's gRPC-web server-stream is
1487
+ * killed after 5 min. The server sends one every ~15s. It carries no state and
1488
+ * does NOT advance `seq`; client caches treat a keepalive StateUpdate as a no-op.
1489
+ *
1490
+ * @generated from message babelconnect.v1.Keepalive
1491
+ */
1492
+ export class Keepalive extends Message {
1493
+ constructor(data) {
1494
+ super();
1495
+ proto3.util.initPartial(data, this);
1496
+ }
1497
+ static runtime = proto3;
1498
+ static typeName = "babelconnect.v1.Keepalive";
1499
+ static fields = proto3.util.newFieldList(() => []);
1500
+ static fromBinary(bytes, options) {
1501
+ return new Keepalive().fromBinary(bytes, options);
1502
+ }
1503
+ static fromJson(jsonValue, options) {
1504
+ return new Keepalive().fromJson(jsonValue, options);
1505
+ }
1506
+ static fromJsonString(jsonString, options) {
1507
+ return new Keepalive().fromJsonString(jsonString, options);
1508
+ }
1509
+ static equals(a, b) {
1510
+ return proto3.util.equals(Keepalive, a, b);
1511
+ }
1512
+ }
1469
1513
  /**
1470
1514
  * Patch is an entity-level delta applied mechanically by the client cache:
1471
1515
  * replace the agent block, upsert/remove a call by id, or set wrap-up.
package/dist/index.d.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  export { BabelconnectClient, type ConnectOptions } from "./client.js";
13
13
  export { StateCache } from "./state-cache.js";
14
- export { passwordGrant } from "./auth.js";
14
+ export { passwordGrant, revokeToken, pkceChallenge, buildAuthorizeUrl, authorizationCodeGrant, DEFAULT_CLIENT_ID, type TokenResponse, type PkceChallenge, type AuthorizeUrlOptions, } from "./auth.js";
15
15
  export { BrowserWebrtcMedia, browserMediaFactory, type Media, type MediaFactory } from "./media.js";
16
16
  export * from "./gen/babelconnect/v1/babelconnect_pb.js";
17
17
  export { Agent } from "./gen/babelconnect/v1/babelconnect_connect.js";
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@
11
11
  */
12
12
  export { BabelconnectClient } from "./client.js";
13
13
  export { StateCache } from "./state-cache.js";
14
- export { passwordGrant } from "./auth.js";
14
+ export { passwordGrant, revokeToken, pkceChallenge, buildAuthorizeUrl, authorizationCodeGrant, DEFAULT_CLIENT_ID, } from "./auth.js";
15
15
  export { BrowserWebrtcMedia, browserMediaFactory } from "./media.js";
16
16
  // The generated babelconnect.v1 messages + enums (AgentView, CallState, Command, …)
17
17
  // and the Agent service descriptor.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelforce/babelconnect-sdk",
3
- "version": "0.6.1",
3
+ "version": "0.7.1",
4
4
  "description": "TypeScript SDK for babelconnect — server-authoritative agent state over gRPC-web, native WebRTC audio, and an embeddable widget (iframe + postMessage) for the Flutter web app.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://babelforce.github.io/babelconnect-sdk/",