@aether-stack-dev/client-sdk 1.0.0 → 1.1.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +105 -220
  3. package/dist/AStackCSRClient.d.ts +26 -9
  4. package/dist/AnalyticsCollector.d.ts +1 -2
  5. package/dist/BillingMonitor.d.ts +5 -4
  6. package/dist/ConnectionStateManager.d.ts +4 -3
  7. package/dist/{WebRTCManager.d.ts → MediaManager.d.ts} +1 -6
  8. package/dist/PerformanceMonitor.d.ts +2 -3
  9. package/dist/SecurityLogger.d.ts +1 -5
  10. package/dist/SupabaseSignalingClient.d.ts +0 -1
  11. package/dist/UsageTracker.d.ts +0 -1
  12. package/dist/__tests__/setup.d.ts +12 -2
  13. package/dist/audio/AudioPlayer.d.ts +1 -1
  14. package/dist/audio/index.d.ts +0 -1
  15. package/dist/avatar/TalkingHeadAvatar.d.ts +0 -1
  16. package/dist/avatar/VRMAvatar.d.ts +0 -1
  17. package/dist/avatar/constants.d.ts +0 -1
  18. package/dist/avatar/index.d.ts +0 -1
  19. package/dist/core.d.ts +2 -7
  20. package/dist/index.d.ts +3 -15
  21. package/dist/index.esm.js +283 -1868
  22. package/dist/index.js +283 -1871
  23. package/dist/react/index.d.ts +0 -3
  24. package/dist/react/useAStackCSR.d.ts +0 -1
  25. package/dist/react.esm.js +263 -2088
  26. package/dist/react.js +261 -2087
  27. package/dist/types.d.ts +20 -19
  28. package/package.json +4 -4
  29. package/dist/AStackCSRClient.d.ts.map +0 -1
  30. package/dist/AStackClient.d.ts +0 -90
  31. package/dist/AStackClient.d.ts.map +0 -1
  32. package/dist/AStackEventSetup.d.ts +0 -35
  33. package/dist/AStackEventSetup.d.ts.map +0 -1
  34. package/dist/AStackMediaController.d.ts +0 -34
  35. package/dist/AStackMediaController.d.ts.map +0 -1
  36. package/dist/AnalyticsCollector.d.ts.map +0 -1
  37. package/dist/BillingMonitor.d.ts.map +0 -1
  38. package/dist/ConnectionStateManager.d.ts.map +0 -1
  39. package/dist/PerformanceMonitor.d.ts.map +0 -1
  40. package/dist/SecurityLogger.d.ts.map +0 -1
  41. package/dist/SessionManager.d.ts.map +0 -1
  42. package/dist/SupabaseSignalingClient.d.ts.map +0 -1
  43. package/dist/UsageTracker.d.ts.map +0 -1
  44. package/dist/WebRTCManager.d.ts.map +0 -1
  45. package/dist/__tests__/setup.d.ts.map +0 -1
  46. package/dist/audio/AudioPlayer.d.ts.map +0 -1
  47. package/dist/audio/index.d.ts.map +0 -1
  48. package/dist/avatar/TalkingHeadAvatar.d.ts.map +0 -1
  49. package/dist/avatar/VRMAvatar.d.ts.map +0 -1
  50. package/dist/avatar/constants.d.ts.map +0 -1
  51. package/dist/avatar/index.d.ts.map +0 -1
  52. package/dist/core.d.ts.map +0 -1
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/index.esm.js.map +0 -1
  55. package/dist/index.js.map +0 -1
  56. package/dist/react/index.d.ts.map +0 -1
  57. package/dist/react/useAStack.d.ts +0 -34
  58. package/dist/react/useAStack.d.ts.map +0 -1
  59. package/dist/react/useAStackCSR.d.ts.map +0 -1
  60. package/dist/react.esm.js.map +0 -1
  61. package/dist/react.js.map +0 -1
  62. package/dist/types.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -3,1816 +3,6 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var eventemitter3 = require('eventemitter3');
