@babelforce/babelconnect-sdk 0.1.0 → 0.7.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
@@ -22,7 +22,7 @@ messaging) on top of `babelconnect-server`. Two ways to use it:
22
22
  npm install @babelforce/babelconnect-sdk
23
23
  ```
24
24
 
25
- ESM-only, targets ES2022. Runs in modern browsers; Node 18+ for control-only (no-audio) use. Native WebRTC
25
+ ESM-only, targets ES2022. Runs in modern browsers; Node 20+ for control-only (no-audio) use. Native WebRTC
26
26
  audio requires a browser (microphone permission).
27
27
 
28
28
  ## Quickstart — programmatic client (with audio)
@@ -90,8 +90,10 @@ bc.session.set({ number: "+49301234567" }); // correlate / prefill
90
90
  (`setPresence`, `setDisplayAs`, `setAgentNumber`, `setWebrtc`), and history/contacts fetches (`getHistory`,
91
91
  `getSmsThread`, `getPhonebook`).
92
92
 
93
- Full API reference and the embedding guide:
94
- **[babelconnect-sdk-docs](https://babelforce.github.io/babelconnect-sdk-docs/)**.
93
+ Full docs — API reference, guides, and the embedding guide:
94
+ **[babelconnect SDK docs](https://babelforce.github.io/babelconnect-sdk/)**. Good starting points:
95
+ the **[tutorial](https://babelforce.github.io/babelconnect-sdk/docs/tutorial/first-softphone)** and,
96
+ if you hit a snag, **[troubleshooting](https://babelforce.github.io/babelconnect-sdk/docs/guides/troubleshooting)**.
95
97
 
96
98
  ## How it works
97
99
 
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.d.ts CHANGED
@@ -8,7 +8,7 @@ export interface ConnectOptions {
8
8
  token: string;
9
9
  /** Per-call WebRTC leg. Defaults to {@link browserMediaFactory}; pass `null` for a control-only client. */
10
10
  mediaFactory?: MediaFactory | null;
11
- /** Auto-answer the agent's own OUTBOUND ringing leg (default `true`). INBOUND waits for {@link answerCall}. */
11
+ /** Auto-answer the agent's own OUTBOUND ringing leg (default `true`). INBOUND waits for {@link BabelconnectClient.answerCall}. */
12
12
  autoAnswer?: boolean;
13
13
  /** Out-of-band server notices (command rejections) + local media failures. */
14
14
  onError?: (err: BcError) => void;
@@ -34,7 +34,18 @@ export declare class BabelconnectClient {
34
34
  private readonly pending;
35
35
  private closed;
36
36
  private constructor();
37
- /** Open the session and start mirroring server state. */
37
+ /**
38
+ * Open the session and start mirroring server state. Returns synchronously and queues intents until the
39
+ * stream is live, so you can subscribe and register immediately.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const bc = BabelconnectClient.connect({ serverUrl, token });
44
+ * bc.subscribe(render); // render(AgentView) on every update
45
+ * bc.register(); // announce reachability + load deployment data
46
+ * bc.placeCall("+15551234567"); // the result arrives as a callUpsert patch
47
+ * ```
48
+ */
38
49
  static connect(opts: ConnectOptions): BabelconnectClient;
39
50
  /** The current view (deep copy). */
40
51
  get view(): AgentView;
