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