@aether-stack-dev/client-sdk 1.0.0 → 1.1.1

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 +10 -10
  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.esm.js CHANGED
@@ -1,1814 +1,4 @@
1
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
2
 
1813
3
  const ARKIT_BLENDSHAPES = [
1814
4
  'jawOpen', 'jawForward', 'jawLeft', 'jawRight',
@@ -1829,12 +19,17 @@ const ARKIT_BLENDSHAPES = [
1829
19
  ];
1830
20
  const BLENDSHAPE_COUNT = 52;
1831
21
 
22
+ const IDX_JAW_OPEN = ARKIT_BLENDSHAPES.indexOf('jawOpen');
23
+ const IDX_MOUTH_FUNNEL = ARKIT_BLENDSHAPES.indexOf('mouthFunnel');
24
+ const IDX_MOUTH_LOWER_DOWN_LEFT = ARKIT_BLENDSHAPES.indexOf('mouthLowerDownLeft');
25
+ const IDX_MOUTH_LOWER_DOWN_RIGHT = ARKIT_BLENDSHAPES.indexOf('mouthLowerDownRight');
1832
26
  class AudioPlayer extends EventEmitter {
1833
27
  constructor(sampleRate = 24000) {
1834
28
  super();
1835
29
  this.audioContext = null;
1836
30
  this.audioQueue = [];
1837
31
  this.isPlaying = false;
32
+ this.animationFrameId = null;
1838
33
  this.sampleRate = sampleRate;
1839
34
  }
1840
35
  async ensureAudioContext() {
@@ -1847,16 +42,18 @@ class AudioPlayer extends EventEmitter {
1847
42
  return this.audioContext;
1848
43
  }
1849
44
  enqueue(chunk) {
1850
- console.log('[AudioPlayer] Enqueue called, queue length before:', this.audioQueue.length, 'isPlaying:', this.isPlaying);
1851
45
  this.audioQueue.push(chunk);
1852
46
  if (!this.isPlaying) {
1853
- console.log('[AudioPlayer] Not playing, starting playNext');
1854
47
  this.playNext();
1855
48
  }
1856
49
  }
1857
50
  clearQueue() {
1858
51
  this.audioQueue = [];
1859
52
  this.isPlaying = false;
53
+ if (this.animationFrameId !== null) {
54
+ cancelAnimationFrame(this.animationFrameId);
55
+ this.animationFrameId = null;
56
+ }
1860
57
  this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
1861
58
  }
1862
59
  async playNext() {
@@ -1883,33 +80,28 @@ class AudioPlayer extends EventEmitter {
1883
80
  const jawOpen = Math.min(amplitude * 3, 1);
1884
81
  const mouthOpen = Math.min(amplitude * 2.5, 0.8);
1885
82
  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
83
+ frameData[IDX_JAW_OPEN] = jawOpen;
84
+ frameData[IDX_MOUTH_FUNNEL] = mouthOpen;
85
+ frameData[IDX_MOUTH_LOWER_DOWN_LEFT] = mouthOpen * 0.3;
86
+ frameData[IDX_MOUTH_LOWER_DOWN_RIGHT] = mouthOpen * 0.3;
1890
87
  blendshapes.push(frameData);
1891
88
  }
1892
89
  return blendshapes;
1893
90
  }
1894
91
  async playChunk(chunk) {
1895
92
  try {
1896
- console.log('[AudioPlayer] playChunk called, audio byteLength:', chunk.audio.byteLength);
1897
93
  const ctx = await this.ensureAudioContext();
1898
- console.log('[AudioPlayer] AudioContext state:', ctx.state, 'sampleRate:', ctx.sampleRate);
1899
94
  const int16Array = new Int16Array(chunk.audio);
1900
95
  const floatArray = new Float32Array(int16Array.length);
1901
96
  for (let i = 0; i < int16Array.length; i++) {
1902
97
  floatArray[i] = int16Array[i] / 32768.0;
1903
98
  }
1904
- console.log('[AudioPlayer] Converted to float, samples:', floatArray.length);
1905
99
  const audioBuffer = ctx.createBuffer(1, floatArray.length, this.sampleRate);
1906
100
  audioBuffer.getChannelData(0).set(floatArray);
1907
101
  const source = ctx.createBufferSource();
1908
102
  source.buffer = audioBuffer;
1909
103
  source.connect(ctx.destination);
1910
- console.log('[AudioPlayer] Audio buffer duration:', audioBuffer.duration, 'seconds');
1911
104
  const hasBlendshapes = chunk.blendshapes && chunk.blendshapes.length > 0;
1912
- console.log('[AudioPlayer] Has blendshapes:', hasBlendshapes, 'generating amplitude fallback:', !hasBlendshapes);
1913
105
  const blendshapes = hasBlendshapes
1914
106
  ? chunk.blendshapes
1915
107
  : this.generateAmplitudeBlendshapes(floatArray);
@@ -1919,8 +111,13 @@ class AudioPlayer extends EventEmitter {
1919
111
  this.isPlaying = true;
1920
112
  this.emit('playbackStarted');
1921
113
  const animate = () => {
114
+ if (!this.isPlaying) {
115
+ this.animationFrameId = null;
116
+ return;
117
+ }
1922
118
  const elapsed = performance.now() - startTime;
1923
119
  if (elapsed >= duration || frameIndex >= blendshapes.length) {
120
+ this.animationFrameId = null;
1924
121
  this.emit('blendshapeUpdate', new Array(BLENDSHAPE_COUNT).fill(0));
1925
122
  this.emit('playbackEnded');
1926
123
  this.playNext();
@@ -1935,11 +132,10 @@ class AudioPlayer extends EventEmitter {
1935
132
  this.emit('blendshapeUpdate', frame);
1936
133
  }
1937
134
  }
1938
- requestAnimationFrame(animate);
135
+ this.animationFrameId = requestAnimationFrame(animate);
1939
136
  };
1940
- console.log('[AudioPlayer] Starting audio playback');
1941
137
  source.start();
1942
- requestAnimationFrame(animate);
138
+ this.animationFrameId = requestAnimationFrame(animate);
1943
139
  }
1944
140
  catch (err) {
1945
141
  console.error('[AudioPlayer] Error in playChunk:', err);
@@ -1970,15 +166,24 @@ class AStackCSRClient extends EventEmitter {
1970
166
  this.imageCaptureInterval = null;
1971
167
  this.callStatus = 'idle';
1972
168
  this.currentBlendshapes = new Array(BLENDSHAPE_COUNT).fill(0);
169
+ this.reconnectAttempts = 0;
170
+ this.reconnectTimer = null;
171
+ this.intentionalClose = false;
172
+ this.hasConnected = false;
173
+ this.clientId = null;
174
+ this.pendingAuth = null;
1973
175
  this.config = {
1974
176
  workerUrl: config.workerUrl,
1975
177
  sessionToken: config.sessionToken || '',
1976
- sessionId: config.sessionId || '',
1977
178
  sampleRate: config.sampleRate || 24000,
1978
- providers: config.providers || { asr: 'self', llm: 'self', tts: 'self' },
1979
179
  fps: config.fps || 30,
1980
180
  enableImageCapture: config.enableImageCapture ?? true,
1981
- imageCaptureInterval: config.imageCaptureInterval || 5000
181
+ imageCaptureInterval: config.imageCaptureInterval || 5000,
182
+ autoReconnect: config.autoReconnect ?? true,
183
+ maxRetries: config.maxRetries ?? 5,
184
+ reconnectDelay: config.reconnectDelay ?? 1000,
185
+ audioProcessorUrl: config.audioProcessorUrl || '/audio-processor.js',
186
+ debug: config.debug ?? false,
1982
187
  };
1983
188
  this.audioPlayer = new AudioPlayer(this.config.sampleRate);
1984
189
  this.setupAudioPlayerEvents();
@@ -1999,32 +204,93 @@ class AStackCSRClient extends EventEmitter {
1999
204
  });
2000
205
  }
2001
206
  async connect() {
207
+ this.intentionalClose = false;
208
+ return this.connectInternal(false);
209
+ }
210
+ connectInternal(isReconnect) {
2002
211
  return new Promise((resolve, reject) => {
2003
212
  const wsUrl = this.config.workerUrl.replace(/^http/, 'ws');
2004
213
  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);
214
+ if (url.protocol === 'ws:') {
215
+ console.warn('[CSR] Connecting over unencrypted ws:// — use wss:// in production');
2010
216
  }
217
+ const timeout = setTimeout(() => {
218
+ if (this.ws) {
219
+ this.ws.close();
220
+ this.ws = null;
221
+ }
222
+ const error = new Error('WebSocket connection timeout');
223
+ this.emit('error', error);
224
+ if (!isReconnect)
225
+ reject(error);
226
+ }, 30000);
2011
227
  this.ws = new WebSocket(url.toString());
2012
228
  this.ws.onopen = () => {
2013
- this.emit('connected');
2014
- resolve();
229
+ this.pendingAuth = {
230
+ resolve: () => {
231
+ clearTimeout(timeout);
232
+ this.reconnectAttempts = 0;
233
+ this.hasConnected = true;
234
+ if (isReconnect) {
235
+ if (this.callStatus === 'active' || this.callStatus === 'starting') {
236
+ this.stopCall();
237
+ this.emit('callStopped');
238
+ }
239
+ this.emit('reconnected');
240
+ }
241
+ else {
242
+ this.emit('connected');
243
+ }
244
+ resolve();
245
+ },
246
+ reject: (err) => {
247
+ clearTimeout(timeout);
248
+ this.pendingAuth = null;
249
+ if (this.ws) {
250
+ this.ws.close();
251
+ this.ws = null;
252
+ }
253
+ this.emit('error', err);
254
+ if (!isReconnect)
255
+ reject(err);
256
+ }
257
+ };
258
+ this.sendAuthentication();
2015
259
  };
2016
260
  this.ws.onclose = () => {
2017
- this.emit('disconnected');
2018
- this.callStatus = 'idle';
261
+ clearTimeout(timeout);
262
+ this.ws = null;
263
+ if (this.pendingAuth) {
264
+ this.pendingAuth.reject(new Error('WebSocket closed before authentication completed'));
265
+ this.pendingAuth = null;
266
+ }
267
+ if (this.intentionalClose) {
268
+ this.stopCall();
269
+ this.emit('disconnected');
270
+ return;
271
+ }
272
+ if (this.hasConnected && this.config.autoReconnect) {
273
+ this.attemptReconnect();
274
+ }
275
+ else {
276
+ this.stopCall();
277
+ this.emit('disconnected');
278
+ }
2019
279
  };
2020
- this.ws.onerror = (event) => {
280
+ this.ws.onerror = (_event) => {
281
+ clearTimeout(timeout);
2021
282
  const error = new Error('WebSocket connection error');
2022
283
  this.emit('error', error);
2023
- reject(error);
284
+ if (!isReconnect)
285
+ reject(error);
2024
286
  };
2025
287
  this.ws.onmessage = (event) => {
2026
288
  try {
2027
289
  const data = JSON.parse(event.data);
290
+ if (!data || typeof data !== 'object' || typeof data.type !== 'string') {
291
+ console.error('[CSR] Invalid message: missing type');
292
+ return;
293
+ }
2028
294
  this.handleMessage(data);
2029
295
  }
2030
296
  catch (err) {
@@ -2033,43 +299,123 @@ class AStackCSRClient extends EventEmitter {
2033
299
  };
2034
300
  });
2035
301
  }
302
+ sendAuthentication() {
303
+ if (!this.config.sessionToken) {
304
+ if (this.pendingAuth) {
305
+ this.pendingAuth.reject(new Error('Missing sessionToken in config'));
306
+ }
307
+ return;
308
+ }
309
+ if (this.ws) {
310
+ this.ws.send(JSON.stringify({
311
+ type: 'authenticate',
312
+ sessionToken: this.config.sessionToken
313
+ }));
314
+ }
315
+ }
316
+ attemptReconnect() {
317
+ if (this.reconnectAttempts >= this.config.maxRetries) {
318
+ this.stopCall();
319
+ this.emit('error', new Error('Maximum reconnection attempts reached'));
320
+ this.emit('disconnected');
321
+ return;
322
+ }
323
+ this.reconnectAttempts++;
324
+ const delay = this.config.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
325
+ this.emit('reconnecting', this.reconnectAttempts);
326
+ this.reconnectTimer = setTimeout(async () => {
327
+ this.reconnectTimer = null;
328
+ try {
329
+ await this.connectInternal(true);
330
+ }
331
+ catch {
332
+ this.attemptReconnect();
333
+ }
334
+ }, delay);
335
+ }
2036
336
  handleMessage(data) {
2037
- console.log('[CSR] Received message:', data.type);
337
+ if (this.config.debug)
338
+ console.log('[CSR] Received message:', data.type);
2038
339
  switch (data.type) {
2039
- case 'a2f_call_started':
340
+ case 'connected':
341
+ if (typeof data.clientId === 'string')
342
+ this.clientId = data.clientId;
343
+ break;
344
+ case 'authenticated':
345
+ if (this.pendingAuth) {
346
+ this.pendingAuth.resolve();
347
+ this.pendingAuth = null;
348
+ }
349
+ break;
350
+ case 'auth_error': {
351
+ const msg = typeof data.message === 'string' ? data.message : 'Authentication failed';
352
+ if (this.pendingAuth) {
353
+ this.pendingAuth.reject(new Error(msg));
354
+ this.pendingAuth = null;
355
+ }
356
+ else {
357
+ this.emit('error', new Error(msg));
358
+ }
359
+ break;
360
+ }
361
+ case 'call_started':
2040
362
  this.callStatus = 'active';
2041
363
  this.emit('callStarted');
2042
364
  break;
2043
- case 'a2f_call_stopped':
365
+ case 'call_stopped':
2044
366
  this.callStatus = 'idle';
2045
367
  this.emit('callStopped');
2046
368
  break;
2047
- case 'a2f_call_error':
369
+ case 'call_error':
2048
370
  this.callStatus = 'error';
2049
- this.emit('error', new Error(data.message));
371
+ this.emit('error', new Error(typeof data.message === 'string' ? data.message : 'Unknown error'));
372
+ break;
373
+ case 'call_interim':
374
+ if (typeof data.text !== 'string')
375
+ return;
376
+ this.emit('interim', data.text);
2050
377
  break;
2051
- case 'a2f_transcript':
378
+ case 'call_transcript':
379
+ if (typeof data.text !== 'string')
380
+ return;
2052
381
  this.emit('transcript', data.text);
2053
382
  break;
2054
- case 'a2f_response':
383
+ case 'call_response':
384
+ if (typeof data.text !== 'string')
385
+ return;
2055
386
  this.emit('response', data.text);
2056
387
  break;
388
+ case 'call_response_complete':
389
+ this.emit('responseComplete');
390
+ break;
391
+ case 'call_speech_started':
392
+ this.emit('speechStarted');
393
+ break;
394
+ case 'call_utterance_end':
395
+ this.emit('utteranceEnd');
396
+ break;
397
+ case 'call_asr_error':
398
+ this.emit('asrError', typeof data.message === 'string' ? data.message : 'Unknown ASR error');
399
+ break;
2057
400
  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) {
401
+ if (this.config.debug)
402
+ console.log('[CSR] Audio chunk received, has audio:', !!data.audio, 'has blendshapes:', !!data.blendshapes);
403
+ if (typeof data.audio === 'string') {
2061
404
  try {
2062
405
  const binaryString = atob(data.audio);
2063
406
  const bytes = new Uint8Array(binaryString.length);
2064
407
  for (let i = 0; i < binaryString.length; i++) {
2065
408
  bytes[i] = binaryString.charCodeAt(i);
2066
409
  }
2067
- console.log('[CSR] Decoded audio bytes:', bytes.length);
410
+ if (this.config.debug)
411
+ console.log('[CSR] Decoded audio bytes:', bytes.length);
412
+ const blendshapes = Array.isArray(data.blendshapes) ? data.blendshapes : undefined;
2068
413
  const chunk = {
2069
414
  audio: bytes.buffer,
2070
- blendshapes: data.blendshapes
415
+ blendshapes
2071
416
  };
2072
- console.log('[CSR] Enqueueing audio chunk');
417
+ if (this.config.debug)
418
+ console.log('[CSR] Enqueueing audio chunk');
2073
419
  this.audioPlayer.enqueue(chunk);
2074
420
  }
2075
421
  catch (err) {
@@ -2080,15 +426,15 @@ class AStackCSRClient extends EventEmitter {
2080
426
  console.warn('[CSR] Audio chunk missing audio data');
2081
427
  }
2082
428
  break;
2083
- case 'a2f_model_status':
429
+ case 'model_status':
2084
430
  this.emit('modelStatus', {
2085
- model_loaded: data.model_loaded,
2086
- blendshape_count: data.blendshape_count
431
+ model_loaded: typeof data.model_loaded === 'boolean' ? data.model_loaded : undefined,
432
+ blendshape_count: typeof data.blendshape_count === 'number' ? data.blendshape_count : undefined
2087
433
  });
2088
434
  break;
2089
435
  }
2090
436
  }
2091
- async startCall() {
437
+ async startCall(options) {
2092
438
  if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
2093
439
  throw new Error('WebSocket not connected');
2094
440
  }
@@ -2099,7 +445,7 @@ class AStackCSRClient extends EventEmitter {
2099
445
  this.audioPlayer.clearQueue();
2100
446
  try {
2101
447
  this.audioContext = new AudioContext({ sampleRate: 16000 });
2102
- await this.audioContext.audioWorklet.addModule('/audio-processor.js');
448
+ await this.audioContext.audioWorklet.addModule(this.config.audioProcessorUrl);
2103
449
  this.mediaStream = await navigator.mediaDevices.getUserMedia({
2104
450
  audio: {
2105
451
  sampleRate: 16000,
@@ -2127,17 +473,22 @@ class AStackCSRClient extends EventEmitter {
2127
473
  }
2128
474
  }
2129
475
  this.ws.send(JSON.stringify({
2130
- type: 'a2f_call_start',
2131
- providers: this.config.providers,
2132
- fps: this.config.fps
476
+ type: 'call_start',
477
+ fps: this.config.fps,
478
+ ...options
2133
479
  }));
2134
480
  const source = this.audioContext.createMediaStreamSource(this.mediaStream);
2135
481
  this.audioProcessor = new AudioWorkletNode(this.audioContext, 'audio-processor');
2136
482
  this.audioProcessor.port.onmessage = (event) => {
2137
483
  if (event.data.type === 'audio_data' && this.ws?.readyState === WebSocket.OPEN) {
2138
- const base64String = btoa(String.fromCharCode(...new Uint8Array(event.data.audioBuffer)));
484
+ const bytes = new Uint8Array(event.data.audioBuffer);
485
+ let binary = '';
486
+ for (let i = 0; i < bytes.length; i += 8192) {
487
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 8192));
488
+ }
489
+ const base64String = btoa(binary);
2139
490
  this.ws.send(JSON.stringify({
2140
- type: 'a2f_call_audio',
491
+ type: 'call_audio',
2141
492
  audio: base64String
2142
493
  }));
2143
494
  }
@@ -2147,6 +498,22 @@ class AStackCSRClient extends EventEmitter {
2147
498
  }
2148
499
  catch (err) {
2149
500
  this.callStatus = 'error';
501
+ if (this.ws?.readyState === WebSocket.OPEN) {
502
+ this.ws.send(JSON.stringify({ type: 'call_stop' }));
503
+ }
504
+ if (this.mediaStream) {
505
+ this.mediaStream.getTracks().forEach(track => track.stop());
506
+ this.mediaStream = null;
507
+ }
508
+ if (this.audioContext) {
509
+ this.audioContext.close();
510
+ this.audioContext = null;
511
+ }
512
+ if (this.videoRef) {
513
+ this.videoRef.srcObject = null;
514
+ this.videoRef = null;
515
+ }
516
+ this.imageCaptureCanvas = null;
2150
517
  throw err;
2151
518
  }
2152
519
  }
@@ -2174,7 +541,7 @@ class AStackCSRClient extends EventEmitter {
2174
541
  const imageDataUrl = this.imageCaptureCanvas.toDataURL('image/jpeg', 0.7);
2175
542
  const base64Image = imageDataUrl.split(',')[1];
2176
543
  this.ws.send(JSON.stringify({
2177
- type: 'a2f_call_image',
544
+ type: 'call_image',
2178
545
  image: base64Image,
2179
546
  timestamp: Date.now()
2180
547
  }));
@@ -2185,7 +552,7 @@ class AStackCSRClient extends EventEmitter {
2185
552
  this.imageCaptureInterval = null;
2186
553
  }
2187
554
  if (this.ws?.readyState === WebSocket.OPEN) {
2188
- this.ws.send(JSON.stringify({ type: 'a2f_call_stop' }));
555
+ this.ws.send(JSON.stringify({ type: 'call_stop' }));
2189
556
  }
2190
557
  if (this.videoRef) {
2191
558
  this.videoRef.srcObject = null;
@@ -2214,17 +581,39 @@ class AStackCSRClient extends EventEmitter {
2214
581
  throw new Error('WebSocket not connected');
2215
582
  }
2216
583
  this.ws.send(JSON.stringify({
2217
- type: 'a2f_text_message',
584
+ type: 'call_text_input',
2218
585
  text: message
2219
586
  }));
2220
587
  }
2221
- disconnect() {
588
+ async disconnect() {
589
+ this.intentionalClose = true;
590
+ if (this.reconnectTimer) {
591
+ clearTimeout(this.reconnectTimer);
592
+ this.reconnectTimer = null;
593
+ }
594
+ this.reconnectAttempts = 0;
2222
595
  this.stopCall();
2223
596
  if (this.ws) {
597
+ await this.waitForFlush(this.ws);
2224
598
  this.ws.close();
2225
599
  this.ws = null;
2226
600
  }
2227
601
  }
602
+ waitForFlush(ws, timeoutMs = 100) {
603
+ if (ws.bufferedAmount === 0)
604
+ return Promise.resolve();
605
+ return new Promise((resolve) => {
606
+ const start = Date.now();
607
+ const check = () => {
608
+ if (ws.bufferedAmount === 0 || Date.now() - start >= timeoutMs) {
609
+ resolve();
610
+ return;
611
+ }
612
+ setTimeout(check, 10);
613
+ };
614
+ check();
615
+ });
616
+ }
2228
617
  getCallStatus() {
2229
618
  return this.callStatus;
2230
619
  }
@@ -2234,12 +623,38 @@ class AStackCSRClient extends EventEmitter {
2234
623
  isConnected() {
2235
624
  return this.ws?.readyState === WebSocket.OPEN;
2236
625
  }
626
+ getReconnectAttempts() {
627
+ return this.reconnectAttempts;
628
+ }
2237
629
  async destroy() {
2238
- this.disconnect();
630
+ await this.disconnect();
2239
631
  await this.audioPlayer.destroy();
2240
632
  this.removeAllListeners();
2241
633
  }
2242
634
  }
2243
635
 
2244
- export { ARKIT_BLENDSHAPES, AStackCSRClient, AStackClient, AStackError, AudioPlayer, BLENDSHAPE_COUNT, ErrorCodes, SupabaseSignalingClient, WebRTCManager, AStackClient as default };
2245
- //# sourceMappingURL=index.esm.js.map
636
+ class AStackError extends Error {
637
+ constructor(message, code) {
638
+ super(message);
639
+ this.name = 'AStackError';
640
+ this.code = code;
641
+ }
642
+ }
643
+ const ErrorCodes = {
644
+ INVALID_TOKEN: 'INVALID_TOKEN',
645
+ AUTHENTICATION_FAILED: 'AUTHENTICATION_FAILED',
646
+ SESSION_EXPIRED: 'SESSION_EXPIRED',
647
+ SESSION_CREATION_FAILED: 'SESSION_CREATION_FAILED',
648
+ CONNECTION_LOST: 'CONNECTION_LOST',
649
+ SIGNALING_ERROR: 'SIGNALING_ERROR',
650
+ NETWORK_ERROR: 'NETWORK_ERROR',
651
+ MEDIA_ACCESS_DENIED: 'MEDIA_ACCESS_DENIED',
652
+ AUDIO_ERROR: 'AUDIO_ERROR',
653
+ VIDEO_ERROR: 'VIDEO_ERROR',
654
+ RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED',
655
+ INSUFFICIENT_CREDITS: 'INSUFFICIENT_CREDITS',
656
+ BILLING_ERROR: 'BILLING_ERROR',
657
+ VALIDATION_ERROR: 'VALIDATION_ERROR'
658
+ };
659
+
660
+ export { ARKIT_BLENDSHAPES, AStackCSRClient, AStackError, AudioPlayer, BLENDSHAPE_COUNT, ErrorCodes, AStackCSRClient as default };