6
- var supabaseJs = require('@supabase/supabase-js');
7
-
8
- class WebRTCManager extends eventemitter3.EventEmitter {
9
- constructor(config) {
10
- super();
11
- this.localStream = null;
12
- this.config = config;
13
- }
14
- async initialize() {
15
- this.emit('initialized');
16
- }
17
- async getStats() {
18
- return null;
19
- }
20
- async getUserMedia() {
21
- const constraints = {
22
- audio: this.config.enableAudio !== false ? {
23
- echoCancellation: this.config.audio?.echoCancellation ?? true,
24
- noiseSuppression: this.config.audio?.noiseSuppression ?? true,
25
- autoGainControl: this.config.audio?.autoGainControl ?? true
26
- } : false
27
- };
28
- return navigator.mediaDevices.getUserMedia(constraints);
29
- }
30
- async addLocalStream(stream) {
31
- this.localStream = stream;
32
- this.emit('localStreamAdded', stream);
33
- }
34
- removeLocalStream(streamId) {
35
- if (this.localStream) {
36
- this.localStream.getTracks().forEach(track => {
37
- if (track.id === streamId)
38
- track.stop();
39
- });
40
- }
41
- }
42
- getLocalStreams() {
43
- if (!this.localStream)
44
- return [];
45
- return this.localStream.getTracks().map(track => ({
46
- id: track.id,
47
- kind: track.kind,
48
- active: track.enabled,
49
- track
50
- }));
51
- }
52
- getRemoteStreams() {
53
- return [];
54
- }
55
- async getMediaDevices() {
56
- return navigator.mediaDevices.enumerateDevices();
57
- }
58
- async switchAudioInput(deviceId) {
59
- if (this.localStream) {
60
- this.localStream.getAudioTracks().forEach(track => track.stop());
61
- }
62
- const stream = await navigator.mediaDevices.getUserMedia({
63
- audio: { deviceId: { exact: deviceId } }
64
- });
65
- this.localStream = stream;
66
- this.emit('localStreamAdded', stream);
67
- }
68
- muteAudio() {
69
- if (this.localStream) {
70
- this.localStream.getAudioTracks().forEach(track => { track.enabled = false; });
71
- }
72
- }
73
- unmuteAudio() {
74
- if (this.localStream) {
75
- this.localStream.getAudioTracks().forEach(track => { track.enabled = true; });
76
- }
77
- }
78
- muteVideo() {
79
- if (this.localStream) {
80
- this.localStream.getVideoTracks().forEach(track => { track.enabled = false; });
81
- }
82
- }
83
- unmuteVideo() {
84
- if (this.localStream) {
85
- this.localStream.getVideoTracks().forEach(track => { track.enabled = true; });
86
- }
87
- }
88
- async createOffer() {
89
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
90
- return { type: 'offer', sdp: '' };
91
- }
92
- async createAnswer(remoteSdp) {
93
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
94
- return { type: 'answer', sdp: '' };
95
- }
96
- async handleAnswer(answer) {
97
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
98
- }
99
- async addIceCandidate(candidate) {
100
- console.warn('WebRTC is deprecated. Use WebSocket-based AStackCSRClient instead.');
101
- }
102
- async close() {
103
- if (this.localStream) {
104
- this.localStream.getTracks().forEach(track => track.stop());
105
- this.localStream = null;
106
- }
107
- this.emit('closed');
108
- }
109
- }
110
-
111
- class AStackError extends Error {
112
- constructor(message, code) {
113
- super(message);
114
- this.name = 'AStackError';
115
- this.code = code;
116
- }
117
- }
118
- const ErrorCodes = {
119
- INVALID_API_KEY: 'INVALID_API_KEY',
120
- INVALID_TOKEN: 'INVALID_TOKEN',
121
- AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED',
122
- SESSION_EXPIRED: 'SESSION_EXPIRED',
123
- SESSION_CREATION_FAILED: 'SESSION_CREATION_FAILED',
124
- CONNECTION_LOST: 'CONNECTION_LOST',
125
- SIGNALING_ERROR: 'SIGNALING_ERROR',
126
- NETWORK_ERROR: 'NETWORK_ERROR',
127
- MEDIA_ACCESS_DENIED: 'MEDIA_ACCESS_DENIED',
128
- AUDIO_ERROR: 'AUDIO_ERROR',
129
- RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
130
- INSUFFICIENT_CREDITS: 'INSUFFICIENT_CREDITS',
131
- BILLING_ERROR: 'BILLING_ERROR'
132
- };
133
-
134
- class SupabaseSignalingClient extends eventemitter3.EventEmitter {
135
- constructor(config) {
136
- super();
137
- this.channel = null;
138
- this.sessionId = null;
139
- this.channelName = null;
140
- this.wsToken = null;
141
- this.channelId = null;
142
- this.connected = false;
143
- this.reconnectAttempts = 0;
144
- this.heartbeatInterval = null;
145
- this.config = config;
146
- this.maxReconnectAttempts = config.maxRetries || 3;
147
- this.reconnectDelay = config.reconnectDelay || 1000;
148
- this.supabase = supabaseJs.createClient(config.supabaseUrl || process.env.NEXT_PUBLIC_SUPABASE_URL || '', config.supabaseAnonKey || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || '', {
149
- realtime: {
150
- params: {
151
- eventsPerSecond: 10
152
- }
153
- }
154
- });
155
- }
156
- async connect(sessionId, channelName, wsToken) {
157
- this.sessionId = sessionId;
158
- this.channelName = channelName;
159
- this.wsToken = wsToken;
160
- return new Promise(async (resolve, reject) => {
161
- try {
162
- const { data: channelData, error } = await this.supabase
163
- .from('signaling_channels')
164
- .select('id')
165
- .eq('channel_name', channelName)
166
- .eq('ws_token', wsToken)
167
- .single();
168
- if (error || !channelData) {
169
- throw new AStackError('Invalid channel credentials', ErrorCodes.SIGNALING_ERROR);
170
- }
171
- this.channelId = channelData.id;
172
- this.channel = this.supabase.channel(channelName);
173
- this.channel
174
- .on('postgres_changes', {
175
- event: 'INSERT',
176
- schema: 'public',
177
- table: 'signaling_messages',
178
- filter: `channel_id=eq.${this.channelId}`
179
- }, (payload) => {
180
- this.handleSignalingMessage(payload.new);
181
- })
182
- .on('presence', { event: 'sync' }, () => {
183
- const state = this.channel?.presenceState();
184
- this.handlePresenceSync(state);
185
- })
186
- .on('presence', { event: 'join' }, ({ key, newPresences }) => {
187
- this.emit('userJoined', { key, presences: newPresences });
188
- })
189
- .on('presence', { event: 'leave' }, ({ key, leftPresences }) => {
190
- this.emit('userLeft', { key, presences: leftPresences });
191
- })
192
- .subscribe(async (status) => {
193
- if (status === 'SUBSCRIBED') {
194
- this.connected = true;
195
- this.reconnectAttempts = 0;
196
- await this.updateChannelStatus(true);
197
- await this.channel.track({
198
- user_type: 'client',
199
- user_id: sessionId,
200
- status: 'online',
201
- last_seen: new Date().toISOString(),
202
- metadata: {
203
- userAgent: navigator.userAgent,
204
- capabilities: {
205
- audio: this.config.enableAudio !== false,
206
- video: this.config.enableVideo === true,
207
- text: this.config.enableText !== false
208
- }
209
- }
210
- });
211
- this.startHeartbeat();
212
- this.emit('sessionJoined', { sessionId, channelName });
213
- resolve();
214
- }
215
- else if (status === 'CHANNEL_ERROR') {
216
- reject(new AStackError('Failed to subscribe to signaling channel', ErrorCodes.SIGNALING_ERROR));
217
- }
218
- });
219
- }
220
- catch (error) {
221
- reject(new AStackError(`Failed to connect to signaling: ${error}`, ErrorCodes.SIGNALING_ERROR));
222
- }
223
- });
224
- }
225
- async handleSignalingMessage(message) {
226
- if (message.sender_type === 'client') {
227
- return;
228
- }
229
- switch (message.message_type) {
230
- case 'audio':
231
- this.emit('audioResponse', message.payload);
232
- break;
233
- case 'text':
234
- this.emit('textResponse', message.payload);
235
- break;
236
- case 'control':
237
- this.emit('controlMessage', message.payload);
238
- break;
239
- case 'ready':
240
- this.emit('workerReady', message.payload);
241
- break;
242
- case 'error':
243
- this.emit('error', new AStackError(message.payload.message || 'Worker error', ErrorCodes.SIGNALING_ERROR));
244
- break;
245
- }
246
- }
247
- handlePresenceSync(state) {
248
- if (!state)
249
- return;
250
- const presences = Object.values(state);
251
- const workerPresent = presences.some((presence) => presence.user_type === 'worker' && presence.status === 'online');
252
- if (workerPresent) {
253
- this.emit('workerConnected');
254
- }
255
- else {
256
- this.emit('workerDisconnected');
257
- }
258
- }
259
- async updateChannelStatus(connected) {
260
- if (!this.channelId)
261
- return;
262
- try {
263
- await this.supabase
264
- .from('signaling_channels')
265
- .update({ client_connected: connected })
266
- .eq('id', this.channelId);
267
- }
268
- catch (error) { }
269
- }
270
- async sendText(text) {
271
- await this.sendSignalingMessage('text', { text });
272
- }
273
- async sendAudio(audioData, metadata) {
274
- await this.sendSignalingMessage('audio', { audio: audioData, ...metadata });
275
- }
276
- async sendControl(action, data) {
277
- await this.sendSignalingMessage('control', { action, ...data });
278
- }
279
- async sendSignalingMessage(messageType, payload) {
280
- if (!this.channelId || !this.connected) {
281
- throw new AStackError('Not connected to signaling channel', ErrorCodes.SIGNALING_ERROR);
282
- }
283
- try {
284
- const { error } = await this.supabase
285
- .from('signaling_messages')
286
- .insert({
287
- channel_id: this.channelId,
288
- sender_type: 'client',
289
- message_type: messageType,
290
- payload
291
- });
292
- if (error) {
293
- throw new AStackError(`Failed to send ${messageType}: ${error.message}`, ErrorCodes.SIGNALING_ERROR);
294
- }
295
- }
296
- catch (error) {
297
- throw error;
298
- }
299
- }
300
- sendTextMessage(text) {
301
- this.sendText(text).catch(() => { });
302
- }
303
- sendAudioData(audioBlob) {
304
- const reader = new FileReader();
305
- reader.onload = () => {
306
- const base64 = reader.result.split(',')[1];
307
- this.sendAudio(base64).catch(() => { });
308
- };
309
- reader.readAsDataURL(audioBlob);
310
- }
311
- startHeartbeat() {
312
- if (this.heartbeatInterval) {
313
- clearInterval(this.heartbeatInterval);
314
- }
315
- const interval = this.config.heartbeatInterval || 30000;
316
- this.heartbeatInterval = setInterval(async () => {
317
- if (this.channel && this.connected) {
318
- try {
319
- await this.channel.track({
320
- user_type: 'client',
321
- user_id: this.sessionId,
322
- status: 'online',
323
- last_seen: new Date().toISOString()
324
- });
325
- }
326
- catch (error) { }
327
- }
328
- }, interval);
329
- }
330
- stopHeartbeat() {
331
- if (this.heartbeatInterval) {
332
- clearInterval(this.heartbeatInterval);
333
- this.heartbeatInterval = null;
334
- }
335
- }
336
- async attemptReconnect() {
337
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
338
- this.emit('error', new AStackError('Maximum reconnection attempts reached', ErrorCodes.CONNECTION_LOST));
339
- return;
340
- }
341
- this.reconnectAttempts++;
342
- const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
343
- this.emit('reconnecting', this.reconnectAttempts);
344
- setTimeout(async () => {
345
- if (!this.connected && this.sessionId && this.channelName && this.wsToken) {
346
- try {
347
- await this.connect(this.sessionId, this.channelName, this.wsToken);
348
- this.emit('reconnected');
349
- }
350
- catch (error) {
351
- this.attemptReconnect();
352
- }
353
- }
354
- }, delay);
355
- }
356
- async leaveSession() {
357
- if (this.channel) {
358
- try {
359
- await this.channel.untrack();
360
- await this.updateChannelStatus(false);
361
- }
362
- catch (error) { }
363
- }
364
- }
365
- isConnected() {
366
- return this.connected;
367
- }
368
- getSessionId() {
369
- return this.sessionId;
370
- }
371
- async disconnect() {
372
- this.connected = false;
373
- this.stopHeartbeat();
374
- if (this.channel) {
375
- await this.leaveSession();
376
- await this.channel.unsubscribe();
377
- this.channel = null;
378
- }
379
- this.removeAllListeners();
380
- }
381
- }
382
-
383
- class SessionManager {
384
- constructor(apiEndpoint, sessionExpirationWarning = 1, onSessionExpiring) {
385
- this.sessionCredentials = null;
386
- this.workerInfo = null;
387
- this.expirationTimer = null;
388
- this.apiEndpoint = apiEndpoint;
389
- this.sessionExpirationWarning = sessionExpirationWarning;
390
- this.onSessionExpiring = onSessionExpiring;
391
- }
392
- async createSession(request) {
393
- try {
394
- const response = await fetch(`${this.apiEndpoint}/functions/v1/session`, {
395
- method: 'POST',
396
- headers: {
397
- 'Content-Type': 'application/json',
398
- 'Authorization': `Bearer ${request.apiKey}`
399
- },
400
- body: JSON.stringify({
401
- organizationId: request.organizationId,
402
- endUserId: request.endUserId,
403
- metadata: request.metadata,
404
- workerPreferences: request.workerPreferences
405
- })
406
- });
407
- if (!response.ok) {
408
- const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
409
- if (response.status === 401) {
410
- throw new AStackError('Invalid API key', ErrorCodes.INVALID_API_KEY);
411
- }
412
- else if (response.status === 429) {
413
- throw new AStackError('Rate limit exceeded', ErrorCodes.RATE_LIMIT_EXCEEDED);
414
- }
415
- else if (response.status === 402) {
416
- throw new AStackError('Insufficient credits', ErrorCodes.INSUFFICIENT_CREDITS);
417
- }
418
- throw new AStackError(errorData.error || 'Session creation failed', ErrorCodes.SESSION_CREATION_FAILED);
419
- }
420
- const data = await response.json();
421
- const expiresAt = new Date(Date.now() + (data.expiresIn * 1000));
422
- this.sessionCredentials = {
423
- sessionId: data.sessionId,
424
- wsToken: data.wsToken,
425
- channelName: data.channelName,
426
- workerUrl: data.workerUrl,
427
- expiresAt
428
- };
429
- if (data.workerInfo) {
430
- this.workerInfo = data.workerInfo;
431
- }
432
- this.scheduleExpirationWarning();
433
- return this.sessionCredentials;
434
- }
435
- catch (error) {
436
- if (error instanceof AStackError) {
437
- throw error;
438
- }
439
- throw new AStackError(`Failed to create session: ${error instanceof Error ? error.message : 'Unknown error'}`, ErrorCodes.SESSION_CREATION_FAILED);
440
- }
441
- }
442
- async renewSession(apiKey) {
443
- if (!this.sessionCredentials) {
444
- throw new AStackError('No active session to renew', ErrorCodes.SESSION_EXPIRED);
445
- }
446
- const request = {
447
- apiKey,
448
- organizationId: this.sessionCredentials.sessionId.split('-')[0],
449
- endUserId: undefined,
450
- metadata: { previousSessionId: this.sessionCredentials.sessionId }
451
- };
452
- return this.createSession(request);
453
- }
454
- scheduleExpirationWarning() {
455
- if (this.expirationTimer) {
456
- clearTimeout(this.expirationTimer);
457
- }
458
- if (!this.sessionCredentials || !this.onSessionExpiring) {
459
- return;
460
- }
461
- const now = Date.now();
462
- const expirationTime = this.sessionCredentials.expiresAt.getTime();
463
- const warningTime = expirationTime - (this.sessionExpirationWarning * 60 * 1000);
464
- if (warningTime > now) {
465
- this.expirationTimer = setTimeout(() => {
466
- if (this.onSessionExpiring && this.sessionCredentials) {
467
- const minutesRemaining = Math.floor((this.sessionCredentials.expiresAt.getTime() - Date.now()) / 60000);
468
- this.onSessionExpiring(minutesRemaining);
469
- }
470
- }, warningTime - now);
471
- }
472
- }
473
- getCredentials() {
474
- return this.sessionCredentials;
475
- }
476
- isSessionValid() {
477
- if (!this.sessionCredentials) {
478
- return false;
479
- }
480
- return this.sessionCredentials.expiresAt.getTime() > Date.now();
481
- }
482
- getTimeUntilExpiration() {
483
- if (!this.sessionCredentials) {
484
- return 0;
485
- }
486
- const remaining = this.sessionCredentials.expiresAt.getTime() - Date.now();
487
- return Math.max(0, remaining);
488
- }
489
- clearSession() {
490
- this.sessionCredentials = null;
491
- this.workerInfo = null;
492
- if (this.expirationTimer) {
493
- clearTimeout(this.expirationTimer);
494
- this.expirationTimer = null;
495
- }
496
- }
497
- getWorkerInfo() {
498
- return this.workerInfo;
499
- }
500
- destroy() {
501
- this.clearSession();
502
- }
503
- }
504
-
505
- class BillingMonitor {
506
- constructor(apiEndpoint, apiKey) {
507
- this.apiEndpoint = apiEndpoint;
508
- this.apiKey = apiKey;
509
- this.updateCallbacks = [];
510
- this.alertCallbacks = [];
511
- this.warningThresholds = {
512
- creditBalance: 10, // Warn when credits drop below 10
513
- usagePercentage: 80 // Warn when usage reaches 80% of rate limit
514
- };
515
- }
516
- async fetchBillingInfo() {
517
- try {
518
- const response = await fetch(`${this.apiEndpoint}/billing/info`, {
519
- headers: {
520
- 'Authorization': `Bearer ${this.apiKey}`,
521
- 'Content-Type': 'application/json'
522
- }
523
- });
524
- if (!response.ok) {
525
- throw new Error(`Failed to fetch billing info: ${response.statusText}`);
526
- }
527
- const info = await response.json();
528
- this.updateBillingInfo(info);
529
- return info;
530
- }
531
- catch (error) {
532
- throw error;
533
- }
534
- }
535
- updateBillingInfo(info) {
536
- const previousInfo = this.billingInfo;
537
- this.billingInfo = info;
538
- this.updateCallbacks.forEach(callback => callback(info));
539
- this.checkForAlerts(info, previousInfo);
540
- }
541
- checkForAlerts(current, previous) {
542
- if (current.creditBalance < this.warningThresholds.creditBalance) {
543
- this.triggerAlert({
544
- type: 'low_balance',
545
- details: {
546
- currentBalance: current.creditBalance,
547
- threshold: this.warningThresholds.creditBalance
548
- }
549
- });
550
- }
551
- const usagePercentage = (current.currentUsage / current.rateLimit) * 100;
552
- if (usagePercentage >= this.warningThresholds.usagePercentage) {
553
- this.triggerAlert({
554
- type: 'limit_exceeded',
555
- details: {
556
- currentUsage: current.currentUsage,
557
- rateLimit: current.rateLimit,
558
- percentage: usagePercentage
559
- }
560
- });
561
- }
562
- if (previous && previous.creditBalance - current.creditBalance > 100) {
563
- this.triggerAlert({
564
- type: 'low_balance',
565
- details: {
566
- previousBalance: previous.creditBalance,
567
- currentBalance: current.creditBalance,
568
- spent: previous.creditBalance - current.creditBalance
569
- }
570
- });
571
- }
572
- }
573
- triggerAlert(alert) {
574
- this.alertCallbacks.forEach(callback => callback(alert));
575
- }
576
- startMonitoring(intervalMs = 60000) {
577
- this.stopMonitoring();
578
- this.fetchBillingInfo().catch(() => { });
579
- this.checkInterval = setInterval(() => {
580
- this.fetchBillingInfo().catch(() => { });
581
- }, intervalMs);
582
- }
583
- stopMonitoring() {
584
- if (this.checkInterval) {
585
- clearInterval(this.checkInterval);
586
- this.checkInterval = undefined;
587
- }
588
- }
589
- onUpdate(callback) {
590
- this.updateCallbacks.push(callback);
591
- }
592
- onAlert(callback) {
593
- this.alertCallbacks.push(callback);
594
- }
595
- removeUpdateCallback(callback) {
596
- this.updateCallbacks = this.updateCallbacks.filter(cb => cb !== callback);
597
- }
598
- removeAlertCallback(callback) {
599
- this.alertCallbacks = this.alertCallbacks.filter(cb => cb !== callback);
600
- }
601
- getCurrentBillingInfo() {
602
- return this.billingInfo;
603
- }
604
- setWarningThresholds(thresholds) {
605
- this.warningThresholds = { ...this.warningThresholds, ...thresholds };
606
- }
607
- async checkRateLimit() {
608
- const info = await this.fetchBillingInfo();
609
- return info.currentUsage < info.rateLimit;
610
- }
611
- destroy() {
612
- this.stopMonitoring();
613
- this.updateCallbacks = [];
614
- this.alertCallbacks = [];
615
- }
616
- }
617
-
618
- class SecurityLogger {
619
- constructor(config) {
620
- this.eventQueue = [];
621
- this.config = {
622
- enableLocalLogging: true,
623
- batchSize: 10,
624
- flushInterval: 5000,
625
- sessionId: '',
626
- organizationId: '',
627
- ...config
628
- };
629
- if (this.config.flushInterval > 0) {
630
- this.startAutoFlush();
631
- }
632
- }
633
- setSessionId(sessionId) {
634
- this.config.sessionId = sessionId;
635
- }
636
- logEvent(eventType, severity, details) {
637
- const event = {
638
- eventType,
639
- severity,
640
- details: {
641
- ...details,
642
- sessionId: this.config.sessionId,
643
- organizationId: this.config.organizationId,
644
- userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : undefined,
645
- timestamp: new Date().toISOString()
646
- },
647
- timestamp: new Date()
648
- };
649
- if (this.config.enableLocalLogging) {
650
- this.logToConsole(event);
651
- }
652
- this.eventQueue.push(event);
653
- if (this.eventQueue.length >= this.config.batchSize) {
654
- this.flush().catch(() => { });
655
- }
656
- }
657
- logAuthFailure(reason, details) {
658
- this.logEvent('auth_failure', 'warning', {
659
- reason,
660
- ...details
661
- });
662
- }
663
- logRateLimit(limit, current, endpoint) {
664
- this.logEvent('rate_limit', 'warning', {
665
- limit,
666
- current,
667
- endpoint,
668
- exceeded: current >= limit
669
- });
670
- }
671
- logInvalidRequest(reason, request) {
672
- this.logEvent('invalid_request', 'warning', {
673
- reason,
674
- request: this.sanitizeRequest(request)
675
- });
676
- }
677
- logSessionHijackAttempt(details) {
678
- this.logEvent('session_hijack', 'critical', details);
679
- }
680
- sanitizeRequest(request) {
681
- if (!request)
682
- return undefined;
683
- const sanitized = { ...request };
684
- const sensitiveFields = ['password', 'token', 'apiKey', 'secret', 'credential'];
685
- for (const field of sensitiveFields) {
686
- if (sanitized[field]) {
687
- sanitized[field] = '[REDACTED]';
688
- }
689
- }
690
- return sanitized;
691
- }
692
- logToConsole(event) {
693
- }
694
- getConsoleLogLevel(severity) {
695
- switch (severity) {
696
- case 'info':
697
- return 'log';
698
- case 'warning':
699
- return 'warn';
700
- case 'error':
701
- case 'critical':
702
- return 'error';
703
- default:
704
- return 'log';
705
- }
706
- }
707
- async flush() {
708
- if (this.eventQueue.length === 0)
709
- return;
710
- const events = [...this.eventQueue];
711
- this.eventQueue = [];
712
- try {
713
- const response = await fetch(`${this.config.apiEndpoint}/security/events`, {
714
- method: 'POST',
715
- headers: {
716
- 'Authorization': `Bearer ${this.config.apiKey}`,
717
- 'Content-Type': 'application/json'
718
- },
719
- body: JSON.stringify({ events })
720
- });
721
- if (!response.ok) {
722
- this.eventQueue.unshift(...events);
723
- }
724
- }
725
- catch (error) {
726
- this.eventQueue.unshift(...events);
727
- }
728
- }
729
- startAutoFlush() {
730
- this.flushInterval = setInterval(() => {
731
- this.flush().catch(() => { });
732
- }, this.config.flushInterval);
733
- }
734
- stopAutoFlush() {
735
- if (this.flushInterval) {
736
- clearInterval(this.flushInterval);
737
- this.flushInterval = undefined;
738
- }
739
- }
740
- async destroy() {
741
- this.stopAutoFlush();
742
- await this.flush();
743
- }
744
- }
745
-
746
- class ConnectionStateManager extends eventemitter3.EventEmitter {
747
- constructor() {
748
- super();
749
- this.stateHistory = [];
750
- this.maxHistorySize = 100;
751
- this.state = {
752
- signaling: 'disconnected',
753
- websocket: 'new',
754
- overall: 'disconnected',
755
- lastActivity: new Date(),
756
- reconnectAttempts: 0,
757
- errors: []
758
- };
759
- this.quality = {
760
- qualityScore: 1.0
761
- };
762
- }
763
- updateSignalingState(state) {
764
- const previousState = this.state.signaling;
765
- this.state.signaling = state;
766
- this.state.lastActivity = new Date();
767
- if (state === 'connected') {
768
- this.state.reconnectAttempts = 0;
769
- }
770
- else if (state === 'reconnecting') {
771
- this.state.reconnectAttempts++;
772
- }
773
- this.updateOverallState();
774
- this.addToHistory();
775
- if (previousState !== state) {
776
- this.emit('signalingStateChange', state, previousState);
777
- }
778
- }
779
- updateWebSocketState(state) {
780
- const previousState = this.state.websocket;
781
- this.state.websocket = state;
782
- this.state.lastActivity = new Date();
783
- this.updateOverallState();
784
- this.addToHistory();
785
- if (previousState !== state) {
786
- this.emit('websocketStateChange', state, previousState);
787
- }
788
- }
789
- updateWebRTCState(state) {
790
- this.updateWebSocketState(state);
791
- }
792
- updateICEState(state) {
793
- // No-op for backwards compatibility
794
- }
795
- updateOverallState() {
796
- const previousOverall = this.state.overall;
797
- if (this.state.signaling === 'disconnected' || this.state.websocket === 'failed' || this.state.websocket === 'closed') {
798
- this.state.overall = 'disconnected';
799
- }
800
- else if (this.state.signaling === 'connecting' || this.state.websocket === 'connecting') {
801
- this.state.overall = 'connecting';
802
- }
803
- else if (this.state.signaling === 'connected' && this.state.websocket === 'connected') {
804
- this.state.overall = 'connected';
805
- }
806
- else if (this.state.signaling === 'reconnecting' || this.state.websocket === 'disconnected') {
807
- this.state.overall = 'degraded';
808
- }
809
- else if (this.state.errors.length > 0) {
810
- this.state.overall = 'error';
811
- }
812
- else {
813
- this.state.overall = 'connecting';
814
- }
815
- if (previousOverall !== this.state.overall) {
816
- this.emit('overallStateChange', this.state.overall, previousOverall);
817
- }
818
- }
819
- addError(error) {
820
- this.state.errors.push(error);
821
- if (this.state.errors.length > 10) {
822
- this.state.errors = this.state.errors.slice(-10);
823
- }
824
- this.updateOverallState();
825
- this.emit('error', error);
826
- }
827
- clearErrors() {
828
- this.state.errors = [];
829
- this.updateOverallState();
830
- }
831
- updateConnectionQuality(stats) {
832
- const previousScore = this.quality.qualityScore;
833
- if (stats.latency !== undefined)
834
- this.quality.latency = stats.latency;
835
- if (stats.jitter !== undefined)
836
- this.quality.jitter = stats.jitter;
837
- if (stats.packetLoss !== undefined)
838
- this.quality.packetLoss = stats.packetLoss;
839
- if (stats.bandwidth !== undefined)
840
- this.quality.bandwidth = stats.bandwidth;
841
- this.quality.qualityScore = this.calculateQualityScore();
842
- if (Math.abs(previousScore - this.quality.qualityScore) > 0.1) {
843
- this.emit('qualityChange', this.quality);
844
- }
845
- }
846
- calculateQualityScore() {
847
- let score = 1.0;
848
- if (this.quality.latency !== undefined) {
849
- if (this.quality.latency < 150) ;
850
- else if (this.quality.latency < 300) {
851
- score -= 0.1;
852
- }
853
- else if (this.quality.latency < 500) {
854
- score -= 0.3;
855
- }
856
- else {
857
- score -= 0.5;
858
- }
859
- }
860
- if (this.quality.packetLoss !== undefined) {
861
- if (this.quality.packetLoss < 1) ;
862
- else if (this.quality.packetLoss < 3) {
863
- score -= 0.2;
864
- }
865
- else if (this.quality.packetLoss < 5) {
866
- score -= 0.4;
867
- }
868
- else {
869
- score -= 0.6;
870
- }
871
- }
872
- if (this.quality.jitter !== undefined) {
873
- if (this.quality.jitter < 30) ;
874
- else if (this.quality.jitter < 50) {
875
- score -= 0.1;
876
- }
877
- else if (this.quality.jitter < 100) {
878
- score -= 0.2;
879
- }
880
- else {
881
- score -= 0.3;
882
- }
883
- }
884
- return Math.max(0, Math.min(1, score));
885
- }
886
- startHealthCheck(intervalMs = 5000) {
887
- this.stopHealthCheck();
888
- this.healthCheckInterval = setInterval(() => {
889
- const timeSinceLastActivity = Date.now() - this.state.lastActivity.getTime();
890
- if (timeSinceLastActivity > 30000 && this.state.overall === 'connected') {
891
- this.emit('healthCheckFailed', timeSinceLastActivity);
892
- }
893
- }, intervalMs);
894
- }
895
- stopHealthCheck() {
896
- if (this.healthCheckInterval) {
897
- clearInterval(this.healthCheckInterval);
898
- this.healthCheckInterval = undefined;
899
- }
900
- }
901
- addToHistory() {
902
- this.stateHistory.push({
903
- state: { ...this.state },
904
- timestamp: new Date()
905
- });
906
- if (this.stateHistory.length > this.maxHistorySize) {
907
- this.stateHistory = this.stateHistory.slice(-this.maxHistorySize);
908
- }
909
- }
910
- getState() {
911
- return { ...this.state };
912
- }
913
- getQuality() {
914
- return { ...this.quality };
915
- }
916
- getStateHistory() {
917
- return [...this.stateHistory];
918
- }
919
- isConnected() {
920
- return this.state.overall === 'connected';
921
- }
922
- isHealthy() {
923
- return this.state.overall === 'connected' &&
924
- this.quality.qualityScore > 0.5 &&
925
- this.state.errors.length === 0;
926
- }
927
- getReconnectAttempts() {
928
- return this.state.reconnectAttempts;
929
- }
930
- reset() {
931
- this.state = {
932
- signaling: 'disconnected',
933
- websocket: 'new',
934
- overall: 'disconnected',
935
- lastActivity: new Date(),
936
- reconnectAttempts: 0,
937
- errors: []
938
- };
939
- this.quality = {
940
- qualityScore: 1.0
941
- };
942
- this.stateHistory = [];
943
- this.emit('reset');
944
- }
945
- destroy() {
946
- this.stopHealthCheck();
947
- this.removeAllListeners();
948
- }
949
- }
950
-
951
- class PerformanceMonitor extends eventemitter3.EventEmitter {
952
- constructor(config) {
953
- super();
954
- this.benchmarks = [];
955
- this.isCollecting = false;
956
- this.config = {
957
- collectionInterval: 5000,
958
- enableAutoReport: true,
959
- reportingInterval: 30000,
960
- sessionId: '',
961
- ...config
962
- };
963
- this.startTime = Date.now();
964
- this.metrics = this.initializeMetrics();
965
- }
966
- initializeMetrics() {
967
- return {
968
- latency: {
969
- signaling: { avg: 0, min: Infinity, max: 0, samples: [] },
970
- websocket: { avg: 0, min: Infinity, max: 0, samples: [] },
971
- audio: { avg: 0, min: Infinity, max: 0, samples: [] }
972
- },
973
- throughput: {
974
- audio: { inbound: 0, outbound: 0 },
975
- data: { inbound: 0, outbound: 0 }
976
- },
977
- quality: {
978
- audioQuality: 1.0,
979
- connectionQuality: 1.0
980
- },
981
- resources: {
982
- cpuUsage: 0,
983
- memoryUsage: 0,
984
- bandwidth: { upload: 0, download: 0 }
985
- }
986
- };
987
- }
988
- startCollection(sessionId) {
989
- if (this.isCollecting)
990
- return;
991
- this.isCollecting = true;
992
- if (sessionId) {
993
- this.config.sessionId = sessionId;
994
- }
995
- this.collectionTimer = setInterval(() => {
996
- this.collectMetrics();
997
- }, this.config.collectionInterval);
998
- if (this.config.enableAutoReport) {
999
- this.reportingTimer = setInterval(() => {
1000
- this.reportMetrics();
1001
- }, this.config.reportingInterval);
1002
- }
1003
- this.emit('collectionStarted');
1004
- }
1005
- stopCollection() {
1006
- if (!this.isCollecting)
1007
- return;
1008
- this.isCollecting = false;
1009
- if (this.collectionTimer) {
1010
- clearInterval(this.collectionTimer);
1011
- this.collectionTimer = undefined;
1012
- }
1013
- if (this.reportingTimer) {
1014
- clearInterval(this.reportingTimer);
1015
- this.reportingTimer = undefined;
1016
- }
1017
- if (this.config.enableAutoReport) {
1018
- this.reportMetrics();
1019
- }
1020
- this.emit('collectionStopped');
1021
- }
1022
- recordLatency(type, latency) {
1023
- const key = type === 'webrtc' ? 'websocket' : type === 'video' ? 'audio' : type;
1024
- const metric = this.metrics.latency[key];
1025
- if (!metric)
1026
- return;
1027
- metric.samples.push(latency);
1028
- if (metric.samples.length > 100) {
1029
- metric.samples.shift();
1030
- }
1031
- metric.min = Math.min(metric.min, latency);
1032
- metric.max = Math.max(metric.max, latency);
1033
- metric.avg = metric.samples.reduce((a, b) => a + b, 0) / metric.samples.length;
1034
- }
1035
- updateThroughput(type, direction, bytesPerSecond) {
1036
- const key = type === 'video' ? 'audio' : type;
1037
- if (this.metrics.throughput[key]) {
1038
- this.metrics.throughput[key][direction] = bytesPerSecond;
1039
- }
1040
- }
1041
- updateQuality(quality) {
1042
- this.metrics.quality = { ...this.metrics.quality, ...quality };
1043
- }
1044
- updateResources(resources) {
1045
- this.metrics.resources = { ...this.metrics.resources, ...resources };
1046
- }
1047
- recordBenchmark(benchmark) {
1048
- const fullBenchmark = {
1049
- ...benchmark,
1050
- timestamp: new Date()
1051
- };
1052
- this.benchmarks.push(fullBenchmark);
1053
- if (this.benchmarks.length > 50) {
1054
- this.benchmarks.shift();
1055
- }
1056
- this.emit('benchmarkRecorded', fullBenchmark);
1057
- }
1058
- getMetrics() {
1059
- return JSON.parse(JSON.stringify(this.metrics));
1060
- }
1061
- getBenchmarks() {
1062
- return [...this.benchmarks];
1063
- }
1064
- getLatencyStats(type) {
1065
- if (type) {
1066
- const key = type === 'webrtc' ? 'websocket' : type === 'video' ? 'audio' : type;
1067
- return { ...this.metrics.latency[key] };
1068
- }
1069
- return JSON.parse(JSON.stringify(this.metrics.latency));
1070
- }
1071
- async reportMetrics() {
1072
- if (!this.config.sessionId)
1073
- return;
1074
- try {
1075
- const report = {
1076
- sessionId: this.config.sessionId,
1077
- timestamp: new Date(),
1078
- duration: Date.now() - this.startTime,
1079
- metrics: this.getMetrics(),
1080
- benchmarks: this.getBenchmarks()
1081
- };
1082
- const response = await fetch(`${this.config.apiEndpoint}/performance/report`, {
1083
- method: 'POST',
1084
- headers: {
1085
- 'Content-Type': 'application/json'
1086
- },
1087
- body: JSON.stringify(report)
1088
- });
1089
- if (!response.ok) {
1090
- throw new Error(`Failed to report metrics: ${response.statusText}`);
1091
- }
1092
- this.emit('metricsReported', report);
1093
- }
1094
- catch (error) {
1095
- this.emit('error', error);
1096
- }
1097
- }
1098
- collectMetrics() {
1099
- this.emit('collectMetrics', {
1100
- timestamp: Date.now(),
1101
- metrics: this.metrics
1102
- });
1103
- }
1104
- destroy() {
1105
- this.stopCollection();
1106
- this.removeAllListeners();
1107
- this.benchmarks = [];
1108
- }
1109
- }
1110
-
1111
- class AnalyticsCollector extends eventemitter3.EventEmitter {
1112
- constructor(config) {
1113
- super();
1114
- this.events = [];
1115
- this.sessionAnalytics = null;
1116
- this.isCollecting = false;
1117
- this.config = {
1118
- enableAutoFlush: true,
1119
- flushInterval: 60000, // 1 minute
1120
- batchSize: 100,
1121
- sessionId: '',
1122
- userId: '',
1123
- organizationId: '',
1124
- ...config
1125
- };
1126
- }
1127
- startSession(sessionId) {
1128
- if (this.isCollecting) {
1129
- this.endSession();
1130
- }
1131
- this.isCollecting = true;
1132
- this.config.sessionId = sessionId;
1133
- this.sessionAnalytics = {
1134
- sessionId,
1135
- startTime: new Date(),
1136
- duration: 0,
1137
- eventsCount: 0,
1138
- errorCount: 0,
1139
- mediaTypes: [],
1140
- connectionQuality: 1.0,
1141
- dataTransferred: {
1142
- audio: { sent: 0, received: 0 },
1143
- video: { sent: 0, received: 0 },
1144
- text: { sent: 0, received: 0 }
1145
- }
1146
- };
1147
- if (this.config.enableAutoFlush) {
1148
- this.flushTimer = setInterval(() => {
1149
- this.flush();
1150
- }, this.config.flushInterval);
1151
- }
1152
- this.trackEvent('session_started', {});
1153
- }
1154
- endSession() {
1155
- if (!this.isCollecting || !this.sessionAnalytics)
1156
- return;
1157
- this.isCollecting = false;
1158
- this.sessionAnalytics.endTime = new Date();
1159
- this.sessionAnalytics.duration =
1160
- (this.sessionAnalytics.endTime.getTime() - this.sessionAnalytics.startTime.getTime()) / 1000;
1161
- this.trackEvent('session_ended', {
1162
- duration: this.sessionAnalytics.duration
1163
- });
1164
- this.flush();
1165
- if (this.flushTimer) {
1166
- clearInterval(this.flushTimer);
1167
- this.flushTimer = undefined;
1168
- }
1169
- this.sendSessionAnalytics();
1170
- }
1171
- trackEvent(eventType, metadata = {}) {
1172
- const event = {
1173
- eventType,
1174
- timestamp: new Date(),
1175
- sessionId: this.config.sessionId,
1176
- userId: this.config.userId,
1177
- organizationId: this.config.organizationId,
1178
- metadata
1179
- };
1180
- this.events.push(event);
1181
- if (this.sessionAnalytics) {
1182
- this.sessionAnalytics.eventsCount++;
1183
- if (eventType.includes('error') || metadata.error) {
1184
- this.sessionAnalytics.errorCount++;
1185
- }
1186
- }
1187
- if (this.events.length >= this.config.batchSize) {
1188
- this.flush();
1189
- }
1190
- this.emit('eventTracked', event);
1191
- }
1192
- updateMediaTypes(mediaTypes) {
1193
- if (this.sessionAnalytics) {
1194
- this.sessionAnalytics.mediaTypes = [...new Set(mediaTypes)];
1195
- }
1196
- }
1197
- updateConnectionQuality(quality) {
1198
- if (this.sessionAnalytics) {
1199
- const weight = 0.1;
1200
- this.sessionAnalytics.connectionQuality =
1201
- this.sessionAnalytics.connectionQuality * (1 - weight) + quality * weight;
1202
- }
1203
- }
1204
- updateDataTransferred(type, direction, bytes) {
1205
- if (this.sessionAnalytics) {
1206
- this.sessionAnalytics.dataTransferred[type][direction] += bytes;
1207
- }
1208
- }
1209
- getSessionAnalytics() {
1210
- if (!this.sessionAnalytics)
1211
- return null;
1212
- return {
1213
- ...this.sessionAnalytics,
1214
- duration: this.isCollecting
1215
- ? (Date.now() - this.sessionAnalytics.startTime.getTime()) / 1000
1216
- : this.sessionAnalytics.duration
1217
- };
1218
- }
1219
- getEvents() {
1220
- return [...this.events];
1221
- }
1222
- async flush() {
1223
- if (this.events.length === 0)
1224
- return;
1225
- const eventsToSend = [...this.events];
1226
- this.events = [];
1227
- try {
1228
- const response = await fetch(`${this.config.apiEndpoint}/analytics/events`, {
1229
- method: 'POST',
1230
- headers: {
1231
- 'Content-Type': 'application/json',
1232
- 'Authorization': `Bearer ${this.config.apiKey}`
1233
- },
1234
- body: JSON.stringify({
1235
- events: eventsToSend
1236
- })
1237
- });
1238
- if (!response.ok) {
1239
- throw new Error(`Failed to send analytics: ${response.statusText}`);
1240
- }
1241
- this.emit('eventsFlushed', eventsToSend.length);
1242
- }
1243
- catch (error) {
1244
- this.events.unshift(...eventsToSend);
1245
- this.emit('error', error);
1246
- }
1247
- }
1248
- async sendSessionAnalytics() {
1249
- if (!this.sessionAnalytics)
1250
- return;
1251
- try {
1252
- const response = await fetch(`${this.config.apiEndpoint}/analytics/sessions`, {
1253
- method: 'POST',
1254
- headers: {
1255
- 'Content-Type': 'application/json',
1256
- 'Authorization': `Bearer ${this.config.apiKey}`
1257
- },
1258
- body: JSON.stringify(this.sessionAnalytics)
1259
- });
1260
- if (!response.ok) {
1261
- throw new Error(`Failed to send session analytics: ${response.statusText}`);
1262
- }
1263
- this.emit('sessionAnalyticsSent', this.sessionAnalytics);
1264
- }
1265
- catch (error) {
1266
- this.emit('error', error);
1267
- }
1268
- }
1269
- destroy() {
1270
- if (this.isCollecting) {
1271
- this.endSession();
1272
- }
1273
- if (this.flushTimer) {
1274
- clearInterval(this.flushTimer);
1275
- this.flushTimer = undefined;
1276
- }
1277
- this.removeAllListeners();
1278
- this.events = [];
1279
- this.sessionAnalytics = null;
1280
- }
1281
- }
1282
-
1283
- class UsageTracker {
1284
- constructor(sessionId) {
1285
- this.inputTokens = 0;
1286
- this.outputTokens = 0;
1287
- this.errorCount = 0;
1288
- this.qualityScores = [];
1289
- this.sessionId = sessionId;
1290
- this.startTime = new Date();
1291
- }
1292
- start() {
1293
- this.startTime = new Date();
1294
- }
1295
- stop() {
1296
- this.endTime = new Date();
1297
- }
1298
- trackInputTokens(tokens) {
1299
- this.inputTokens += tokens;
1300
- }
1301
- trackOutputTokens(tokens) {
1302
- this.outputTokens += tokens;
1303
- }
1304
- trackError() {
1305
- this.errorCount++;
1306
- }
1307
- trackQualityScore(score) {
1308
- if (score >= 0 && score <= 1) {
1309
- this.qualityScores.push(score);
1310
- }
1311
- }
1312
- getDuration() {
1313
- const end = this.endTime || new Date();
1314
- return Math.floor((end.getTime() - this.startTime.getTime()) / 1000);
1315
- }
1316
- getAverageQualityScore() {
1317
- if (this.qualityScores.length === 0)
1318
- return undefined;
1319
- const sum = this.qualityScores.reduce((acc, score) => acc + score, 0);
1320
- return sum / this.qualityScores.length;
1321
- }
1322
- getMetrics() {
1323
- return {
1324
- sessionId: this.sessionId,
1325
- duration: this.getDuration(),
1326
- inputTokens: this.inputTokens > 0 ? this.inputTokens : undefined,
1327
- outputTokens: this.outputTokens > 0 ? this.outputTokens : undefined,
1328
- errorCount: this.errorCount,
1329
- qualityScore: this.getAverageQualityScore()
1330
- };
1331
- }
1332
- reset() {
1333
- this.startTime = new Date();
1334
- this.endTime = undefined;
1335
- this.inputTokens = 0;
1336
- this.outputTokens = 0;
1337
- this.errorCount = 0;
1338
- this.qualityScores = [];
1339
- }
1340
- }
1341
-
1342
- /**
1343
- * @deprecated This module is deprecated. Use AStackCSRClient for WebSocket-based communication.
1344
- * This file is kept for backwards compatibility only.
1345
- */
1346
- /**
1347
- * @deprecated Use AStackCSRClient instead. This function is kept for backwards compatibility.
1348
- */
1349
- function setupEventHandlers(emitter, deps) {
1350
- console.warn('setupEventHandlers is deprecated. Use AStackCSRClient for WebSocket-based communication.');
1351
- const { signaling, connectionStateManager, securityLogger } = deps;
1352
- signaling.on('sessionJoined', () => {
1353
- const sessionId = deps.getSessionId();
1354
- const usageTracker = new UsageTracker(sessionId);
1355
- usageTracker.start();
1356
- deps.setUsageTracker(usageTracker);
1357
- securityLogger.setSessionId(sessionId);
1358
- connectionStateManager.updateSignalingState('connected');
1359
- connectionStateManager.startHealthCheck();
1360
- emitter.emit('sessionReady', sessionId);
1361
- });
1362
- signaling.on('aiResponse', (data) => {
1363
- const response = {
1364
- type: 'text',
1365
- content: data.text || data.message,
1366
- timestamp: data.timestamp || Date.now(),
1367
- messageId: data.messageId
1368
- };
1369
- emitter.emit('messageReceived', response);
1370
- });
1371
- signaling.on('audioResponse', (data) => {
1372
- if (data.audio) {
1373
- try {
1374
- const binaryString = atob(data.audio.split(',')[1]);
1375
- const bytes = new Uint8Array(binaryString.length);
1376
- for (let i = 0; i < binaryString.length; i++) {
1377
- bytes[i] = binaryString.charCodeAt(i);
1378
- }
1379
- const audioBlob = new Blob([bytes], { type: 'audio/wav' });
1380
- const response = {
1381
- type: 'audio',
1382
- content: audioBlob,
1383
- timestamp: data.timestamp || Date.now(),
1384
- messageId: data.messageId
1385
- };
1386
- emitter.emit('messageReceived', response);
1387
- }
1388
- catch (error) { }
1389
- }
1390
- });
1391
- signaling.on('textResponse', (data) => {
1392
- emitter.emit('text-response', data);
1393
- });
1394
- signaling.on('disconnected', () => {
1395
- deps.setStatus({ status: 'disconnected' });
1396
- connectionStateManager.updateSignalingState('disconnected');
1397
- emitter.emit('disconnected');
1398
- });
1399
- signaling.on('reconnecting', (attempt) => {
1400
- connectionStateManager.updateSignalingState('reconnecting');
1401
- emitter.emit('reconnecting', attempt);
1402
- });
1403
- signaling.on('error', (error) => {
1404
- const usageTracker = deps.getUsageTracker();
1405
- if (usageTracker) {
1406
- usageTracker.trackError();
1407
- }
1408
- connectionStateManager.addError(error);
1409
- if (error.code === ErrorCodes.AUTHENTICATION_FAILED || error.code === ErrorCodes.INVALID_API_KEY) {
1410
- securityLogger.logAuthFailure(error.message, { code: error.code });
1411
- }
1412
- else if (error.code === ErrorCodes.RATE_LIMIT_EXCEEDED) {
1413
- securityLogger.logRateLimit(0, 0, 'signaling');
1414
- }
1415
- emitter.emit('error', error);
1416
- });
1417
- }
1418
-
1419
- class AStackMediaController {
1420
- constructor(webrtc, signaling, analyticsCollector, config, emitter) {
1421
- this.audioRecorder = null;
1422
- this.isRecording = false;
1423
- this.webrtc = webrtc;
1424
- this.signaling = signaling;
1425
- this.analyticsCollector = analyticsCollector;
1426
- this.config = config;
1427
- this.emitter = emitter;
1428
- }
1429
- async startVideo() {
1430
- if (!this.config.enableVideo) {
1431
- throw new AStackError('Video is disabled in configuration', ErrorCodes.VIDEO_ERROR);
1432
- }
1433
- try {
1434
- const stream = await this.webrtc.getUserMedia();
1435
- this.webrtc.addLocalStream(stream);
1436
- this.emitter.emit('videoStarted');
1437
- this.analyticsCollector.trackEvent('video_started');
1438
- this.analyticsCollector.updateMediaTypes(['video']);
1439
- return stream;
1440
- }
1441
- catch (error) {
1442
- throw new AStackError(`Failed to start video: ${error}`, ErrorCodes.VIDEO_ERROR);
1443
- }
1444
- }
1445
- async stopVideo(localVideoRef) {
1446
- const localStreams = this.webrtc.getLocalStreams();
1447
- localStreams.forEach(stream => {
1448
- if (stream.kind === 'video') {
1449
- this.webrtc.removeLocalStream(stream.id);
1450
- }
1451
- });
1452
- if (localVideoRef) {
1453
- localVideoRef.srcObject = null;
1454
- }
1455
- this.emitter.emit('videoStopped');
1456
- }
1457
- async startAudio() {
1458
- if (this.config.enableAudio === false) {
1459
- throw new AStackError('Audio is disabled in configuration', ErrorCodes.AUDIO_ERROR);
1460
- }
1461
- try {
1462
- const stream = await this.webrtc.getUserMedia();
1463
- this.webrtc.addLocalStream(stream);
1464
- this.emitter.emit('audioStarted');
1465
- this.analyticsCollector.trackEvent('audio_started');
1466
- this.analyticsCollector.updateMediaTypes(['audio']);
1467
- return stream;
1468
- }
1469
- catch (error) {
1470
- throw new AStackError(`Failed to start audio: ${error}`, ErrorCodes.AUDIO_ERROR);
1471
- }
1472
- }
1473
- async stopAudio() {
1474
- const localStreams = this.webrtc.getLocalStreams();
1475
- localStreams.forEach(stream => {
1476
- if (stream.kind === 'audio') {
1477
- this.webrtc.removeLocalStream(stream.id);
1478
- }
1479
- });
1480
- if (this.isRecording) {
1481
- this.stopRecording();
1482
- }
1483
- this.emitter.emit('audioStopped');
1484
- }
1485
- async sendText(message) {
1486
- if (!this.signaling.isConnected()) {
1487
- throw new AStackError('Not connected to AStack', ErrorCodes.CONNECTION_LOST);
1488
- }
1489
- try {
1490
- this.signaling.sendTextMessage(message);
1491
- this.emitter.emit('messageSent', message);
1492
- }
1493
- catch (error) {
1494
- throw new AStackError(`Failed to send text message: ${error}`, ErrorCodes.SIGNALING_ERROR);
1495
- }
1496
- }
1497
- async sendAudio(audioBlob) {
1498
- if (!this.signaling.isConnected()) {
1499
- throw new AStackError('Not connected to AStack', ErrorCodes.CONNECTION_LOST);
1500
- }
1501
- try {
1502
- this.signaling.sendAudioData(audioBlob);
1503
- this.emitter.emit('messageSent', audioBlob);
1504
- }
1505
- catch (error) {
1506
- throw new AStackError(`Failed to send audio: ${error}`, ErrorCodes.AUDIO_ERROR);
1507
- }
1508
- }
1509
- startRecording() {
1510
- const localStreams = this.webrtc.getLocalStreams();
1511
- const audioStream = localStreams.find(s => s.kind === 'audio');
1512
- if (!audioStream?.track) {
1513
- throw new AStackError('No audio stream available for recording', ErrorCodes.AUDIO_ERROR);
1514
- }
1515
- const stream = new MediaStream([audioStream.track]);
1516
- this.audioRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
1517
- const audioChunks = [];
1518
- this.audioRecorder.ondataavailable = (event) => {
1519
- if (event.data.size > 0) {
1520
- audioChunks.push(event.data);
1521
- }
1522
- };
1523
- this.audioRecorder.onstop = () => {
1524
- const audioBlob = new Blob(audioChunks, { type: 'audio/webm' });
1525
- this.sendAudio(audioBlob);
1526
- };
1527
- this.audioRecorder.start();
1528
- this.isRecording = true;
1529
- }
1530
- stopRecording() {
1531
- if (this.audioRecorder && this.isRecording) {
1532
- this.audioRecorder.stop();
1533
- this.isRecording = false;
1534
- }
1535
- }
1536
- getIsRecording() {
1537
- return this.isRecording;
1538
- }
1539
- async getDevices() {
1540
- return this.webrtc.getMediaDevices();
1541
- }
1542
- async switchCamera(deviceId) {
1543
- throw new AStackError('Camera switching not yet implemented', ErrorCodes.VIDEO_ERROR);
1544
- }
1545
- async switchMicrophone(deviceId) {
1546
- return this.webrtc.switchAudioInput(deviceId);
1547
- }
1548
- muteAudio() {
1549
- this.webrtc.muteAudio();
1550
- }
1551
- unmuteAudio() {
1552
- this.webrtc.unmuteAudio();
1553
- }
1554
- pauseVideo() {
1555
- this.webrtc.muteVideo();
1556
- }
1557
- resumeVideo() {
1558
- this.webrtc.unmuteVideo();
1559
- }
1560
- getLocalStreams() {
1561
- return this.webrtc.getLocalStreams();
1562
- }
1563
- getRemoteStreams() {
1564
- return this.webrtc.getRemoteStreams();
1565
- }
1566
- }
1567
-
1568
- /**
1569
- * @deprecated AStackClient is deprecated. Use AStackCSRClient for WebSocket-based communication.
1570
- * This class is kept for backwards compatibility only.
1571
- *
1572
- * Example:
1573
- * ```typescript
1574
- * import { AStackCSRClient } from '@astack/client-sdk';
1575
- * const client = new AStackCSRClient({ apiEndpoint: 'https://api.astack.com' });
1576
- * ```
1577
- */
1578
- class AStackClient extends eventemitter3.EventEmitter {
1579
- constructor(config) {
1580
- super();
1581
- this.usageTracker = null;
1582
- this.sessionId = null;
1583
- this.localVideoRef = null;
1584
- this.remoteVideoRef = null;
1585
- if (!config.apiKey) {
1586
- throw new AStackError('API key is required', ErrorCodes.INVALID_API_KEY);
1587
- }
1588
- if (!config.supabaseUrl || !config.supabaseAnonKey) {
1589
- throw new AStackError('Supabase URL and Anon Key are required', ErrorCodes.AUTHENTICATION_FAILED);
1590
- }
1591
- this.config = {
1592
- enableVideo: false, enableAudio: true, enableText: true, autoReconnect: true,
1593
- maxRetries: 3, reconnectDelay: 1000, heartbeatInterval: 30000, connectionTimeout: 10000,
1594
- sessionExpirationWarning: 1, apiEndpoint: `${config.supabaseUrl}/functions/v1`, ...config
1595
- };
1596
- this.webrtc = new WebRTCManager(this.config);
1597
- this.signaling = new SupabaseSignalingClient(this.config);
1598
- this.sessionManager = new SessionManager(this.config.apiEndpoint, this.config.sessionExpirationWarning, (minutesRemaining) => this.emit('sessionExpiring', minutesRemaining));
1599
- this.billingMonitor = new BillingMonitor(this.config.apiEndpoint, this.config.apiKey);
1600
- this.billingMonitor.onAlert((alert) => this.emit('billingAlert', alert));
1601
- this.securityLogger = new SecurityLogger({
1602
- apiEndpoint: this.config.apiEndpoint, apiKey: this.config.apiKey,
1603
- organizationId: this.config.organizationId, enableLocalLogging: true
1604
- });
1605
- this.connectionStateManager = new ConnectionStateManager();
1606
- this.connectionStateManager.on('qualityChange', (quality) => {
1607
- this.analyticsCollector.updateConnectionQuality(quality.qualityScore);
1608
- });
1609
- this.performanceMonitor = new PerformanceMonitor({
1610
- apiEndpoint: this.config.apiEndpoint,
1611
- enableAutoReport: this.config.enablePerformanceReporting ?? true,
1612
- collectionInterval: this.config.performanceCollectionInterval ?? 5000,
1613
- reportingInterval: this.config.performanceReportingInterval ?? 30000
1614
- });
1615
- this.analyticsCollector = new AnalyticsCollector({
1616
- apiEndpoint: this.config.apiEndpoint, apiKey: this.config.apiKey,
1617
- organizationId: this.config.organizationId, userId: this.config.endUserId,
1618
- enableAutoFlush: this.config.enableAnalytics ?? true
1619
- });
1620
- this.mediaController = new AStackMediaController(this.webrtc, this.signaling, this.analyticsCollector, this.config, this);
1621
- this.status = { id: '', status: 'pending', connectionState: 'disconnected', iceConnectionState: 'new' };
1622
- this.initEventHandlers();
1623
- }
1624
- initEventHandlers() {
1625
- setupEventHandlers(this, {
1626
- webrtc: this.webrtc, signaling: this.signaling,
1627
- connectionStateManager: this.connectionStateManager, analyticsCollector: this.analyticsCollector,
1628
- securityLogger: this.securityLogger, performanceMonitor: this.performanceMonitor,
1629
- getUsageTracker: () => this.usageTracker, setUsageTracker: (t) => { this.usageTracker = t; },
1630
- getSessionId: () => this.sessionId, getStatus: () => this.status,
1631
- setStatus: (s) => { this.status = { ...this.status, ...s }; },
1632
- localVideoRef: this.localVideoRef, remoteVideoRef: this.remoteVideoRef,
1633
- startWebRTCStatsCollection: () => this.startWebRTCStatsCollection(),
1634
- stopWebRTCStatsCollection: () => this.stopWebRTCStatsCollection()
1635
- });
1636
- }
1637
- startWebRTCStatsCollection() {
1638
- this.stopWebRTCStatsCollection();
1639
- this.statsCollectionTimer = setInterval(async () => {
1640
- try {
1641
- const stats = await this.webrtc.getStats();
1642
- if (stats) {
1643
- const processed = this.processWebRTCStats(stats);
1644
- if (processed.audioLatency !== undefined)
1645
- this.performanceMonitor.recordLatency('audio', processed.audioLatency);
1646
- if (processed.videoLatency !== undefined)
1647
- this.performanceMonitor.recordLatency('video', processed.videoLatency);
1648
- if (processed.throughput) {
1649
- this.performanceMonitor.updateThroughput('audio', 'inbound', processed.throughput.audio.inbound);
1650
- this.performanceMonitor.updateThroughput('audio', 'outbound', processed.throughput.audio.outbound);
1651
- this.performanceMonitor.updateThroughput('video', 'inbound', processed.throughput.video.inbound);
1652
- this.performanceMonitor.updateThroughput('video', 'outbound', processed.throughput.video.outbound);
1653
- }
1654
- if (processed.quality)
1655
- this.performanceMonitor.updateQuality(processed.quality);
1656
- }
1657
- }
1658
- catch (error) { }
1659
- }, 5000);
1660
- }
1661
- stopWebRTCStatsCollection() {
1662
- if (this.statsCollectionTimer) {
1663
- clearInterval(this.statsCollectionTimer);
1664
- this.statsCollectionTimer = undefined;
1665
- }
1666
- }
1667
- processWebRTCStats(stats) {
1668
- return { throughput: { audio: { inbound: 0, outbound: 0 }, video: { inbound: 0, outbound: 0 } }, quality: {} };
1669
- }
1670
- async connect() {
1671
- try {
1672
- this.connectionStateManager.updateSignalingState('connecting');
1673
- const sessionRequest = {
1674
- apiKey: this.config.apiKey, organizationId: this.config.organizationId,
1675
- endUserId: this.config.endUserId,
1676
- metadata: { enableVideo: this.config.enableVideo, enableAudio: this.config.enableAudio },
1677
- workerPreferences: this.config.workerPreferences
1678
- };
1679
- const credentials = await this.sessionManager.createSession(sessionRequest);
1680
- this.sessionId = credentials.sessionId;
1681
- this.status = {
1682
- id: this.sessionId, status: 'connecting', connectionState: 'disconnected',
1683
- iceConnectionState: 'new', startTime: new Date()
1684
- };
1685
- this.billingMonitor.startMonitoring();
1686
- this.performanceMonitor.startCollection(this.sessionId);
1687
- this.analyticsCollector.startSession(this.sessionId);
1688
- await this.webrtc.initialize();
1689
- await this.signaling.connect(credentials.sessionId, credentials.channelName, credentials.wsToken);
1690
- }
1691
- catch (error) {
1692
- this.status.status = 'error';
1693
- this.status.lastError = error;
1694
- if (error instanceof AStackError) {
1695
- if (error.code === ErrorCodes.INSUFFICIENT_CREDITS) {
1696
- this.emit('billingAlert', { type: 'limit_exceeded', details: { error: error.message } });
1697
- }
1698
- else if (error.code === ErrorCodes.RATE_LIMIT_EXCEEDED) {
1699
- this.emit('rateLimitWarning', { limit: 0, current: 0, resetIn: 60 });
1700
- }
1701
- }
1702
- throw error;
1703
- }
1704
- }
1705
- async startVideo() { return this.mediaController.startVideo(); }
1706
- async stopVideo() { return this.mediaController.stopVideo(this.localVideoRef); }
1707
- async startAudio() { return this.mediaController.startAudio(); }
1708
- async stopAudio() { return this.mediaController.stopAudio(); }
1709
- async sendText(message) { return this.mediaController.sendText(message); }
1710
- async sendAudio(audioBlob) { return this.mediaController.sendAudio(audioBlob); }
1711
- startRecording() { this.mediaController.startRecording(); }
1712
- stopRecording() { this.mediaController.stopRecording(); }
1713
- async getDevices() { return this.mediaController.getDevices(); }
1714
- async switchCamera(deviceId) { return this.mediaController.switchCamera(deviceId); }
1715
- async switchMicrophone(deviceId) { return this.mediaController.switchMicrophone(deviceId); }
1716
- muteAudio() { this.mediaController.muteAudio(); }
1717
- unmuteAudio() { this.mediaController.unmuteAudio(); }
1718
- pauseVideo() { this.mediaController.pauseVideo(); }
1719
- resumeVideo() { this.mediaController.resumeVideo(); }
1720
- getSessionStatus() { return { ...this.status }; }
1721
- getLocalStreams() { return this.mediaController.getLocalStreams(); }
1722
- getRemoteStreams() { return this.mediaController.getRemoteStreams(); }
1723
- attachVideo(localVideo, remoteVideo) {
1724
- this.localVideoRef = localVideo;
1725
- this.remoteVideoRef = remoteVideo;
1726
- }
1727
- addEventListener(event, listener) {
1728
- return super.on(event, listener);
1729
- }
1730
- removeEventListener(event, listener) {
1731
- return super.off(event, listener);
1732
- }
1733
- async disconnect() {
1734
- this.status.status = 'disconnected';
1735
- if (this.mediaController.getIsRecording())
1736
- this.mediaController.stopRecording();
1737
- if (this.usageTracker) {
1738
- this.usageTracker.stop();
1739
- this.usageTracker.getMetrics();
1740
- }
1741
- this.billingMonitor.stopMonitoring();
1742
- this.performanceMonitor.stopCollection();
1743
- this.analyticsCollector.endSession();
1744
- this.connectionStateManager.stopHealthCheck();
1745
- this.connectionStateManager.updateSignalingState('disconnected');
1746
- await this.webrtc.close();
1747
- await this.signaling.disconnect();
1748
- this.sessionManager.clearSession();
1749
- if (this.localVideoRef)
1750
- this.localVideoRef.srcObject = null;
1751
- if (this.remoteVideoRef)
1752
- this.remoteVideoRef.srcObject = null;
1753
- this.emit('sessionEnded', this.sessionId);
1754
- this.sessionId = null;
1755
- this.usageTracker = null;
1756
- }
1757
- async renewSession() {
1758
- if (!this.sessionManager.isSessionValid()) {
1759
- throw new AStackError('No active session to renew', ErrorCodes.SESSION_EXPIRED);
1760
- }
1761
- try {
1762
- const credentials = await this.sessionManager.renewSession(this.config.apiKey);
1763
- await this.signaling.disconnect();
1764
- await this.signaling.connect(credentials.sessionId, credentials.channelName, credentials.wsToken);
1765
- this.sessionId = credentials.sessionId;
1766
- this.emit('sessionReady', this.sessionId);
1767
- }
1768
- catch (error) {
1769
- this.emit('error', error);
1770
- throw error;
1771
- }
1772
- }
1773
- getSessionTimeRemaining() { return this.sessionManager.getTimeUntilExpiration(); }
1774
- isSessionValid() { return this.sessionManager.isSessionValid(); }
1775
- getUsageMetrics() { return this.usageTracker?.getMetrics() || null; }
1776
- trackQualityScore(score) { this.usageTracker?.trackQualityScore(score); }
1777
- async getBillingInfo() { return this.billingMonitor.fetchBillingInfo(); }
1778
- getCurrentBillingInfo() { return this.billingMonitor.getCurrentBillingInfo(); }
1779
- setBillingWarningThresholds(thresholds) {
1780
- this.billingMonitor.setWarningThresholds(thresholds);
1781
- }
1782
- getConnectionState() { return this.connectionStateManager.getState(); }
1783
- getConnectionQuality() { return this.connectionStateManager.getQuality(); }
1784
- isHealthy() { return this.connectionStateManager.isHealthy(); }
1785
- getConnectionHistory() {
1786
- return this.connectionStateManager.getStateHistory();
1787
- }
1788
- getPerformanceMetrics() { return this.performanceMonitor.getMetrics(); }
1789
- getPerformanceBenchmarks() { return this.performanceMonitor.getBenchmarks(); }
1790
- recordPerformanceBenchmark(benchmark) {
1791
- this.performanceMonitor.recordBenchmark(benchmark);
1792
- }
1793
- recordLatency(type, latency) {
1794
- this.performanceMonitor.recordLatency(type, latency);
1795
- }
1796
- async reportPerformanceMetrics() { await this.performanceMonitor.reportMetrics(); }
1797
- setWorkerPreferences(preferences) { this.config.workerPreferences = preferences; }
1798
- getWorkerPreferences() { return this.config.workerPreferences; }
1799
- getAssignedWorkerInfo() { return this.sessionManager.getWorkerInfo() || undefined; }
1800
- trackEvent(eventType, metadata) {
1801
- this.analyticsCollector.trackEvent(eventType, metadata);
1802
- }
1803
- getSessionAnalytics() { return this.analyticsCollector.getSessionAnalytics(); }
1804
- async flushAnalytics() { await this.analyticsCollector.flush(); }
1805
- async destroy() {
1806
- await this.disconnect();
1807
- this.sessionManager.destroy();
1808
- this.billingMonitor.destroy();
1809
- this.connectionStateManager.destroy();
1810
- this.performanceMonitor.destroy();
1811
- this.analyticsCollector.destroy();
1812
- await this.securityLogger.destroy();
1813
- this.removeAllListeners();
1814
- }
1815
- }
1816
6
 