@@ -42,6 +53,12 @@ export declare class BabelconnectClient {
42
53
  subscribe(fn: (v: AgentView) => void): () => void;
43
54
  /** The first active call, or undefined. */
44
55
  activeCall(): CallState | undefined;
56
+ /**
57
+ * Announce the agent and load its deployment data (presence options, caller-ID numbers, phonebook, feature
58
+ * config) — call this once after {@link BabelconnectClient.subscribe}. It also marks the agent
59
+ * **WebRTC-reachable** on the backend, so a control-only client that takes calls should follow it with
60
+ * {@link BabelconnectClient.setWebrtc}(false) + {@link BabelconnectClient.setAgentNumber}.
61
+ */
45
62
  register(capabilities?: string[]): void;
46
63
  placeCall(to: string, opts?: {
47
64
  displayAsTo?: string;
@@ -51,10 +68,15 @@ export declare class BabelconnectClient {
51
68
  }): void;
52
69
  /** Manually answer a RINGING call by id (no-op under autoAnswer once already answered). */
53
70
  answerCall(callId: string): Promise<void>;
71
+ /** End (or reject) a call by id. */
54
72
  hangup(callId: string): void;
73
+ /** Mute or unmute the agent's own leg of a call. */
55
74
  mute(callId: string, on: boolean): void;
75
+ /** Put a call on hold or retrieve it. */
56
76
  hold(callId: string, on: boolean): void;
77
+ /** Send DTMF tones into the call (e.g. an IVR menu choice). Valid characters: `0`–`9`, `*`, `#`, `A`–`D`. */
57
78
  sendDigits(callId: string, digits: string): void;
79
+ /** Set the outbound caller ID the consumer sees (choose from `agent.availableNumbers`). */
58
80
  setDisplayAs(number: string): void;
59
81
  /** Switch presence (the selector): "available" or a configured pause reason (see `AgentInfo.presenceOptions`). */
60
82
  setPresence(name: string): void;
@@ -64,24 +86,40 @@ export declare class BabelconnectClient {
64
86
  agentId?: string;
65
87
  applicationId?: string;
66
88
  }): void;
89
+ /** Open a conference around the current call. `hold` parks that call while you add members and consult. */
67
90
  startConference(hold?: boolean): void;
91
+ /** Invite a participant — pass exactly one of `agentId` or `number`. Starts a conference first if none is active. */
68
92
  addConferenceMember(opts: {
69
93
  agentId?: string;
70
94
  number?: string;
71
95
  }): void;
96
+ /** Remove a member from the conference (moderator only). */
72
97
  kickConferenceMember(memberId: string): void;
98
+ /** Hold or unhold an individual conference member (moderator only). */
73
99
  holdConferenceMember(memberId: string, on: boolean): void;
100
+ /** Mute or unmute an individual conference member (moderator only). */
74
101
  muteConferenceMember(memberId: string, on: boolean): void;
102
+ /** End the whole conference for everyone (moderator only). */
75
103
  endConference(): void;
104
+ /** Drop only the agent's own leg; the other members stay connected. */
76
105
  leaveConference(): void;
106
+ /** Add seconds to the after-call-work countdown (default 30). Show only when `wrapUp.canExtend`. */
77
107
  wrapUpExtend(seconds?: number): void;
108
+ /** End after-call work early. Show only when `wrapUp.canCancel`. */
78
109
  wrapUpCancel(): void;
110
+ /** Clear a blocked line (busy / unreachable / declined) so new calls reach the agent again. */
79
111
  resetLineStatus(): void;
112
+ /** Begin recording the current call. Gated on `agent.canRecord`. */
80
113
  startRecording(callId: string): void;
114
+ /** Stop the call's active recording. */
81
115
  stopRecording(callId: string): void;
116
+ /** Toggle the "flagged" mark on the call's active recording. */
82
117
  flagRecording(callId: string): void;
118
+ /** Replace the tags on the call's active recording (choose from `agent.availableTags`). */
83
119
  setRecordingTags(callId: string, tags: string[]): void;
120
+ /** Turn the in-browser WebRTC phone on or off. With it off, set an agent number so calls bridge there. */
84
121
  setWebrtc(on: boolean): void;
122
+ /** Set the agent's external phone number; the backend bridges calls there when WebRTC is off. */
85
123
  setAgentNumber(number: string): void;
86
124
  /** Send an SMS. `from` may be empty (server picks the default); `session` carries CTI/embedding correlation. */
87
125
  sendSms(to: string, text: string, opts?: {
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
@@ -28,7 +28,18 @@ export class BabelconnectClient {
28
28
  });
29
29
  this.rpc = createClient(Agent, transport);
30
30
  }
31
- /** Open the session and start mirroring server state. */
31
+ /**
32
+ * Open the session and start mirroring server state. Returns synchronously and queues intents until the
33
+ * stream is live, so you can subscribe and register immediately.
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const bc = BabelconnectClient.connect({ serverUrl, token });
38
+ * bc.subscribe(render); // render(AgentView) on every update
39
+ * bc.register(); // announce reachability + load deployment data
40
+ * bc.placeCall("+15551234567"); // the result arrives as a callUpsert patch
41
+ * ```
42
+ */
32
43
  static connect(opts) {
33
44
  const c = new BabelconnectClient(opts);
34
45
  void c.runSubscribe();
@@ -47,8 +58,16 @@ export class BabelconnectClient {
47
58
  return this.cache.current.activeCalls[0];
48
59
  }
49
60
  // --- intents ---
61
+ /**
62
+ * Announce the agent and load its deployment data (presence options, caller-ID numbers, phonebook, feature
63
+ * config) — call this once after {@link BabelconnectClient.subscribe}. It also marks the agent
64
+ * **WebRTC-reachable** on the backend, so a control-only client that takes calls should follow it with
65
+ * {@link BabelconnectClient.setWebrtc}(false) + {@link BabelconnectClient.setAgentNumber}.
66
+ */
50
67
  register(capabilities = ["webrtc"]) {
51
- 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
+ }));
52
71
  }
