@babelforce/babelconnect-sdk 0.6.1 → 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/dist/auth.d.ts +76 -0
- package/dist/auth.js +96 -0
- package/dist/client.js +141 -34
- package/dist/gen/babelconnect/v1/babelconnect_pb.d.ts +38 -1
- package/dist/gen/babelconnect/v1/babelconnect_pb.js +45 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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({
|
|
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: {
|
|
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: {
|
|
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({}), {
|
|
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" &&
|
|
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
|
|
396
|
+
const factory = this.opts.mediaFactory === undefined
|
|
397
|
+
? browserMediaFactory
|
|
398
|
+
: this.opts.mediaFactory;
|
|
305
399
|
if (!factory) {
|
|
306
|
-
this.opts.onError?.(new BcError({
|
|
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({
|
|
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({
|
|
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.
|
|
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
6
|
"homepage": "https://babelforce.github.io/babelconnect-sdk/",
|