11import { VapiClient } from './vapi.js' ;
22
3+ // Configuration: API endpoint from env variable
4+ const API_BASE_URL = import . meta. env . VITE_API_BASE_URL ;
5+ const JWT_ENDPOINT = `${ API_BASE_URL } /api/auth/jwt` ;
6+
37export class BuzzwaldWidget {
48 constructor ( config = { } ) {
59 this . config = {
@@ -23,13 +27,14 @@ export class BuzzwaldWidget {
2327 this . init ( ) ;
2428 }
2529
26- init ( ) {
30+ async init ( ) {
2731 if ( this . isInitialized ) {
2832 console . warn ( 'Buzzwald widget is already initialized' ) ;
2933 return ;
3034 }
3135
3236 try {
37+ await this . ensureToken ( ) ;
3338 this . validateConfig ( ) ;
3439 this . checkBrowserSupport ( ) ;
3540 this . injectStyles ( ) ;
@@ -42,6 +47,41 @@ export class BuzzwaldWidget {
4247 }
4348 }
4449
50+ async ensureToken ( ) {
51+ if ( ! this . config . token ) {
52+ if ( ! this . config . id ) {
53+ throw new Error ( 'ID is required to fetch JWT token' ) ;
54+ }
55+ try {
56+ this . config . token = await this . fetchJwt ( this . config . id ) ;
57+ } catch ( err ) {
58+ throw new Error ( 'Failed to fetch authentication token: ' + ( err . message || err ) ) ;
59+ }
60+ }
61+ }
62+
63+ async fetchJwt ( assistant_id ) {
64+ try {
65+ const response = await fetch ( JWT_ENDPOINT , {
66+ method : 'POST' ,
67+ headers : {
68+ 'Content-Type' : 'application/json' ,
69+ } ,
70+ body : JSON . stringify ( { assistant_id } ) ,
71+ credentials : 'include' , // if you need cookies for auth, else remove
72+ } ) ;
73+ if ( ! response . ok ) {
74+ const error = await response . json ( ) . catch ( ( ) => ( { } ) ) ;
75+ throw new Error ( error . message || `Failed to fetch JWT: ${ response . status } ` ) ;
76+ }
77+ const data = await response . json ( ) ;
78+ return data . token ;
79+ } catch ( err ) {
80+ console . error ( 'JWT fetch error:' , err ) ;
81+ throw err ;
82+ }
83+ }
84+
4585 validateConfig ( ) {
4686 if ( ! this . config . token ) {
4787 throw new Error ( 'Token is required' ) ;
@@ -436,19 +476,72 @@ export class BuzzwaldWidget {
436476 token : this . config . token ,
437477 phoneNumber : this . config . phoneNumber
438478 } ) ;
479+ this . attachVapiListeners ( ) ;
480+ }
481+
482+ // Attach all Vapi event listeners in one place
483+ attachVapiListeners ( ) {
484+ // Remove previous listeners if needed (optional, depending on your VapiClient implementation)
485+ // If your VapiClient supports an 'off' method, you could remove old listeners here.
439486
440- // Listen to call state changes
441487 this . vapi . on ( 'call-start' , ( ) => this . updateCallState ( 'connecting' ) ) ;
442488 this . vapi . on ( 'speech-start' , ( ) => this . updateCallState ( 'connected' ) ) ;
443489 this . vapi . on ( 'call-end' , ( ) => {
444490 this . updateCallState ( 'ended' ) ;
445491 setTimeout ( ( ) => this . updateCallState ( 'idle' ) , 2000 ) ;
446492 } ) ;
447- this . vapi . on ( 'error' , ( error ) => {
493+ this . vapi . on ( 'error' , async ( error ) => {
448494 console . error ( 'Buzzwald: Vapi error' , error ) ;
495+ // Check for JWT expiry error from Vapi
496+ if (
497+ error &&
498+ error . type === 'start-method-error' &&
499+ error . error &&
500+ typeof error . error . message === 'string' &&
501+ error . error . message . includes ( 'JWT has expired' )
502+ ) {
503+ try {
504+ console . log ( 'JWT expired, fetching new token' ) ;
505+ this . config . token = await this . fetchJwt ( this . config . id ) ;
506+ // Re-instantiate VapiClient with new token
507+ if ( this . vapi ) {
508+ this . vapi . destroy ( ) ;
509+ }
510+ this . vapi = new VapiClient ( {
511+ id : this . config . id ,
512+ token : this . config . token ,
513+ phoneNumber : this . config . phoneNumber
514+ } ) ;
515+ this . attachVapiListeners ( ) ;
516+ // Optionally, you could retry the failed action here
517+ } catch ( err ) {
518+ this . showErrorMessage ( 'Session expired. Please refresh.' ) ;
519+ }
520+ }
449521 this . updateCallState ( 'ended' ) ;
450522 setTimeout ( ( ) => this . updateCallState ( 'idle' ) , 2000 ) ;
451523 } ) ;
524+ // Handle JWT expiry: listen for auth errors from VapiClient if supported
525+ if ( typeof this . vapi . on === 'function' ) {
526+ this . vapi . on ( 'authError' , async ( error ) => {
527+ if ( error && error . code === 'jwt_expired' ) {
528+ try {
529+ this . config . token = await this . fetchJwt ( this . config . id ) ;
530+ if ( this . vapi ) {
531+ this . vapi . destroy ( ) ;
532+ }
533+ this . vapi = new VapiClient ( {
534+ id : this . config . id ,
535+ token : this . config . token ,
536+ phoneNumber : this . config . phoneNumber
537+ } ) ;
538+ this . attachVapiListeners ( ) ;
539+ } catch ( err ) {
540+ this . showErrorMessage ( 'Session expired. Please refresh.' ) ;
541+ }
542+ }
543+ } ) ;
544+ }
452545 }
453546
454547 updateCallState ( state ) {
0 commit comments