53
72
  placeCall(to, opts = {}) {
54
73
  this.send(new Command({
@@ -70,24 +89,41 @@ export class BabelconnectClient {
70
89
  if (call)
71
90
  await this.doAnswer(call);
72
91
  }
92
+ /** End (or reject) a call by id. */
73
93
  hangup(callId) {
74
- 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
+ }));
75
97
  }
98
+ /** Mute or unmute the agent's own leg of a call. */
76
99
  mute(callId, on) {
77
- 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
+ }));
78
103
  }
104
+ /** Put a call on hold or retrieve it. */
79
105
  hold(callId, on) {
80
- 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
+ }));
81
109
  }
110
+ /** Send DTMF tones into the call (e.g. an IVR menu choice). Valid characters: `0`–`9`, `*`, `#`, `A`–`D`. */
82
111
  sendDigits(callId, digits) {
83
- 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
+ }));
84
115
  }
116
+ /** Set the outbound caller ID the consumer sees (choose from `agent.availableNumbers`). */
85
117
  setDisplayAs(number) {
86
- 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
+ }));
87
121
  }
88
122
  /** Switch presence (the selector): "available" or a configured pause reason (see `AgentInfo.presenceOptions`). */
89
123
  setPresence(name) {
90
- 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
+ }));
91
127
  }
92
128
  /** Transfer to exactly one of `to` (number), `agentId`, or `applicationId`; `warm` = attended. */
93
129
  transfer(callId, to, opts = {}) {
@@ -105,78 +141,165 @@ export class BabelconnectClient {
105
141
  }));
106
142
  }
107
143
  // --- Conferences ---
144
+ /** Open a conference around the current call. `hold` parks that call while you add members and consult. */
108
145
  startConference(hold = false) {
109
- 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
+ }));
110
152
  }
153
+ /** Invite a participant — pass exactly one of `agentId` or `number`. Starts a conference first if none is active. */
111
154
  addConferenceMember(opts) {
112
155
  this.send(new Command({
113
156
  command: {
114
157
  case: "addConferenceMember",
115
- value: new AddConferenceMember({ agentId: opts.agentId ?? "", number: opts.number ?? "" }),
158
+ value: new AddConferenceMember({
159
+ agentId: opts.agentId ?? "",
160
+ number: opts.number ?? "",
161
+ }),
116
162
  },
117
163
  }));
118
164
  }
165
+ /** Remove a member from the conference (moderator only). */
119
166
  kickConferenceMember(memberId) {
120
- 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
+ }));
121
173
  }
174
+ /** Hold or unhold an individual conference member (moderator only). */
122
175
  holdConferenceMember(memberId, on) {
123
- 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
+ }));
124
182
  }
183
+ /** Mute or unmute an individual conference member (moderator only). */
125
184
  muteConferenceMember(memberId, on) {
126
- 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
+ }));
127
191
  }
192
+ /** End the whole conference for everyone (moderator only). */
128
193
  endConference() {
129
- this.send(new Command({ command: { case: "endConference", value: new EndConference({}) } }));
194
+ this.send(new Command({
195
+ command: { case: "endConference", value: new EndConference({}) },
196
+ }));
130
197
  }
198
+ /** Drop only the agent's own leg; the other members stay connected. */
131
199
  leaveConference() {
132
- this.send(new Command({ command: { case: "leaveConference", value: new LeaveConference({}) } }));
200
+ this.send(new Command({
201
+ command: { case: "leaveConference", value: new LeaveConference({}) },
202
+ }));
133
203
  }
