Skip to content

Commit b1f11fe

Browse files
JWT client
1 parent 41a816a commit b1f11fe

4 files changed

Lines changed: 121 additions & 27 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ dist-ssr
2222
*.sln
2323
*.sw?
2424

25-
CLAUDE.md
25+
CLAUDE.md
26+
.env

dist/widget.iife.js

Lines changed: 19 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ import { BuzzwaldWidget } from './widget.js';
1717
const config = window.BuzzwaldConfig || {};
1818

1919
// Validate required configuration (unless in mock mode)
20-
if (!config.mockMode && !config.token) {
21-
console.error('Buzzwald: token is required in window.BuzzwaldConfig');
22-
return;
23-
}
20+
// if (!config.mockMode && !config.token) {
21+
// console.error('Buzzwald: token is required in window.BuzzwaldConfig');
22+
// return;
23+
// }
2424

2525
if (!config.mockMode && !config.id) {
2626
console.error('Buzzwald: id is required in window.BuzzwaldConfig');

src/widget.js

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { 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+
37
export 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

Comments
 (0)