1817
7
  const ARKIT_BLENDSHAPES = [
1818
8
  'jawOpen', 'jawForward', 'jawLeft', 'jawRight',
@@ -1833,12 +23,17 @@ const ARKIT_BLENDSHAPES = [
1833
23
  ];
1834
24
  const BLENDSHAPE_COUNT = 52;
1835
25
 
26
+ const IDX_JAW_OPEN = ARKIT_BLENDSHAPES.indexOf('jawOpen');
27
+ const IDX_MOUTH_FUNNEL = ARKIT_BLENDSHAPES.indexOf('mouthFunnel');
28
+ const IDX_MOUTH_LOWER_DOWN_LEFT = ARKIT_BLENDSHAPES.indexOf('mouthLowerDownLeft');
29
+ const IDX_MOUTH_LOWER_DOWN_RIGHT = ARKIT_BLENDSHAPES.indexOf('mouthLowerDownRight');
1836
30
  class AudioPlayer extends eventemitter3.EventEmitter {
1837
31
  constructor(sampleRate = 24000) {
1838
32
  super();
1839
33
  this.audioContext = null;
1840
34
  this.audioQueue = [];
1841
35
  this.isPlaying = false;
36
+ this.animationFrameId = null;
1842
37
  this.sampleRate = sampleRate;
1843
38
  }
1844
39
  async ensureAudioContext() {
@@ -1851,16 +46,18 @@ class AudioPlayer extends eventemitter3.EventEmitter {
1851
46
  return this.audioContext;
1852
47
  }
1853
48
  enqueue(chunk) {
1854
- console.log('[AudioPlayer] Enqueue called, queue length before:', this.audioQueue.length, 'isPlaying:', this.isPlaying);
1855
49
  this.audioQueue.push(chunk);
1856
50
  if (!this.isPlaying) {
1857
- console.log('[AudioPlayer] Not playing, starting playNext');
1858
51
  this.playNext();
1859
52
  }
1860
53
  }
1861
54
  clearQueue() {
1862
55
  this.audioQueue = [];
1863
56
  this.isPlaying = false;
57
+ if (this.animationFrameId !== null) {
58
+ cancelAnimationFrame(this.animationFrameId);
59
+ this.animationFrameId = null;
60
+ }
1864
61
  this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
1865
62
  }
1866
63
  async playNext() {
@@ -1887,33 +84,28 @@ class AudioPlayer extends eventemitter3.EventEmitter {
1887
84
  const jawOpen = Math.min(amplitude * 3, 1);
1888
85
  const mouthOpen = Math.min(amplitude * 2.5, 0.8);
1889
86
  const frameData = new Array(BLENDSHAPE_COUNT).fill(0);
1890
- frameData[25] = jawOpen; // jawOpen
1891
- frameData[27] = mouthOpen; // mouthFunnel
1892
- frameData[29] = mouthOpen * 0.3; // mouthLowerDownLeft
1893
- frameData[30] = mouthOpen * 0.3; // mouthLowerDownRight
87
+ frameData[IDX_JAW_OPEN] = jawOpen;
88
+ frameData[IDX_MOUTH_FUNNEL] = mouthOpen;
89
+ frameData[IDX_MOUTH_LOWER_DOWN_LEFT] = mouthOpen * 0.3;
90
+ frameData[IDX_MOUTH_LOWER_DOWN_RIGHT] = mouthOpen * 0.3;
1894
91
  blendshapes.push(frameData);
1895
92
  }
1896
93
  return blendshapes;
1897
94
  }
1898
95
  async playChunk(chunk) {
1899
96
  try {
1900
- console.log('[AudioPlayer] playChunk called, audio byteLength:', chunk.audio.byteLength);
1901
97
  const ctx = await this.ensureAudioContext();
1902
- console.log('[AudioPlayer] AudioContext state:', ctx.state, 'sampleRate:', ctx.sampleRate);
1903
98
  const int16Array = new Int16Array(chunk.audio);
1904
99
  const floatArray = new Float32Array(int16Array.length);
1905
100
  for (let i = 0; i < int16Array.length; i++) {
1906
101
  floatArray[i] = int16Array[i] / 32768.0;
1907
102
  }
1908
- console.log('[AudioPlayer] Converted to float, samples:', floatArray.length);
1909
103
  const audioBuffer = ctx.createBuffer(1, floatArray.length, this.sampleRate);
1910
104
  audioBuffer.getChannelData(0).set(floatArray);
1911
105
  const source = ctx.createBufferSource();
1912
106
  source.buffer = audioBuffer;
1913
107
  source.connect(ctx.destination);
1914
- console.log('[AudioPlayer] Audio buffer duration:', audioBuffer.duration, 'seconds');
1915
108
  const hasBlendshapes = chunk.blendshapes && chunk.blendshapes.length > 0;
1916
- console.log('[AudioPlayer] Has blendshapes:', hasBlendshapes, 'generating amplitude fallback:', !hasBlendshapes);
1917
109
  const blendshapes = hasBlendshapes
1918
110
  ? chunk.blendshapes
1919
111
  : this.generateAmplitudeBlendshapes(floatArray);
@@ -1923,8 +115,13 @@ class AudioPlayer extends eventemitter3.EventEmitter {
1923
115
  this.isPlaying = true;
1924
116
  this.emit('playbackStarted');
1925
117
  const animate = () => {
118
+ if (!this.isPlaying) {
119
+ this.animationFrameId = null;
120
+ return;
121
+ }
1926
122
  const elapsed = performance.now() - startTime;
1927
123
  if (elapsed >= duration || frameIndex >= blendshapes.length) {
124
+ this.animationFrameId = null;
1928
125
  this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
1929
126
  this.emit('playbackEnded');
1930
127
  this.playNext();
@@ -1939,11 +136,10 @@ class AudioPlayer extends eventemitter3.EventEmitter {
1939
136
  this.emit('blendshapeUpdate', frame);
1940
137
  }
1941
138
  }
1942
- requestAnimationFrame(animate);
139
+ this.animationFrameId = requestAnimationFrame(animate);
1943
140
  };
1944
- console.log('[AudioPlayer] Starting audio playback');
1945
141
  source.start();
1946
- requestAnimationFrame(animate);
142
+ this.animationFrameId = requestAnimationFrame(animate);
1947
143
  }
1948
144
  catch (err) {
1949
145
  console.error('[AudioPlayer] Error in playChunk:', err);
@@ -1974,15 +170,24 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
1974
170
  this.imageCaptureInterval = null;
1975
171
  this.callStatus = 'idle';
1976
172
  this.currentBlendshapes = new Array(BLENDSHAPE_COUNT).fill(0);
173
+ this.reconnectAttempts = 0;
174
+ this.reconnectTimer = null;
175
+ this.intentionalClose = false;
176
+ this.hasConnected = false;
177
+ this.clientId = null;
178
+ this.pendingAuth = null;
1977
179
  this.config = {
1978
180
  workerUrl: config.workerUrl,
1979
181
  sessionToken: config.sessionToken || '',
1980
- sessionId: config.sessionId || '',
1981
182
  sampleRate: config.sampleRate || 24000,
1982
- providers: config.providers || { asr: 'self', llm: 'self', tts: 'self' },
1983
183
  fps: config.fps || 30,
1984
184
  enableImageCapture: config.enableImageCapture ?? true,
1985
- imageCaptureInterval: config.imageCaptureInterval || 5000
185
+ imageCaptureInterval: config.imageCaptureInterval || 5000,
186
+ autoReconnect: config.autoReconnect ?? true,
187
+ maxRetries: config.maxRetries ?? 5,
188
+ reconnectDelay: config.reconnectDelay ?? 1000,
189
+ audioProcessorUrl: config.audioProcessorUrl || '/audio-processor.js',
190
+ debug: config.debug ?? false,
1986
191
  };
1987
192
  this.audioPlayer = new AudioPlayer(this.config.sampleRate);
1988
193
  this.setupAudioPlayerEvents();
@@ -2003,32 +208,93 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2003
208
  });
2004
209
  }
2005
210
  async connect() {
211
+ this.intentionalClose = false;
212
+ return this.connectInternal(false);
213
+ }
214
+ connectInternal(isReconnect) {
2006
215
  return new Promise((resolve, reject) => {
2007
216
  const wsUrl = this.config.workerUrl.replace(/^http/, 'ws');
2008
217
  const url = new URL(wsUrl);
2009
- if (this.config.sessionToken) {
2010
- url.searchParams.set('token', this.config.sessionToken);
2011
- }
2012
- if (this.config.sessionId) {
2013
- url.searchParams.set('sessionId', this.config.sessionId);
218
+ if (url.protocol === 'ws:') {
219
+ console.warn('[CSR] Connecting over unencrypted ws:// — use wss:// in production');
2014
220
  }
221
+ const timeout = setTimeout(() => {
222
+ if (this.ws) {
223
+ this.ws.close();
224
+ this.ws = null;
225
+ }
226
+ const error = new Error('WebSocket connection timeout');
227
+ this.emit('error', error);
228
+ if (!isReconnect)
229
+ reject(error);
230
+ }, 30000);
2015
231
  this.ws = new WebSocket(url.toString());
2016
232
  this.ws.onopen = () => {
2017
- this.emit('connected');
2018
- resolve();
233
+ this.pendingAuth = {
234
+ resolve: () => {
235
+ clearTimeout(timeout);
236
+ this.reconnectAttempts = 0;
237
+ this.hasConnected = true;
238
+ if (isReconnect) {
239
+ if (this.callStatus === 'active' || this.callStatus === 'starting') {
240
+ this.stopCall();
241
+ this.emit('callStopped');
242
+ }
243
+ this.emit('reconnected');
244
+ }
245
+ else {
246
+ this.emit('connected');
247
+ }
248
+ resolve();
249
+ },
250
+ reject: (err) => {
251
+ clearTimeout(timeout);
252
+ this.pendingAuth = null;
253
+ if (this.ws) {
254
+ this.ws.close();
255
+ this.ws = null;
256
+ }
257
+ this.emit('error', err);
258
+ if (!isReconnect)
259
+ reject(err);
260
+ }
261
+ };
262
+ this.sendAuthentication();
2019
263
  };
2020
264
  this.ws.onclose = () => {
2021
- this.emit('disconnected');
2022
- this.callStatus = 'idle';
265
+ clearTimeout(timeout);
266
+ this.ws = null;
267
+ if (this.pendingAuth) {
268
+ this.pendingAuth.reject(new Error('WebSocket closed before authentication completed'));
269
+ this.pendingAuth = null;
270
+ }
271
+ if (this.intentionalClose) {
272
+ this.stopCall();
273
+ this.emit('disconnected');
274
+ return;
275
+ }
276
+ if (this.hasConnected && this.config.autoReconnect) {
277
+ this.attemptReconnect();
278
+ }
279
+ else {
280
+ this.stopCall();
281
+ this.emit('disconnected');
282
+ }
2023
283
  };
2024
- this.ws.onerror = (event) => {
284
+ this.ws.onerror = (_event) => {
285
+ clearTimeout(timeout);
2025
286
  const error = new Error('WebSocket connection error');
2026
287
  this.emit('error', error);
2027
- reject(error);
288
+ if (!isReconnect)
289
+ reject(error);
2028
290
  };
2029
291
  this.ws.onmessage = (event) => {
2030
292
  try {
2031
293
  const data = JSON.parse(event.data);
294
+ if (!data || typeof data !== 'object' || typeof data.type !== 'string') {
295
+ console.error('[CSR] Invalid message: missing type');
296
+ return;
297
+ }
2032
298
  this.handleMessage(data);
2033
299
  }
2034
300
  catch (err) {
@@ -2037,43 +303,123 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2037
303
  };
2038
304
  });
2039
305
  }
306
+ sendAuthentication() {
307
+ if (!this.config.sessionToken) {
308
+ if (this.pendingAuth) {
309
+ this.pendingAuth.reject(new Error('Missing sessionToken in config'));
310
+ }
311
+ return;
312
+ }
313
+ if (this.ws) {
314
+ this.ws.send(JSON.stringify({
315
+ type: 'authenticate',
316
+ sessionToken: this.config.sessionToken
317
+ }));
318
+ }
319
+ }
320
+ attemptReconnect() {
321
+ if (this.reconnectAttempts >= this.config.maxRetries) {
322
+ this.stopCall();
323
+ this.emit('error', new Error('Maximum reconnection attempts reached'));
324
+ this.emit('disconnected');
325
+ return;
326
+ }
327
+ this.reconnectAttempts++;
328
+ const delay = this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
329
+ this.emit('reconnecting', this.reconnectAttempts);
330
+ this.reconnectTimer = setTimeout(async () => {
331
+ this.reconnectTimer = null;
332
+ try {
333
+ await this.connectInternal(true);
334
+ }
335
+ catch {
336
+ this.attemptReconnect();
337
+ }
338
+ }, delay);
339
+ }
2040
340
  handleMessage(data) {
2041
- console.log('[CSR] Received message:', data.type);
341
+ if (this.config.debug)
342
+ console.log('[CSR] Received message:', data.type);
2042
343
  switch (data.type) {
2043
- case 'a2f_call_started':
344
+ case 'connected':
345
+ if (typeof data.clientId === 'string')
346
+ this.clientId = data.clientId;
347
+ break;
348
+ case 'authenticated':
349
+ if (this.pendingAuth) {
350
+ this.pendingAuth.resolve();
351
+ this.pendingAuth = null;
352
+ }
353
+ break;
354
+ case 'auth_error': {
355
+ const msg = typeof data.message === 'string' ? data.message : 'Authentication failed';
356
+ if (this.pendingAuth) {
357
+ this.pendingAuth.reject(new Error(msg));
358
+ this.pendingAuth = null;
359
+ }
360
+ else {
361
+ this.emit('error', new Error(msg));
362
+ }
363
+ break;
364
+ }
365
+ case 'call_started':
2044
366
  this.callStatus = 'active';
2045
367
  this.emit('callStarted');
2046
368
  break;
2047
- case 'a2f_call_stopped':
369
+ case 'call_stopped':
2048
370
  this.callStatus = 'idle';
2049
371
  this.emit('callStopped');
2050
372
  break;
2051
- case 'a2f_call_error':
373
+ case 'call_error':
2052
374
  this.callStatus = 'error';
2053
- this.emit('error', new Error(data.message));
375
+ this.emit('error', new Error(typeof data.message === 'string' ? data.message : 'Unknown error'));
376
+ break;
377
+ case 'call_interim':
378
+ if (typeof data.text !== 'string')
379
+ return;
380
+ this.emit('interim', data.text);
2054
381
  break;
2055
- case 'a2f_transcript':
382
+ case 'call_transcript':
383
+ if (typeof data.text !== 'string')
384
+ return;
2056
385
  this.emit('transcript', data.text);
2057
386
  break;
2058
- case 'a2f_response':
387
+ case 'call_response':
388
+ if (typeof data.text !== 'string')
389
+ return;
2059
390
  this.emit('response', data.text);
2060
391
  break;
392
+ case 'call_response_complete':
393
+ this.emit('responseComplete');
394
+ break;
395
+ case 'call_speech_started':
396
+ this.emit('speechStarted');
397
+ break;
398
+ case 'call_utterance_end':
399
+ this.emit('utteranceEnd');
400
+ break;
401
+ case 'call_asr_error':
402
+ this.emit('asrError', typeof data.message === 'string' ? data.message : 'Unknown ASR error');
403
+ break;
2061
404
  case 'call_chunk':
2062
- case 'a2f_call_chunk':
2063
- console.log('[CSR] Audio chunk received, has audio:', !!data.audio, 'has blendshapes:', !!data.blendshapes);
2064
- if (data.audio) {
405
+ if (this.config.debug)
406
+ console.log('[CSR] Audio chunk received, has audio:', !!data.audio, 'has blendshapes:', !!data.blendshapes);
407
+ if (typeof data.audio === 'string') {
2065
408
  try {
2066
409
  const binaryString = atob(data.audio);
2067
410
  const bytes = new Uint8Array(binaryString.length);
2068
411
  for (let i = 0; i < binaryString.length; i++) {
2069
412
  bytes[i] = binaryString.charCodeAt(i);
2070
413
  }
2071
- console.log('[CSR] Decoded audio bytes:', bytes.length);
414
+ if (this.config.debug)
415
+ console.log('[CSR] Decoded audio bytes:', bytes.length);
416
+ const blendshapes = Array.isArray(data.blendshapes) ? data.blendshapes : undefined;
2072
417
  const chunk = {
2073
418
  audio: bytes.buffer,
2074
- blendshapes: data.blendshapes
419
+ blendshapes
2075
420
  };
2076
- console.log('[CSR] Enqueueing audio chunk');
421
+ if (this.config.debug)
422
+ console.log('[CSR] Enqueueing audio chunk');
2077
423
  this.audioPlayer.enqueue(chunk);
2078
424
  }
2079
425
  catch (err) {
@@ -2084,15 +430,15 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2084
430
  console.warn('[CSR] Audio chunk missing audio data');
2085
431
  }
2086
432
  break;
2087
- case 'a2f_model_status':
433
+ case 'model_status':
2088
434
  this.emit('modelStatus', {
2089
- model_loaded: data.model_loaded,
2090
- blendshape_count: data.blendshape_count
435
+ model_loaded: typeof data.model_loaded === 'boolean' ? data.model_loaded : undefined,
436
+ blendshape_count: typeof data.blendshape_count === 'number' ? data.blendshape_count : undefined
2091
437
  });
2092
438
  break;
2093
439
  }
2094
440
  }
2095
- async startCall() {
441
+ async startCall(options) {
2096
442
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2097
443
  throw new Error('WebSocket not connected');
2098
444
  }
@@ -2103,7 +449,7 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2103
449
  this.audioPlayer.clearQueue();
2104
450
  try {
2105
451
  this.audioContext = new AudioContext({ sampleRate: 16000 });
2106
- await this.audioContext.audioWorklet.addModule('/audio-processor.js');
452
+ await this.audioContext.audioWorklet.addModule(this.config.audioProcessorUrl);
2107
453
  this.mediaStream = await navigator.mediaDevices.getUserMedia({
2108
454
  audio: {
2109
455
  sampleRate: 16000,
@@ -2131,17 +477,22 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2131
477
  }
2132
478
  }
2133
479
  this.ws.send(JSON.stringify({
2134
- type: 'a2f_call_start',
2135
- providers: this.config.providers,
2136
- fps: this.config.fps
480
+ type: 'call_start',
481
+ fps: this.config.fps,
482
+ ...options
2137
483
  }));
2138
484
  const source = this.audioContext.createMediaStreamSource(this.mediaStream);
2139
485
  this.audioProcessor = new AudioWorkletNode(this.audioContext, 'audio-processor');
2140
486
  this.audioProcessor.port.onmessage = (event) => {
2141
487
  if (event.data.type === 'audio_data' && this.ws?.readyState === WebSocket.OPEN) {
2142
- const base64String = btoa(String.fromCharCode(...new Uint8Array(event.data.audioBuffer)));
488
+ const bytes = new Uint8Array(event.data.audioBuffer);
489
+ let binary = '';
490
+ for (let i = 0; i < bytes.length; i += 8192) {
491
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
492
+ }
493
+ const base64String = btoa(binary);
2143
494
  this.ws.send(JSON.stringify({
2144
- type: 'a2f_call_audio',
495
+ type: 'call_audio',
2145
496
  audio: base64String
2146
497
  }));
2147
498
  }
@@ -2151,6 +502,22 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2151
502
  }
2152
503
  catch (err) {
2153
504
  this.callStatus = 'error';
505
+ if (this.ws?.readyState === WebSocket.OPEN) {
506
+ this.ws.send(JSON.stringify({ type: 'call_stop' }));
507
+ }
508
+ if (this.mediaStream) {
509
+ this.mediaStream.getTracks().forEach(track => track.stop());
510
+ this.mediaStream = null;
511
+ }
512
+ if (this.audioContext) {
513
+ this.audioContext.close();
514
+ this.audioContext = null;
515
+ }
516
+ if (this.videoRef) {
517
+ this.videoRef.srcObject = null;
518
+ this.videoRef = null;
519
+ }
520
+ this.imageCaptureCanvas = null;
2154
521
  throw err;
2155
522
  }
2156
523
  }
@@ -2178,7 +545,7 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2178
545
  const imageDataUrl = this.imageCaptureCanvas.toDataURL('image/jpeg', 0.7);
2179
546
  const base64Image = imageDataUrl.split(',')[1];
2180
547
  this.ws.send(JSON.stringify({
2181
- type: 'a2f_call_image',
548
+ type: 'call_image',
2182
549
  image: base64Image,
2183
550
  timestamp: Date.now()
2184
551
  }));
@@ -2189,7 +556,7 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2189
556
  this.imageCaptureInterval = null;
2190
557
  }
2191
558
  if (this.ws?.readyState === WebSocket.OPEN) {
2192
- this.ws.send(JSON.stringify({ type: 'a2f_call_stop' }));
559
+ this.ws.send(JSON.stringify({ type: 'call_stop' }));
2193
560
  }
2194
561
  if (this.videoRef) {
2195
562
  this.videoRef.srcObject = null;
@@ -2218,17 +585,39 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2218
585
  throw new Error('WebSocket not connected');
2219
586
  }
2220
587
  this.ws.send(JSON.stringify({
2221
- type: 'a2f_text_message',
588
+ type: 'call_text_input',
2222
589
  text: message
2223
590
  }));
2224
591
  }
2225
- disconnect() {
592
+ async disconnect() {
593
+ this.intentionalClose = true;
594
+ if (this.reconnectTimer) {
595
+ clearTimeout(this.reconnectTimer);
596
+ this.reconnectTimer = null;
597
+ }
598
+ this.reconnectAttempts = 0;
2226
599
  this.stopCall();
2227
600
  if (this.ws) {
601
+ await this.waitForFlush(this.ws);
2228
602
  this.ws.close();
2229
603
  this.ws = null;
2230
604
  }
2231
605
  }
606
+ waitForFlush(ws, timeoutMs = 100) {
607
+ if (ws.bufferedAmount === 0)
608
+ return Promise.resolve();
609
+ return new Promise((resolve) => {
610
+ const start = Date.now();
611
+ const check = () => {
612
+ if (ws.bufferedAmount === 0 || Date.now() - start >= timeoutMs) {
613
+ resolve();
614
+ return;
615
+ }
616
+ setTimeout(check, 10);
617
+ };
618
+ check();
619
+ });
620
+ }
2232
621
  getCallStatus() {
2233
622
  return this.callStatus;
2234
623
  }
@@ -2238,21 +627,44 @@ class AStackCSRClient extends eventemitter3.EventEmitter {
2238
627
  isConnected() {
2239
628
  return this.ws?.readyState === WebSocket.OPEN;
2240
629
  }
630
+ getReconnectAttempts() {
631
+ return this.reconnectAttempts;
632
+ }
2241
633
  async destroy() {
2242
- this.disconnect();
634
+ await this.disconnect();
2243
635
  await this.audioPlayer.destroy();
2244
636
  this.removeAllListeners();
2245
637
  }
2246
638
  }
2247
639
 
640
+ class AStackError extends Error {
641
+ constructor(message, code) {
642
+ super(message);
643
+ this.name = 'AStackError';
644
+ this.code = code;
645
+ }
646
+ }
647
+ const ErrorCodes = {
648
+ INVALID_TOKEN: 'INVALID_TOKEN',
649
+ AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED',
650
+ SESSION_EXPIRED: 'SESSION_EXPIRED',
651
+ SESSION_CREATION_FAILED: 'SESSION_CREATION_FAILED',
652
+ CONNECTION_LOST: 'CONNECTION_LOST',
653
+ SIGNALING_ERROR: 'SIGNALING_ERROR',
654
+ NETWORK_ERROR: 'NETWORK_ERROR',
655
+ MEDIA_ACCESS_DENIED: 'MEDIA_ACCESS_DENIED',
656
+ AUDIO_ERROR: 'AUDIO_ERROR',
657
+ VIDEO_ERROR: 'VIDEO_ERROR',
658
+ RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
659
+ INSUFFICIENT_CREDITS: 'INSUFFICIENT_CREDITS',
660
+ BILLING_ERROR: 'BILLING_ERROR',
661
+ VALIDATION_ERROR: 'VALIDATION_ERROR'
662
+ };
663
+
2248
664
  exports.ARKIT_BLENDSHAPES = ARKIT_BLENDSHAPES;
2249
665
  exports.AStackCSRClient = AStackCSRClient;
2250
- exports.AStackClient = AStackClient;
2251
666
  exports.AStackError = AStackError;
2252
667
  exports.AudioPlayer = AudioPlayer;
2253
668
  exports.BLENDSHAPE_COUNT = BLENDSHAPE_COUNT;
2254
669
  exports.ErrorCodes = ErrorCodes;
2255
- exports.SupabaseSignalingClient = SupabaseSignalingClient;
2256
- exports.WebRTCManager = WebRTCManager;
2257
- exports.default = AStackClient;
2258
- //# sourceMappingURL=index.js.map
670
+ exports.default = AStackCSRClient;