204
+ /** Add seconds to the after-call-work countdown (default 30). Show only when `wrapUp.canExtend`. */
134
205
  wrapUpExtend(seconds = 30) {
135
- 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
+ }));
136
209
  }
210
+ /** End after-call work early. Show only when `wrapUp.canCancel`. */
137
211
  wrapUpCancel() {
138
- this.send(new Command({ command: { case: "wrapUpCancel", value: new WrapUpCancel({}) } }));
212
+ this.send(new Command({
213
+ command: { case: "wrapUpCancel", value: new WrapUpCancel({}) },
214
+ }));
139
215
  }
216
+ /** Clear a blocked line (busy / unreachable / declined) so new calls reach the agent again. */
140
217
  resetLineStatus() {
141
- this.send(new Command({ command: { case: "resetLineStatus", value: new ResetLineStatus({}) } }));
218
+ this.send(new Command({
219
+ command: { case: "resetLineStatus", value: new ResetLineStatus({}) },
220
+ }));
142
221
  }
222
+ /** Begin recording the current call. Gated on `agent.canRecord`. */
143
223
  startRecording(callId) {
144
- 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
+ }));
145
230
  }
231
+ /** Stop the call's active recording. */
146
232
  stopRecording(callId) {
147
- 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
+ }));
148
239
  }
240
+ /** Toggle the "flagged" mark on the call's active recording. */
149
241
  flagRecording(callId) {
150
- 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
+ }));
151
248
  }
249
+ /** Replace the tags on the call's active recording (choose from `agent.availableTags`). */
152
250
  setRecordingTags(callId, tags) {
153
- 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
+ }));
154
257
  }
258
+ /** Turn the in-browser WebRTC phone on or off. With it off, set an agent number so calls bridge there. */
155
259
  setWebrtc(on) {
156
- 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
+ }));
157
263
  }
264
+ /** Set the agent's external phone number; the backend bridges calls there when WebRTC is off. */
158
265
  setAgentNumber(number) {
159
- 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
+ }));
160
272
  }
161
273
  /** Send an SMS. `from` may be empty (server picks the default); `session` carries CTI/embedding correlation. */
162
274
  sendSms(to, text, opts = {}) {
163
275
  this.send(new Command({
164
276
  command: {
165
277
  case: "sendSms",
166
- 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
+ }),
167
284
  },
168
285
  }));
169
286
  }
170
287
  /** Open (reopen) or close (resolve) an SMS conversation. */
171
288
  setConversationOpen(conversationId, open) {
172
289
  this.send(new Command({
173
- command: { case: "setConversationOpen", value: new SetConversationOpen({ conversationId, open }) },
290
+ command: {
291
+ case: "setConversationOpen",
292
+ value: new SetConversationOpen({ conversationId, open }),
293
+ },
174
294
  }));
175
295
  }
176
296
  /** Clear the unread count on an SMS conversation (the agent opened its thread). */
177
297
  markConversationRead(conversationId) {
178
298
  this.send(new Command({
179
- command: { case: "markConversationRead", value: new MarkConversationRead({ conversationId }) },
299
+ command: {
300
+ case: "markConversationRead",
301
+ value: new MarkConversationRead({ conversationId }),
302
+ },
180
303
  }));
181
304
  }
182
305
  /** Fetch a page of the agent's call history (the History tab). */
@@ -207,7 +330,9 @@ export class BabelconnectClient {
207
330
  // --- internals ---
208
331
  async runSubscribe() {
209
332
  try {
210
- 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
+ })) {
211
336
  this.onUpdate(u);
212
337
  }
213
338
  }
@@ -217,6 +342,10 @@ export class BabelconnectClient {
217
342
  }
218
343
  }
219
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;
220
349
  // First message = the snapshot ⇒ the session is registered server-side, so
221
350
  // flush intents queued during connect() (register(), an early dial, …).
