@gravity-rail/sdk 0.5.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.
Files changed (60) hide show
  1. package/dist/agui.cjs +78 -0
  2. package/dist/agui.cjs.map +1 -0
  3. package/dist/agui.d.cts +13 -0
  4. package/dist/agui.d.ts +13 -0
  5. package/dist/agui.js +48 -0
  6. package/dist/agui.js.map +1 -0
  7. package/dist/audio.cjs +223 -0
  8. package/dist/audio.cjs.map +1 -0
  9. package/dist/audio.d.cts +25 -0
  10. package/dist/audio.d.ts +25 -0
  11. package/dist/audio.js +147 -0
  12. package/dist/audio.js.map +1 -0
  13. package/dist/chunk-BSOOMHW6.js +62 -0
  14. package/dist/chunk-BSOOMHW6.js.map +1 -0
  15. package/dist/{chunk-XRKRVY46.js → chunk-PEWFZXNK.js} +194 -31
  16. package/dist/chunk-PEWFZXNK.js.map +1 -0
  17. package/dist/{index-BhUZfOAG.d.cts → client-CLxHFOeB.d.cts} +310 -1632
  18. package/dist/{index-fPswHAIF.d.ts → client-Dft3Kmkn.d.ts} +310 -1632
  19. package/dist/helpers.cjs +123 -0
  20. package/dist/helpers.cjs.map +1 -0
  21. package/dist/helpers.d.cts +28 -0
  22. package/dist/helpers.d.ts +28 -0
  23. package/dist/helpers.js +92 -0
  24. package/dist/helpers.js.map +1 -0
  25. package/dist/index-CeVDRf6l.d.cts +1607 -0
  26. package/dist/index-D1G8EKz2.d.ts +1607 -0
  27. package/dist/index.cjs +186 -30
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +3 -2
  30. package/dist/index.d.ts +3 -2
  31. package/dist/index.js +1 -1
  32. package/dist/{inquiries-DiTDrTG2.d.cts → inquiries-BIlL6Obb.d.cts} +28 -3
  33. package/dist/{inquiries-DiTDrTG2.d.ts → inquiries-BIlL6Obb.d.ts} +28 -3
  34. package/dist/internal.cjs +195 -30
  35. package/dist/internal.cjs.map +1 -1
  36. package/dist/internal.d.cts +4 -3
  37. package/dist/internal.d.ts +4 -3
  38. package/dist/internal.js +5 -1
  39. package/dist/oauth-client.cjs +78 -0
  40. package/dist/oauth-client.cjs.map +1 -0
  41. package/dist/oauth-client.d.cts +15 -0
  42. package/dist/oauth-client.d.ts +15 -0
  43. package/dist/oauth-client.js +48 -0
  44. package/dist/oauth-client.js.map +1 -0
  45. package/dist/realtime.cjs +637 -0
  46. package/dist/realtime.cjs.map +1 -0
  47. package/dist/realtime.d.cts +94 -0
  48. package/dist/realtime.d.ts +94 -0
  49. package/dist/realtime.js +580 -0
  50. package/dist/realtime.js.map +1 -0
  51. package/dist/schemas.cjs +7 -3
  52. package/dist/schemas.cjs.map +1 -1
  53. package/dist/schemas.d.cts +28 -6
  54. package/dist/schemas.d.ts +28 -6
  55. package/dist/schemas.js +6 -3
  56. package/dist/schemas.js.map +1 -1
  57. package/dist/types-DkYyrvoC.d.cts +19 -0
  58. package/dist/types-DkYyrvoC.d.ts +19 -0
  59. package/package.json +26 -1
  60. package/dist/chunk-XRKRVY46.js.map +0 -1
