@makemore/agent-frontend 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -175,6 +175,124 @@ See `django-tts-example.py` for the complete Django backend implementation.
175
175
  | `showVoiceSettings` | boolean | `true` | Show voice settings button in header (works with proxy and direct API) |
176
176
  | `showExpandButton` | boolean | `true` | Show expand/minimize button in header |
177
177
  | `onEvent` | function | `null` | Callback for SSE events: `(eventType, payload) => void` |
178
+ | `authStrategy` | string | `null` | Auth strategy: `'token'`, `'jwt'`, `'session'`, `'anonymous'`, `'none'` (auto-detected if null) |
179
+ | `authToken` | string | `null` | Token value for `'token'` or `'jwt'` strategies |
180
+ | `authHeader` | string | `null` | Custom header name (defaults based on strategy) |
181
+ | `authTokenPrefix` | string | `null` | Custom token prefix (defaults based on strategy) |
182
+ | `anonymousSessionEndpoint` | string | `null` | Endpoint for anonymous session (defaults to `apiPaths.anonymousSession`) |
183
+ | `anonymousTokenKey` | string | `'chat_widget_anonymous_token'` | localStorage key for anonymous token |
184
+ | `onAuthError` | function | `null` | Callback for auth errors: `(error) => void` |
185
+
186
+ ### Authentication
187
+
188
+ The widget supports multiple authentication strategies with sensible defaults:
189
+
190
+ #### Token Authentication (Django REST Framework)
191
+
192
+ ```javascript
193
+ ChatWidget.init({
194
+ backendUrl: 'https://api.example.com',
195
+ agentKey: 'my-agent',
196
+ authStrategy: 'token',
197
+ authToken: 'abc123...',
198
+ // Sends: Authorization: Token abc123...
199
+ });
200
+ ```
201
+
202
+ #### JWT/Bearer Authentication
203
+
204
+ ```javascript
205
+ ChatWidget.init({
206
+ backendUrl: 'https://api.example.com',
207
+ agentKey: 'my-agent',
208
+ authStrategy: 'jwt',
209
+ authToken: 'eyJ...',
210
+ // Sends: Authorization: Bearer eyJ...
211
+ });
212
+ ```
213
+
214
+ #### Session-Based Authentication (Cookies)
215
+
216
+ ```javascript
217
+ ChatWidget.init({
218
+ backendUrl: 'https://api.example.com',
219
+ agentKey: 'my-agent',
220
+ authStrategy: 'session',
221
+ // Sends requests with credentials: 'include'
222
+ // No auth header, relies on session cookie
223
+ });
224
+ ```
225
+
226
+ #### Anonymous Session Tokens
227
+
228
+ ```javascript
229
+ ChatWidget.init({
230
+ backendUrl: 'https://api.example.com',
231
+ agentKey: 'my-agent',
232
+ authStrategy: 'anonymous',
233
+ anonymousSessionEndpoint: '/api/accounts/anonymous-session/',
234
+ // On first request: fetches anonymous token from endpoint
235
+ // Persists token to localStorage
236
+ // Sends: X-Anonymous-Token: {token}
237
+ });
238
+ ```
239
+
240
+ #### No Authentication (Public Endpoints)
241
+
242
+ ```javascript
243
+ ChatWidget.init({
244
+ backendUrl: 'https://api.example.com',
245
+ agentKey: 'my-agent',
246
+ authStrategy: 'none',
247
+ // No auth headers sent
248
+ });
249
+ ```
250
+
251
+ #### Custom Headers and Prefixes
252
+
253
+ ```javascript
254
+ ChatWidget.init({
255
+ authStrategy: 'token',
256
+ authToken: 'mytoken123',
257
+ authHeader: 'X-API-Key', // Custom header name
258
+ authTokenPrefix: '', // No prefix (just the token)
259
+ // Sends: X-API-Key: mytoken123
260
+ });
261
+ ```
262
+
263
+ #### Dynamic Token Updates
264
+
265
+ Update authentication after initialization (e.g., after user login):
266
+
267
+ ```javascript
268
+ // After user logs in
269
+ ChatWidget.setAuth({
270
+ strategy: 'jwt',
271
+ token: 'new-jwt-token-after-login'
272
+ });
273
+
274
+ // After user logs out
275
+ ChatWidget.clearAuth();
276
+
277
+ // Handle token refresh on auth errors
278
+ ChatWidget.init({
279
+ authStrategy: 'jwt',
280
+ authToken: initialToken,
281
+ onAuthError: async (error) => {
282
+ if (error.status === 401) {
283
+ const newToken = await refreshToken();
284
+ ChatWidget.setAuth({ token: newToken });
285
+ }
286
+ }
287
+ });
288
+ ```
289
+
290
+ #### Auto-Detection
291
+
292
+ If no `authStrategy` is specified, the widget auto-detects based on config:
293
+ - If `authToken` is provided → uses `'token'` strategy
294
+ - If `anonymousSessionEndpoint` or `apiPaths.anonymousSession` is configured → uses `'anonymous'` strategy
295
+ - Otherwise → uses `'none'`
178
296
 