222
351
  if (!this.ready) {
@@ -229,7 +358,8 @@ export class BabelconnectClient {
229
358
  this.opts.onError?.(u.update.value);
230
359
  return;
231
360
  }
232
- if (u.update.case === "patch" && u.update.value.change.case === "notification") {
361
+ if (u.update.case === "patch" &&
362
+ u.update.value.change.case === "notification") {
233
363
  this.opts.onNotification?.(u.update.value.change.value);
234
364
  return;
235
365
  }
@@ -263,9 +393,15 @@ export class BabelconnectClient {
263
393
  async doAnswer(call) {
264
394
  if (this.answering.has(call.id) || this.media.has(call.id))
265
395
  return;
266
- const factory = this.opts.mediaFactory === undefined ? browserMediaFactory : this.opts.mediaFactory;
396
+ const factory = this.opts.mediaFactory === undefined
397
+ ? browserMediaFactory
398
+ : this.opts.mediaFactory;
267
399
  if (!factory) {
268
- 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
+ }));
269
405
  return;
270
406
  }
271
407
  this.answering.add(call.id);
@@ -276,10 +412,19 @@ export class BabelconnectClient {
276
412
  // undefined so the Media's own fallback applies.
277
413
  const answerSdp = await media.answer(call.webrtcOffer, toRTCIceServers(call.iceServers));
278
414
  this.media.set(call.id, media);
279
- 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
+ }));
280
421
  }
281
422
  catch (e) {
282
- 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
+ }));
283
428
  }
284
429
  finally {
285
430
  this.answering.delete(call.id);
@@ -2,7 +2,7 @@
2
2
  * `@babelforce/babelconnect-sdk/embed` — embed the babelconnect agent app (the
3
3
  * server-served Flutter web app) into a host page via an `<iframe>` and a two-way
4
4
  * `postMessage` bridge. See the Embedding guide at
5
- * https://babelforce.github.io/babelconnect-sdk-docs/ for the full protocol.
5
+ * https://babelforce.github.io/babelconnect-sdk/ for the full protocol.
6
6
  * The embedded app talks only to babelconnect-server.
7
7
  *
8
8
  * @example
@@ -2,7 +2,7 @@
2
2
  * `@babelforce/babelconnect-sdk/embed` — embed the babelconnect agent app (the
3
3
  * server-served Flutter web app) into a host page via an `<iframe>` and a two-way
4
4
  * `postMessage` bridge. See the Embedding guide at
5
- * https://babelforce.github.io/babelconnect-sdk-docs/ for the full protocol.
5
+ * https://babelforce.github.io/babelconnect-sdk/ for the full protocol.
6
6
  * The embedded app talks only to babelconnect-server.
7
7
  *
8
8
  * @example
@@ -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.
@@ -1,6 +1,6 @@
1
1
  import { AgentView, type StateUpdate } from "./gen/babelconnect/v1/babelconnect_pb.js";
2
2
  /**
3
- * The client-side mirror of the server-authoritative {@link AgentView}.
3
+ * The client-side mirror of the server-authoritative `AgentView`.
4
4
  *
5
5
  * It applies `StateUpdate` snapshots and entity-level patches **mechanically** —
6
6
  * there is no domain logic here, by design: the server reduces, the client
@@ -1,6 +1,6 @@
1
1
  import { AgentView } from "./gen/babelconnect/v1/babelconnect_pb.js";
2
2
  /**
3
- * The client-side mirror of the server-authoritative {@link AgentView}.
3
+ * The client-side mirror of the server-authoritative `AgentView`.
4
4
  *
5
5
  * It applies `StateUpdate` snapshots and entity-level patches **mechanically** —
6
6
  * there is no domain logic here, by design: the server reduces, the client
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@babelforce/babelconnect-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.7.0",
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
- "homepage": "https://babelforce.github.io/babelconnect-sdk-docs/",
6
+ "homepage": "https://babelforce.github.io/babelconnect-sdk/",
7
7
  "type": "module",
8
8
  "sideEffects": false,
9
9
  "files": [
@@ -39,8 +39,8 @@
39
39
  "devDependencies": {
40
40
  "@bufbuild/protoc-gen-es": "^1.10.1",
41
41
  "@connectrpc/protoc-gen-connect-es": "^1.6.1",
42
- "typedoc": "^0.27.0",
43
- "typedoc-plugin-markdown": "^4.4.0",
42
+ "typedoc": "~0.27.0",
43
+ "typedoc-plugin-markdown": "4.4.0",
44
44
  "typescript": "^5.6.0"
45
45
  },
46
46
  "publishConfig": {