@@ -0,0 +1,48 @@
1
+ // src/oauth-client/pkce.ts
2
+ function bytesToBase64Url(bytes) {
3
+ let binary = "";
4
+ for (let i = 0; i < bytes.length; i += 1) {
5
+ binary += String.fromCharCode(bytes[i]);
6
+ }
7
+ return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
8
+ }
9
+ function generateOAuthState() {
10
+ const bytes = new Uint8Array(32);
11
+ crypto.getRandomValues(bytes);
12
+ return bytesToBase64Url(bytes);
13
+ }
14
+ function generateCodeVerifier() {
15
+ const bytes = new Uint8Array(32);
16
+ crypto.getRandomValues(bytes);
17
+ return bytesToBase64Url(bytes);
18
+ }
19
+ async function generateCodeChallenge(codeVerifier) {
20
+ const data = new TextEncoder().encode(codeVerifier);
21
+ const digest = await crypto.subtle.digest("SHA-256", data);
22
+ return bytesToBase64Url(new Uint8Array(digest));
23
+ }
24
+ function buildAuthorizationUrl(params) {
25
+ const search = new URLSearchParams({
26
+ response_type: "code",
27
+ client_id: params.clientId,
28
+ redirect_uri: params.redirectUri,
29
+ scope: params.scopes.join(" "),
30
+ state: params.state,
31
+ code_challenge: params.codeChallenge,
32
+ code_challenge_method: "S256"
33
+ });
34
+ if (params.workspaceUuid) {
35
+ search.set("workspace", params.workspaceUuid);
36
+ }
37
+ if (params.orgSlug) {
38
+ search.set("org", params.orgSlug);
39
+ }
40
+ return `${params.apiUrl}/api/v2/oauth/authorize?${search.toString()}`;
41
+ }
42
+ export {
43
+ buildAuthorizationUrl,
44
+ generateCodeChallenge,
45
+ generateCodeVerifier,
46
+ generateOAuthState
47
+ };
48
+ //# sourceMappingURL=oauth-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/oauth-client/pkce.ts"],"sourcesContent":["function bytesToBase64Url(bytes: Uint8Array): string {\n let binary = '';\n for (let i = 0; i < bytes.length; i += 1) {\n binary += String.fromCharCode(bytes[i]);\n }\n return btoa(binary).replace(/\\+/g, '-').replace(/\\//g, '_').replace(/=+$/, '');\n}\n\nexport function generateOAuthState(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return bytesToBase64Url(bytes);\n}\n\nexport function generateCodeVerifier(): string {\n const bytes = new Uint8Array(32);\n crypto.getRandomValues(bytes);\n return bytesToBase64Url(bytes);\n}\n\nexport async function generateCodeChallenge(codeVerifier: string): Promise<string> {\n const data = new TextEncoder().encode(codeVerifier);\n const digest = await crypto.subtle.digest('SHA-256', data);\n return bytesToBase64Url(new Uint8Array(digest));\n}\n\nexport function buildAuthorizationUrl(params: {\n apiUrl: string;\n clientId: string;\n redirectUri: string;\n scopes: string[];\n state: string;\n codeChallenge: string;\n workspaceUuid?: string;\n orgSlug?: string;\n}): string {\n const search = new URLSearchParams({\n response_type: 'code',\n client_id: params.clientId,\n redirect_uri: params.redirectUri,\n scope: params.scopes.join(' '),\n state: params.state,\n code_challenge: params.codeChallenge,\n code_challenge_method: 'S256',\n });\n\n if (params.workspaceUuid) {\n search.set('workspace', params.workspaceUuid);\n }\n if (params.orgSlug) {\n search.set('org', params.orgSlug);\n }\n\n return `${params.apiUrl}/api/v2/oauth/authorize?${search.toString()}`;\n}\n"],"mappings":";AAAA,SAAS,iBAAiB,OAA2B;AAClD,MAAI,SAAS;AACb,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,GAAG;AACvC,cAAU,OAAO,aAAa,MAAM,CAAC,CAAC;AAAA,EACzC;AACA,SAAO,KAAK,MAAM,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,GAAG,EAAE,QAAQ,OAAO,EAAE;AAChF;AAEO,SAAS,qBAA6B;AAC1C,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,iBAAiB,KAAK;AAChC;AAEO,SAAS,uBAA+B;AAC5C,QAAM,QAAQ,IAAI,WAAW,EAAE;AAC/B,SAAO,gBAAgB,KAAK;AAC5B,SAAO,iBAAiB,KAAK;AAChC;AAEA,eAAsB,sBAAsB,cAAuC;AAChF,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,YAAY;AAClD,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI;AACzD,SAAO,iBAAiB,IAAI,WAAW,MAAM,CAAC;AACjD;AAEO,SAAS,sBAAsB,QAS3B;AACR,QAAM,SAAS,IAAI,gBAAgB;AAAA,IAChC,eAAe;AAAA,IACf,WAAW,OAAO;AAAA,IAClB,cAAc,OAAO;AAAA,IACrB,OAAO,OAAO,OAAO,KAAK,GAAG;AAAA,IAC7B,OAAO,OAAO;AAAA,IACd,gBAAgB,OAAO;AAAA,IACvB,uBAAuB;AAAA,EAC1B,CAAC;AAED,MAAI,OAAO,eAAe;AACvB,WAAO,IAAI,aAAa,OAAO,aAAa;AAAA,EAC/C;AACA,MAAI,OAAO,SAAS;AACjB,WAAO,IAAI,OAAO,OAAO,OAAO;AAAA,EACnC;AAEA,SAAO,GAAG,OAAO,MAAM,2BAA2B,OAAO,SAAS,CAAC;AACtE;","names":[]}
@@ -0,0 +1,637 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/realtime/index.ts
21
+ var realtime_exports = {};
22
+ __export(realtime_exports, {
23
+ OperatorSessionClient: () => OperatorSessionClient,
24
+ WorkspaceRealtimeClient: () => WorkspaceRealtimeClient,
25
+ buildWorkspaceWsUrl: () => buildWorkspaceWsUrl,
26
+ initWorkspaceSocket: () => initWorkspaceSocket,
27
+ isActiveCallPhase: () => isActiveCallPhase,
28
+ isWorkspaceSocketConnected: () => isWorkspaceSocketConnected,
29
+ onWorkspaceSocketConnection: () => onWorkspaceSocketConnection,
30
+ subscribeWorkspaceChannel: () => subscribeWorkspaceChannel
31
+ });
32
+ module.exports = __toCommonJS(realtime_exports);
33
+
34
+ // src/realtime/types.ts
35
+ function isActiveCallPhase(phase) {
36
+ return phase === "dialing" || phase === "live" || phase === "onHold";
37
+ }
38
+
39
+ // src/realtime/workspace-client.ts
40
+ var HEARTBEAT_INTERVAL_MS = 3e4;
41
+ var RECONNECT_DELAY_MS = 3e3;
42
+ var MAX_RECONNECT_ATTEMPTS = 5;
43
+ var CLOSE_DELAY_MS = 1e3;
44
+ function createSocketState() {
45
+ return {
46
+ ws: null,
47
+ connected: false,
48
+ connecting: false,
49
+ listeners: /* @__PURE__ */ new Map(),
50
+ subscribedChannels: /* @__PURE__ */ new Set(),
51
+ reconnectAttempts: 0,
52
+ reconnectTimer: null,
53
+ heartbeatTimer: null,
54
+ closeTimer: null,
55
+ intentionalClose: false
56
+ };
57
+ }
58
+ function buildWorkspaceWsUrl(session) {
59
+ const isSsl = session.apiUrl.startsWith("https");
60
+ const host = session.apiUrl.replace(/^https?:\/\//, "");
61
+ return `${isSsl ? "wss" : "ws"}://${host}/api/v2/w/${session.workspaceUuid}/ws/realtime`;
62
+ }
63
+ var WorkspaceRealtimeClient = class {
64
+ session = null;
65
+ getSessionFn;
66
+ state = createSocketState();
67
+ connectionListeners = /* @__PURE__ */ new Set();
68
+ constructor(config) {
69
+ if (typeof config === "function") {
70
+ this.getSessionFn = config;
71
+ } else {
72
+ this.getSessionFn = null;
73
+ if (config) {
74
+ this.session = config;
75
+ }
76
+ }
77
+ }
78
+ resolveSession() {
79
+ return this.session ?? this.getSessionFn?.() ?? null;
80
+ }
81
+ notifyConnectionListeners() {
82
+ for (const listener of this.connectionListeners) {
83
+ listener(this.state.connected);
84
+ }
85
+ }
86
+ clearCloseTimer() {
87
+ if (this.state.closeTimer) {
88
+ clearTimeout(this.state.closeTimer);
89
+ this.state.closeTimer = null;
90
+ }
91
+ }
92
+ setSession(session) {
93
+ const changed = this.session?.accessToken !== session.accessToken || this.session?.workspaceUuid !== session.workspaceUuid || this.session?.apiUrl !== session.apiUrl;
94
+ this.session = session;
95
+ if (changed && this.state.ws) {
96
+ this.closeConnection();
97
+ }
98
+ if (this.state.listeners.size > 0) {
99
+ this.ensureConnection();
100
+ }
101
+ }
102
+ subscribe(channel, listener) {
103
+ this.clearCloseTimer();
104
+ let listeners = this.state.listeners.get(channel);
105
+ if (!listeners) {
106
+ listeners = /* @__PURE__ */ new Set();
107
+ this.state.listeners.set(channel, listeners);
108
+ }
109
+ listeners.add(listener);
110
+ this.ensureConnection();
111
+ if (this.state.connected && this.state.ws?.readyState === WebSocket.OPEN && !this.state.subscribedChannels.has(channel)) {
112
+ this.state.ws.send(JSON.stringify({ type: "subscribe", channels: [channel] }));
113
+ }
114
+ return () => {
115
+ const channelListeners = this.state.listeners.get(channel);
116
+ channelListeners?.delete(listener);
117
+ if (channelListeners && channelListeners.size === 0) {
118
+ this.state.listeners.delete(channel);
119
+ this.state.subscribedChannels.delete(channel);
120
+ if (this.state.connected && this.state.ws?.readyState === WebSocket.OPEN) {
121
+ this.state.ws.send(JSON.stringify({ type: "unsubscribe", channels: [channel] }));
122
+ }
123
+ }
124
+ if (this.state.listeners.size === 0) {
125
+ this.state.closeTimer = setTimeout(() => {
126
+ if (this.state.listeners.size === 0) {
127
+ this.closeConnection();
128
+ }
129
+ }, CLOSE_DELAY_MS);
130
+ }
131
+ };
132
+ }
133
+ send(channel, data) {
134
+ if (!this.state.connected || !this.state.ws) return;
135
+ this.state.ws.send(JSON.stringify({ type: "message", channel, data }));
136
+ }
137
+ onConnection(listener) {
138
+ this.connectionListeners.add(listener);
139
+ listener(this.state.connected);
140
+ return () => {
141
+ this.connectionListeners.delete(listener);
142
+ };
143
+ }
144
+ isConnected() {
145
+ return this.state.connected;
146
+ }
147
+ ensureConnection() {
148
+ const session = this.resolveSession();
149
+ if (!session) return;
150
+ if (typeof window === "undefined" || typeof WebSocket === "undefined") return;
151
+ if (this.state.ws || this.state.connecting) return;
152
+ this.clearCloseTimer();
153
+ this.state.connecting = true;
154
+ this.state.intentionalClose = false;
155
+ const ws = new WebSocket(buildWorkspaceWsUrl(session));
156
+ ws.onopen = () => {
157
+ ws.send(JSON.stringify({ type: "login", token: session.accessToken }));
158
+ };
159
+ ws.onmessage = (event) => {
160
+ try {
161
+ const msg = JSON.parse(String(event.data));
162
+ if (msg.type === "protocol.init" && !this.state.connected) {
163
+ this.state.connected = true;
164
+ this.state.connecting = false;
165
+ this.state.reconnectAttempts = 0;
166
+ if (this.state.heartbeatTimer) clearInterval(this.state.heartbeatTimer);
167
+ this.state.heartbeatTimer = setInterval(() => {
168
+ if (this.state.ws?.readyState === WebSocket.OPEN) {
169
+ this.state.ws.send(JSON.stringify({ type: "heartbeat" }));
170
+ }
171
+ }, HEARTBEAT_INTERVAL_MS);
172
+ if (this.state.listeners.size > 0) {
173
+ ws.send(
174
+ JSON.stringify({
175
+ type: "subscribe",
176
+ channels: Array.from(this.state.listeners.keys())
177
+ })
178
+ );
179
+ }
180
+ this.notifyConnectionListeners();
181
+ return;
182
+ }
183
+ if (msg.type === "subscribed" && Array.isArray(msg.channels)) {
184
+ for (const ch of msg.channels) {
185
+ this.state.subscribedChannels.add(ch);
186
+ }
187
+ return;
188
+ }
189
+ if (msg.type === "unsubscribed" && Array.isArray(msg.channels)) {
190
+ for (const ch of msg.channels) {
191
+ this.state.subscribedChannels.delete(ch);
192
+ }
193
+ return;
194
+ }
195
+ if (msg.type === "event" && msg.channel && msg.event) {
196
+ const channelListeners = this.state.listeners.get(msg.channel);
197
+ if (channelListeners) {
198
+ for (const channelListener of channelListeners) {
199
+ try {
200
+ channelListener(msg.event);
201
+ } catch (err) {
202
+ console.error("Channel listener error:", err);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ } catch {
208
+ }
209
+ };
210
+ ws.onclose = () => {
211
+ this.state.connected = false;
212
+ this.state.connecting = false;
213
+ this.state.ws = null;
214
+ this.state.subscribedChannels.clear();
215
+ if (this.state.heartbeatTimer) {
216
+ clearInterval(this.state.heartbeatTimer);
217
+ this.state.heartbeatTimer = null;
218
+ }
219
+ this.notifyConnectionListeners();
220
+ if (!this.state.intentionalClose && this.state.listeners.size > 0 && this.state.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
221
+ this.state.reconnectAttempts += 1;
222
+ this.state.reconnectTimer = setTimeout(() => {
223
+ this.ensureConnection();
224
+ }, RECONNECT_DELAY_MS * this.state.reconnectAttempts);
225
+ }
226
+ };
227
+ ws.onerror = () => {
228
+ };
229
+ this.state.ws = ws;
230
+ }
231
+ closeConnection() {
232
+ this.state.intentionalClose = true;
233
+ this.clearCloseTimer();
234
+ if (this.state.reconnectTimer) {
235
+ clearTimeout(this.state.reconnectTimer);
236
+ this.state.reconnectTimer = null;
237
+ }
238
+ if (this.state.heartbeatTimer) {
239
+ clearInterval(this.state.heartbeatTimer);
240
+ this.state.heartbeatTimer = null;
241
+ }
242
+ this.state.ws?.close();
243
+ this.state.ws = null;
244
+ this.state.connected = false;
245
+ this.state.connecting = false;
246
+ this.state.subscribedChannels.clear();
247
+ this.notifyConnectionListeners();
248
+ }
249
+ };
250
+ var defaultClient = new WorkspaceRealtimeClient();
251
+ function initWorkspaceSocket(session) {
252
+ defaultClient.setSession(session);
253
+ }
254
+ function subscribeWorkspaceChannel(channel, listener) {
255
+ return defaultClient.subscribe(channel, listener);
256
+ }
257
+ function onWorkspaceSocketConnection(listener) {
258
+ return defaultClient.onConnection(listener);
259
+ }
260
+ function isWorkspaceSocketConnected() {
261
+ return defaultClient.isConnected();
262
+ }
263
+
264
+ // src/audio/pcm.ts
265
+ var SERVER_SAMPLE_RATE = 24e3;
266
+ var TARGET_SAMPLE_RATE = 24e3;
267
+ function float32ArrayToBase64(float32Array) {
268
+ const int16Array = new Int16Array(float32Array.length);
269
+ for (let i = 0; i < float32Array.length; i += 1) {
270
+ const sample = Math.max(-1, Math.min(1, float32Array[i]));
271
+ int16Array[i] = Math.round(sample * 32767);
272
+ }
273
+ const uint8Array = new Uint8Array(int16Array.buffer);
274
+ let binaryString = "";
275
+ for (let i = 0; i < uint8Array.length; i += 1) {
276
+ binaryString += String.fromCharCode(uint8Array[i]);
277
+ }
278
+ return btoa(binaryString);
279
+ }
280
+ function base64ToFloat32Array(base64) {
281
+ const binaryString = atob(base64);
282
+ const len = binaryString.length / 2;
283
+ const float32Array = new Float32Array(len);
284
+ for (let i = 0; i < len; i += 1) {
285
+ const lo = binaryString.charCodeAt(i * 2);
286
+ const hi = binaryString.charCodeAt(i * 2 + 1);
287
+ let int16 = hi << 8 | lo;
288
+ if (int16 >= 32768) int16 -= 65536;
289
+ float32Array[i] = int16 / 32768;
290
+ }
291
+ return float32Array;
292
+ }
293
+
294
+ // src/realtime/operator-session-client.ts
295
+ var OperatorSessionClient = class {
296
+ constructor(handlers = {}) {
297
+ this.handlers = handlers;
298
+ }
299
+ ws = null;
300
+ audioContext = null;
301
+ mediaStream = null;
302
+ processor = null;
303
+ sessionState = "idle";
304
+ callPhase = "idle";
305
+ nextPlaybackTime = 0;
306
+ playbackSources = [];
307
+ micActive = false;
308
+ onHold = false;
309
+ receivedRemoteAudio = false;
310
+ heartbeatTimer = null;
311
+ sessionCloseNotified = false;
312
+ intentionalDisconnect = false;
313
+ getSessionState() {
314
+ return this.sessionState;
315
+ }
316
+ getCallPhase() {
317
+ return this.callPhase;
318
+ }
319
+ setSessionState(next) {
320
+ this.sessionState = next;
321
+ this.handlers.onSessionStateChange?.(next);
322
+ }
323
+ setCallPhase(next) {
324
+ this.callPhase = next;
325
+ this.handlers.onCallPhaseChange?.(next);
326
+ }
327
+ notifySessionClosed(reason) {
328
+ if (this.sessionCloseNotified) return;
329
+ this.sessionCloseNotified = true;
330
+ this.handlers.onSessionClosed?.(reason);
331
+ }
332
+ async connect(session, chatId) {
333
+ if (typeof window === "undefined" || typeof WebSocket === "undefined") {
334
+ throw new Error("Operator session WebSocket is not available in this environment");
335
+ }
336
+ await this.disconnect();
337
+ this.sessionCloseNotified = false;
338
+ this.setSessionState("connecting");
339
+ this.setCallPhase("connecting");
340
+ const isSsl = session.apiUrl.startsWith("https");
341
+ const host = session.apiUrl.replace(/^https?:\/\//, "");
342
+ const wsUrl = `${isSsl ? "wss" : "ws"}://${host}/api/v2/w/${session.workspaceUuid}/operator/chats/${chatId}/session-ws`;
343
+ await new Promise((resolve, reject) => {
344
+ const ws = new WebSocket(wsUrl);
345
+ this.ws = ws;
346
+ ws.onopen = () => {
347
+ ws.send(JSON.stringify({ type: "login", token: session.accessToken }));
348
+ };
349
+ ws.onmessage = (event) => {
350
+ try {
351
+ const msg = JSON.parse(String(event.data));
352
+ this.handleMessage(msg, resolve, reject);
353
+ } catch {
354
+ }
355
+ };
356
+ ws.onerror = () => {
357
+ const message = "Operator session WebSocket error";
358
+ const wasConnecting = this.sessionState === "connecting";
359
+ this.setSessionState("error");
360
+ if (wasConnecting) {
361
+ this.setCallPhase("idle");
362
+ reject(new Error(message));
363
+ } else {
364
+ this.setCallPhase("failed");
365
+ this.handlers.onError?.(message);
366
+ }
367
+ };
368
+ ws.onclose = () => {
369
+ this.clearHeartbeat();
370
+ this.stopMicrophone();
371
+ this.stopPlayback();
372
+ const wasConnected = this.sessionState === "connected";
373
+ this.setSessionState("idle");
374
+ this.setCallPhase("idle");
375
+ if (wasConnected && !this.intentionalDisconnect) {
376
+ this.notifySessionClosed("disconnect");
377
+ }
378
+ this.intentionalDisconnect = false;
379
+ };
380
+ });
381
+ }
382
+ handleMessage(msg, resolveLogin, rejectLogin) {
383
+ if (msg.type === "RUN_ERROR" || msg.type === "error") {
384
+ const message = msg.message ?? "Operator session error";
385
+ if (this.sessionState === "connecting") {
386
+ this.setSessionState("error");
387
+ rejectLogin(new Error(message));
388
+ } else {
389
+ this.setCallPhase("failed");
390
+ this.handlers.onError?.(message);
391
+ }
392
+ return;
393
+ }
394
+ if (msg.type === "protocol.init" && this.sessionState === "connecting") {
395
+ return;
396
+ }
397
+ if (msg.type === "CUSTOM" && msg.name === "operator.session_ready") {
398
+ this.setSessionState("connected");
399
+ this.setCallPhase("ready");
400
+ this.startHeartbeat();
401
+ resolveLogin();
402
+ return;
403
+ }
404
+ if (msg.type === "event" && msg.event) {
405
+ if (this.handleRemoteVoiceAudio(msg.event)) {
406
+ return;
407
+ }
408
+ this.handlers.onAguiEvent?.(msg.event);
409
+ return;
410
+ }
411
+ if (msg.type === "CUSTOM" && msg.name === "operator.call.started") {
412
+ this.receivedRemoteAudio = false;
413
+ this.setCallPhase("dialing");
414
+ return;
415
+ }
416
+ if (msg.type === "CUSTOM" && msg.name === "operator.call.ended") {
417
+ this.onHold = false;
418
+ this.receivedRemoteAudio = false;
419
+ this.stopMicrophone();
420
+ this.stopPlayback();
421
+ const endSession = msg.value?.end_session === true;
422
+ const reason = typeof msg.value?.reason === "string" ? msg.value.reason : void 0;
423
+ if (endSession) {
424
+ this.setCallPhase("idle");
425
+ this.notifySessionClosed(reason ?? "peer_hangup");
426
+ } else {
427
+ this.setCallPhase("ready");
428
+ }
429
+ return;
430
+ }
431
+ }
432
+ /** Play callee PCM from wrapped chat-stream events; returns true if handled. */
433
+ handleRemoteVoiceAudio(event) {
434
+ if (event.type !== "CUSTOM" || event.name !== "voice.audio") {
435
+ return false;
436
+ }
437
+ if (this.onHold) return true;
438
+ const value = event.value;
439
+ if (!value || typeof value !== "object") return true;
440
+ const data = value.data;
441
+ if (typeof data !== "string" || !data) return true;
442
+ if (!this.receivedRemoteAudio && (this.callPhase === "dialing" || this.callPhase === "live")) {
443
+ this.receivedRemoteAudio = true;
444
+ this.setCallPhase("live");
445
+ }
446
+ void this.playPcm16(base64ToFloat32Array(data));
447
+ return true;
448
+ }
449
+ startHeartbeat() {
450
+ this.clearHeartbeat();
451
+ this.heartbeatTimer = setInterval(() => {
452
+ if (this.ws?.readyState === WebSocket.OPEN) {
453
+ this.ws.send(JSON.stringify({ type: "heartbeat" }));
454
+ }
455
+ }, 3e4);
456
+ }
457
+ clearHeartbeat() {
458
+ if (this.heartbeatTimer) {
459
+ clearInterval(this.heartbeatTimer);
460
+ this.heartbeatTimer = null;
461
+ }
462
+ }
463
+ sendCustom(name, value = {}) {
464
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
465
+ throw new Error("Operator session WebSocket is not connected");
466
+ }
467
+ this.ws.send(JSON.stringify({ type: "CUSTOM", name, value }));
468
+ }
469
+ async placeCall(memberId) {
470
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
471
+ throw new Error("Operator session WebSocket is not connected");
472
+ }
473
+ if (this.sessionState !== "connected") {
474
+ throw new Error("Operator session is not ready");
475
+ }
476
+ await this.startMicrophone();
477
+ this.sendCustom("operator.call.place", { member_id: memberId });
478
+ }
479
+ hangUp() {
480
+ try {
481
+ this.sendCustom("operator.call.hangup");
482
+ } catch {
483
+ }
484
+ this.onHold = false;
485
+ this.receivedRemoteAudio = false;
486
+ }
487
+ forwardCall(phoneNumber) {
488
+ this.receivedRemoteAudio = false;
489
+ this.setCallPhase("dialing");
490
+ this.sendCustom("operator.call.forward", { phone_number: phoneNumber });
491
+ }
492
+ setHold(onHold) {
493
+ this.onHold = onHold;
494
+ this.sendCustom("operator.call.hold", { on_hold: onHold });
495
+ if (this.callPhase === "live" || this.callPhase === "onHold") {
496
+ this.setCallPhase(onHold ? "onHold" : "live");
497
+ }
498
+ if (onHold) {
499
+ this.stopPlayback();
500
+ }
501
+ }
502
+ async startMicrophone() {
503
+ if (typeof window === "undefined") return;
504
+ if (!navigator.mediaDevices?.getUserMedia) {
505
+ throw new Error("Microphone not available in this browser");
506
+ }
507
+ if (this.micActive) return;
508
+ this.mediaStream = await navigator.mediaDevices.getUserMedia({
509
+ audio: {
510
+ echoCancellation: true,
511
+ noiseSuppression: true,
512
+ autoGainControl: true
513
+ }
514
+ });
515
+ const Ctx = window.AudioContext ?? window.webkitAudioContext;
516
+ if (!Ctx) throw new Error("Web Audio not supported");
517
+ this.audioContext = new Ctx();
518
+ if (this.audioContext.state === "suspended") {
519
+ await this.audioContext.resume();
520
+ }
521
+ const source = this.audioContext.createMediaStreamSource(this.mediaStream);
522
+ const processor = this.audioContext.createScriptProcessor(4096, 1, 1);
523
+ this.processor = processor;
524
+ processor.onaudioprocess = (event) => {
525
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
526
+ if (this.onHold) return;
527
+ if (this.callPhase !== "dialing" && this.callPhase !== "live" && this.callPhase !== "onHold") {
528
+ return;
529
+ }
530
+ const input = event.inputBuffer.getChannelData(0);
531
+ const copy = new Float32Array(input.length);
532
+ copy.set(input);
533
+ const sampleRate = this.audioContext?.sampleRate ?? TARGET_SAMPLE_RATE;
534
+ let pcm = copy;
535
+ if (sampleRate !== TARGET_SAMPLE_RATE) {
536
+ const ratio = sampleRate / TARGET_SAMPLE_RATE;
537
+ const newLen = Math.floor(copy.length / ratio);
538
+ const down = new Float32Array(newLen);
539
+ for (let i = 0; i < newLen; i += 1) {
540
+ down[i] = copy[Math.floor(i * ratio)];
541
+ }
542
+ pcm = down;
543
+ }
544
+ this.ws.send(
545
+ JSON.stringify({
546
+ type: "CUSTOM",
547
+ name: "voice.audio",
548
+ value: {
549
+ data: float32ArrayToBase64(pcm),
550
+ format: "pcm16",
551
+ sample_rate: TARGET_SAMPLE_RATE
552
+ }
553
+ })
554
+ );
555
+ };
556
+ source.connect(processor);
557
+ processor.connect(this.audioContext.destination);
558
+ this.micActive = true;
559
+ }
560
+ stopMicrophone() {
561
+ this.processor?.disconnect();
562
+ this.processor = null;
563
+ for (const track of this.mediaStream?.getTracks() ?? []) {
564
+ track.stop();
565
+ }
566
+ this.mediaStream = null;
567
+ this.micActive = false;
568
+ }
569
+ async playPcm16(samples) {
570
+ if (typeof window === "undefined") return;
571
+ const Ctx = window.AudioContext ?? window.webkitAudioContext;
572
+ if (!Ctx) return;
573
+ if (!this.audioContext || this.audioContext.state === "closed") {
574
+ this.audioContext = new Ctx();
575
+ }
576
+ if (this.audioContext.state === "suspended") {
577
+ await this.audioContext.resume();
578
+ }
579
+ const buffer = this.audioContext.createBuffer(1, samples.length, SERVER_SAMPLE_RATE);
580
+ buffer.copyToChannel(new Float32Array(samples), 0);
581
+ const source = this.audioContext.createBufferSource();
582
+ source.buffer = buffer;
583
+ source.connect(this.audioContext.destination);
584
+ const now = this.audioContext.currentTime;
585
+ const startAt = this.nextPlaybackTime < now + 0.02 ? now + 0.02 : this.nextPlaybackTime;
586
+ source.start(startAt);
587
+ this.nextPlaybackTime = startAt + buffer.duration;
588
+ this.playbackSources.push(source);
589
+ source.onended = () => {
590
+ const idx = this.playbackSources.indexOf(source);
591
+ if (idx >= 0) this.playbackSources.splice(idx, 1);
592
+ };
593
+ }
594
+ stopPlayback() {
595
+ for (const src of this.playbackSources) {
596
+ try {
597
+ src.stop();
598
+ } catch {
599
+ }
600
+ }
601
+ this.playbackSources = [];
602
+ this.nextPlaybackTime = 0;
603
+ }
604
+ async disconnect() {
605
+ this.intentionalDisconnect = true;
606
+ if (this.callPhase === "dialing" || this.callPhase === "live" || this.callPhase === "onHold") {
607
+ this.hangUp();
608
+ }
609
+ this.clearHeartbeat();
610
+ this.stopMicrophone();
611
+ this.stopPlayback();
612
+ if (this.ws) {
613
+ this.ws.close();
614
+ this.ws = null;
615
+ }
616
+ if (this.audioContext && this.audioContext.state !== "closed") {
617
+ await this.audioContext.close().catch(() => void 0);
618
+ }
619
+ this.audioContext = null;
620
+ this.onHold = false;
621
+ this.receivedRemoteAudio = false;
622
+ this.setSessionState("idle");
623
+ this.setCallPhase("idle");
624
+ }
625
+ };
626
+ // Annotate the CommonJS export names for ESM import in node:
627
+ 0 && (module.exports = {
628
+ OperatorSessionClient,
629
+ WorkspaceRealtimeClient,
630
+ buildWorkspaceWsUrl,
631
+ initWorkspaceSocket,
632
+ isActiveCallPhase,
633
+ isWorkspaceSocketConnected,
634
+ onWorkspaceSocketConnection,
635
+ subscribeWorkspaceChannel
636
+ });
637
+ //# sourceMappingURL=realtime.cjs.map