179
297
  ### Event Callback
180
298
 
@@ -477,6 +595,10 @@ ChatWidget.setAutoRunMode('automatic'); // 'automatic', 'confirm', or 'manual'
477
595
  // Change auto-run delay (in milliseconds)
478
596
  ChatWidget.setAutoRunDelay(2000);
479
597
 
598
+ // Authentication methods
599
+ ChatWidget.setAuth({ strategy: 'jwt', token: 'new-token' }); // Update auth
600
+ ChatWidget.clearAuth(); // Clear authentication
601
+
480
602
  // Remove the widget from the page
481
603
  ChatWidget.destroy();
482
604
 
@@ -33,9 +33,18 @@
33
33
  placeholder: 'Type your message...',
34
34
  emptyStateTitle: 'Start a Conversation',
35
35
  emptyStateMessage: 'Send a message to get started.',
36
+ // Authentication configuration
37
+ authStrategy: null, // 'token' | 'jwt' | 'session' | 'anonymous' | 'none' (auto-detected if null)
38
+ authToken: null, // Token value for 'token' or 'jwt' strategies
39
+ authHeader: null, // Custom header name (defaults based on strategy)
40
+ authTokenPrefix: null, // Custom token prefix (defaults based on strategy)
41
+ anonymousSessionEndpoint: null, // Endpoint for anonymous session (defaults to apiPaths.anonymousSession)
42
+ anonymousTokenKey: 'chat_widget_anonymous_token', // Storage key for anonymous token
43
+ onAuthError: null, // Callback for auth errors: (error) => void
44
+ // Legacy config (deprecated but still supported)
36
45
  anonymousTokenHeader: 'X-Anonymous-Token',
37
46
  conversationIdKey: 'chat_widget_conversation_id',
38
- sessionTokenKey: 'chat_widget_session_token',
47
+ sessionTokenKey: 'chat_widget_session_token', // Deprecated: use anonymousTokenKey
39
48
  // API endpoint paths (can be customized for different backend setups)
40
49
  apiPaths: {
41
50
  anonymousSession: '/api/accounts/anonymous-session/',
@@ -87,7 +96,8 @@
87
96
  journeyType: 'general',
88
97
  messages: [],
89
98
  conversationId: null,
90
- sessionToken: null,
99
+ sessionToken: null, // Deprecated: use authToken
100
+ authToken: null, // Current auth token (for token/jwt/anonymous strategies)
91
101
  error: null,
92
102
  eventSource: null,
93
103
  currentAudio: null,
@@ -219,17 +229,16 @@
219
229
 
220
230
  if (config.ttsProxyUrl) {
221
231
  // Use Django proxy
222
- response = await fetch(config.ttsProxyUrl, {
232
+ response = await fetch(config.ttsProxyUrl, getFetchOptions({
223
233
  method: 'POST',
224
234
  headers: {
225
235
  'Content-Type': 'application/json',
226
- ...(state.sessionToken ? { [config.anonymousTokenHeader]: state.sessionToken } : {}),
227
236
  },
228
237
  body: JSON.stringify({
229
238
  text: text,
230
239
  role: role,
231
240
  }),
232
- });
241
+ }));
233
242
  } else {
234
243
  // Direct ElevenLabs API call
235
244
  const voiceId = role === 'assistant' ? config.ttsVoices.assistant : config.ttsVoices.user;
@@ -308,19 +317,15 @@
308
317
  // If using proxy, notify backend of voice change
309
318
  if (config.ttsProxyUrl) {
310
319
  try {
311
- const token = await getOrCreateSession();
312
- const headers = {
313
- 'Content-Type': 'application/json',
314
- };
315
- if (token) {
316
- headers[config.anonymousTokenHeader] = token;
317
- }
320
+ await getOrCreateSession();
318
321
 
319
- await fetch(`${config.backendUrl}${config.apiPaths.ttsSetVoice}`, {
322
+ await fetch(`${config.backendUrl}${config.apiPaths.ttsSetVoice}`, getFetchOptions({
320
323
  method: 'POST',
321
- headers,
324
+ headers: {
325
+ 'Content-Type': 'application/json',
326
+ },
322
327
  body: JSON.stringify({ role, voice_id: voiceId }),
323
- });
328
+ }));
324
329
  } catch (err) {
325
330
  console.error('[ChatWidget] Failed to set voice on backend:', err);
326
331
  }
@@ -335,15 +340,9 @@
335
340
 
336
341
  if (config.ttsProxyUrl) {
337
342
  // Fetch voices from Django backend
338
- const token = await getOrCreateSession();
339
- const headers = {};
340
- if (token) {
341
- headers[config.anonymousTokenHeader] = token;
342
- }
343
+ await getOrCreateSession();
343
344
 
344
- const response = await fetch(`${config.backendUrl}${config.apiPaths.ttsVoices}`, {
345
- headers,
346
- });
345
+ const response = await fetch(`${config.backendUrl}${config.apiPaths.ttsVoices}`, getFetchOptions());
347
346
 
348
347
  if (response.ok) {
349
348
  const data = await response.json();
@@ -370,34 +369,162 @@
370
369
  }
371
370
  }
372
371
 
372
+ // ============================================================================
373
+ // Authentication
374
+ // ============================================================================
375
+
376
+ /**
377
+ * Determine the effective auth strategy based on config
378
+ */
379
+ function getAuthStrategy() {
380
+ if (config.authStrategy) {
381
+ return config.authStrategy;
382
+ }
383
+
384
+ // Auto-detect strategy based on config
385
+ if (config.authToken) {
386
+ return 'token'; // Default to token auth if token provided
387
+ }
388
+
389
+ // Check for legacy anonymous session config
390
+ if (config.apiPaths.anonymousSession || config.anonymousSessionEndpoint) {
391
+ return 'anonymous';
392
+ }
393
+
394
+ return 'none';
395
+ }
396
+
397
+ /**
398
+ * Get auth headers based on current strategy
399
+ */
400
+ function getAuthHeaders() {
401
+ const strategy = getAuthStrategy();
402
+ const headers = {};
403
+
404
+ switch (strategy) {
405
+ case 'token': {
406
+ const token = config.authToken || state.authToken;
407
+ if (token) {
408
+ const headerName = config.authHeader || 'Authorization';
409
+ const prefix = config.authTokenPrefix !== undefined ? config.authTokenPrefix : 'Token';
410
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
411
+ }
412
+ break;
413
+ }
414
+
415
+ case 'jwt': {
416
+ const token = config.authToken || state.authToken;
417
+ if (token) {
418
+ const headerName = config.authHeader || 'Authorization';
419
+ const prefix = config.authTokenPrefix !== undefined ? config.authTokenPrefix : 'Bearer';
420
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
421
+ }
422
+ break;
423
+ }
424
+
425
+ case 'anonymous': {
426
+ const token = state.authToken || state.sessionToken; // Support legacy sessionToken
427
+ if (token) {
428
+ const headerName = config.authHeader || config.anonymousTokenHeader || 'X-Anonymous-Token';
429
+ headers[headerName] = token;
430
+ }
431
+ break;
432
+ }
433
+
434
+ case 'session':
435
+ // Session auth uses cookies, no headers needed
436
+ break;
437
+
438
+ case 'none':
439
+ // No auth
440
+ break;
441
+ }
442
+
443
+ return headers;
444
+ }
445
+
446
+ /**
447
+ * Get fetch options including auth credentials
448
+ */
449
+ function getFetchOptions(options = {}) {
450
+ const strategy = getAuthStrategy();
451
+ const fetchOptions = { ...options };
452
+
453
+ // Add auth headers
454
+ fetchOptions.headers = {
455
+ ...fetchOptions.headers,
456
+ ...getAuthHeaders(),
457
+ };
458
+
459
+ // For session auth, include credentials
460
+ if (strategy === 'session') {
461
+ fetchOptions.credentials = 'include';
462
+ }
463
+
464
+ return fetchOptions;
465
+ }
466
+
467
+ /**
468
+ * Handle auth errors (401, 403)
469
+ */
470
+ function handleAuthError(error, response) {
471
+ if (config.onAuthError && typeof config.onAuthError === 'function') {
472
+ const authError = new Error(error.message || 'Authentication failed');
473
+ authError.status = response?.status;
474
+ authError.response = response;
475
+ config.onAuthError(authError);
476
+ }
477
+ }
478
+
373
479
  // ============================================================================
374
480
  // Session Management
375
481
  // ============================================================================
376
482
 
377
483
  async function getOrCreateSession() {
484
+ const strategy = getAuthStrategy();
485
+
486
+ // For non-anonymous strategies, return the configured token
487
+ if (strategy !== 'anonymous') {
488
+ return config.authToken || state.authToken;
489
+ }
490
+
491
+ // Anonymous strategy: get or create anonymous token
492
+ if (state.authToken) {
493
+ return state.authToken;
494
+ }
495
+
496
+ // Support legacy sessionToken
378
497
  if (state.sessionToken) {
379
- return state.sessionToken;
498
+ state.authToken = state.sessionToken;
499
+ return state.authToken;
380
500
  }
381
501
 
382
502
  // Try to restore from storage
383
- const stored = getStoredValue(config.sessionTokenKey);
503
+ const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
504
+ const stored = getStoredValue(storageKey);
384
505
  if (stored) {
385
- state.sessionToken = stored;
506
+ state.authToken = stored;
507
+ state.sessionToken = stored; // Keep legacy field in sync
386
508
  return stored;
387
509
  }
388
510
 
389
511
  // Create new anonymous session
390
512
  try {
391
- const response = await fetch(`${config.backendUrl}${config.apiPaths.anonymousSession}`, {
513
+ const endpoint = config.anonymousSessionEndpoint || config.apiPaths.anonymousSession;
514
+ const response = await fetch(`${config.backendUrl}${endpoint}`, {
392
515
  method: 'POST',
393
516
  headers: { 'Content-Type': 'application/json' },
394
517
  });
395
518
 
396
519
  if (response.ok) {
397
520
  const data = await response.json();
398
- state.sessionToken = data.token;
399
- setStoredValue(config.sessionTokenKey, data.token);
400
- return data.token;
521
+ const token = data.token;
522
+ state.authToken = token;
523
+ state.sessionToken = token; // Keep legacy field in sync
524
+ setStoredValue(storageKey, token);
525
+ return token;
526
+ } else if (response.status === 401 || response.status === 403) {
527
+ handleAuthError(new Error('Failed to create anonymous session'), response);
401
528
  }
402
529
  } catch (e) {
403
530
  console.warn('[ChatWidget] Failed to create session:', e);
@@ -428,31 +555,35 @@
428
555
  render();
429
556
 
430
557
  try {
558
+ // Get auth token (if using anonymous strategy)
431
559
  const token = await getOrCreateSession();
432
- const headers = { 'Content-Type': 'application/json' };
433
- if (token) {
434
- headers[config.anonymousTokenHeader] = token;
435
- }
436
560
 
437
561
  // Restore conversation ID from storage if not set
438
562
  if (!state.conversationId) {
439
563
  state.conversationId = getStoredValue(config.conversationIdKey);
440
564
  }
441
565
 
442
- const response = await fetch(`${config.backendUrl}${config.apiPaths.runs}`, {
566
+ const response = await fetch(`${config.backendUrl}${config.apiPaths.runs}`, getFetchOptions({
443
567
  method: 'POST',
444
- headers,
568
+ headers: { 'Content-Type': 'application/json' },
445
569
  body: JSON.stringify({
446
570
  agentKey: config.agentKey,
447
571
  conversationId: state.conversationId,
448
572
  messages: [{ role: 'user', content: content.trim() }],
449
573
  metadata: { journey_type: state.journeyType },
450
574
  }),
451
- });
575
+ }));
452
576
 
453
577
  if (!response.ok) {
454
578
  const errorData = await response.json().catch(() => ({}));
455
- throw new Error(errorData.error || `HTTP ${response.status}`);
579
+ const error = new Error(errorData.error || `HTTP ${response.status}`);
580
+
581
+ // Handle auth errors
582
+ if (response.status === 401 || response.status === 403) {
583
+ handleAuthError(error, response);
584
+ }
585
+
586
+ throw error;
456
587
  }
457
588
 
458
589
  const run = await response.json();
@@ -1148,6 +1279,11 @@
1148
1279
  };
1149
1280
  state.journeyType = config.defaultJourneyType;
1150
1281
 
1282
+ // Initialize auth token from config
1283
+ if (config.authToken) {
1284
+ state.authToken = config.authToken;
1285
+ }
1286
+
1151
1287
  // Restore conversation ID
1152
1288
  state.conversationId = getStoredValue(config.conversationIdKey);
1153
1289
 
@@ -1193,6 +1329,42 @@
1193
1329
  sendMessage(message);
1194
1330
  }
1195
1331
 
1332
+ /**
1333
+ * Update authentication configuration
1334
+ * @param {Object} authConfig - { strategy?: string, token?: string }
1335
+ */
1336
+ function setAuth(authConfig = {}) {
1337
+ if (authConfig.strategy) {
1338
+ config.authStrategy = authConfig.strategy;
1339
+ }
1340
+ if (authConfig.token !== undefined) {
1341
+ config.authToken = authConfig.token;
1342
+ state.authToken = authConfig.token;
1343
+
1344
+ // For anonymous strategy, also persist to storage
1345
+ if (getAuthStrategy() === 'anonymous' && authConfig.token) {
1346
+ const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
1347
+ setStoredValue(storageKey, authConfig.token);
1348
+ }
1349
+ }
1350
+ console.log('[ChatWidget] Auth updated:', { strategy: getAuthStrategy(), hasToken: !!state.authToken });
1351
+ }
1352
+
1353
+ /**
1354
+ * Clear authentication
1355
+ */
1356
+ function clearAuth() {
1357
+ config.authToken = null;
1358
+ state.authToken = null;
1359
+ state.sessionToken = null;
1360
+
1361
+ // Clear from storage
1362
+ const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
1363
+ setStoredValue(storageKey, null);
1364
+
1365
+ console.log('[ChatWidget] Auth cleared');
1366
+ }
1367
+
1196
1368
  // Export public API
1197
1369
  global.ChatWidget = {
1198
1370
  init,
@@ -1209,6 +1381,8 @@
1209
1381
  toggleTTS,
1210
1382
  stopSpeech,
1211
1383
  setVoice,
1384
+ setAuth,
1385
+ clearAuth,
1212
1386
  getState: () => ({ ...state }),
1213
1387
  getConfig: () => ({ ...config }),
1214
1388
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makemore/agent-frontend",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "A standalone, zero-dependency chat widget for AI agents. Embed conversational AI into any website with a single script tag.",
5
5
  "main": "dist/chat-widget.js",
6
6
  "files": [