escobar 0.1.89__py3-none-any.whl → 0.1.91__py3-none-any.whl

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 (31) hide show
  1. escobar/_version.py +1 -1
  2. escobar/handlers.py +57 -147
  3. escobar/labextension/package.json +2 -2
  4. escobar/labextension/schemas/escobar/package.json.orig +1 -1
  5. escobar/labextension/schemas/escobar/plugin.json +2 -2
  6. escobar/labextension/static/653.cd0a1dbb61ede2a5de00.js +1 -0
  7. escobar/labextension/static/{remoteEntry.a8ae0b901c4a30b269e4.js → remoteEntry.48a97be89d680e21e498.js} +1 -1
  8. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/package.json +2 -2
  9. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/schemas/escobar/package.json.orig +1 -1
  10. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/schemas/escobar/plugin.json +2 -2
  11. escobar-0.1.91.data/data/share/jupyter/labextensions/escobar/static/653.cd0a1dbb61ede2a5de00.js +1 -0
  12. escobar-0.1.89.data/data/share/jupyter/labextensions/escobar/static/remoteEntry.a8ae0b901c4a30b269e4.js → escobar-0.1.91.data/data/share/jupyter/labextensions/escobar/static/remoteEntry.48a97be89d680e21e498.js +1 -1
  13. {escobar-0.1.89.dist-info → escobar-0.1.91.dist-info}/METADATA +1 -1
  14. escobar-0.1.91.dist-info/RECORD +40 -0
  15. escobar/labextension/static/237.23439249688997c395a8.js +0 -1
  16. escobar-0.1.89.data/data/share/jupyter/labextensions/escobar/static/237.23439249688997c395a8.js +0 -1
  17. escobar-0.1.89.dist-info/RECORD +0 -40
  18. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/install.json +0 -0
  19. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/304.bf7e91be734e5b36cdc9.js +0 -0
  20. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/346.8a1e61ca6789b6fddfa7.js +0 -0
  21. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/379.40dd59dc19d4a6b42d25.js +0 -0
  22. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/57.17e53b4b9a954f39c4d8.js +0 -0
  23. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/648.a7d314faeacc762d891d.js +0 -0
  24. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/666.3bc65aac3a3be183ee19.js +0 -0
  25. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/874.c539726f31a1baa0267e.js +0 -0
  26. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/986.cbcf9d7c1cd8d06be435.js +0 -0
  27. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/style.js +0 -0
  28. {escobar-0.1.89.data → escobar-0.1.91.data}/data/share/jupyter/labextensions/escobar/static/third-party-licenses.json +0 -0
  29. {escobar-0.1.89.dist-info → escobar-0.1.91.dist-info}/WHEEL +0 -0
  30. {escobar-0.1.89.dist-info → escobar-0.1.91.dist-info}/entry_points.txt +0 -0
  31. {escobar-0.1.89.dist-info → escobar-0.1.91.dist-info}/licenses/LICENSE +0 -0
escobar/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # This file is auto-generated by Hatchling. As such, do not:
2
2
  # - modify
3
3
  # - track in version control e.g. be sure to add to .gitignore
4
- __version__ = VERSION = '0.1.89'
4
+ __version__ = VERSION = '0.1.91'
escobar/handlers.py CHANGED
@@ -15,10 +15,58 @@ import tornado.websocket
15
15
  import aiohttp
16
16
  from traitlets.config import LoggingConfigurable
17
17
  import mimetypes
18
+ import requests
19
+ from google.oauth2 import id_token
20
+ from google.auth.transport import requests as google_requests
18
21
 
19
22
  # Default proxy port
20
23
  DEFAULT_PROXY_PORT = 3000
21
24
 
25
+
26
+ class OAuthCallbackHandler(JupyterHandler):
27
+ """
28
+ Handler for /static/oauth-callback.html endpoint.
29
+ Serves the OAuth callback HTML file for Google authentication.
30
+ """
31
+
32
+ async def get(self):
33
+ """Handle GET requests to serve the OAuth callback HTML"""
34
+ try:
35
+ # Get the path to the static directory
36
+ import os
37
+ current_dir = os.path.dirname(os.path.abspath(__file__))
38
+ project_root = os.path.dirname(current_dir)
39
+ callback_file_path = os.path.join(project_root, 'static', 'oauth-callback.html')
40
+
41
+ self.log.info(f"🔐 CALLBACK: Serving OAuth callback from: {callback_file_path}")
42
+
43
+ # Check if file exists
44
+ if not os.path.exists(callback_file_path):
45
+ self.log.error(f"🔐 CALLBACK: OAuth callback file not found: {callback_file_path}")
46
+ self.set_status(404)
47
+ self.finish("OAuth callback file not found")
48
+ return
49
+
50
+ # Read and serve the HTML file
51
+ with open(callback_file_path, 'r', encoding='utf-8') as f:
52
+ html_content = f.read()
53
+
54
+ # Set proper headers
55
+ self.set_header('Content-Type', 'text/html; charset=UTF-8')
56
+ self.set_header('Cache-Control', 'no-cache, no-store, must-revalidate')
57
+ self.set_header('Pragma', 'no-cache')
58
+ self.set_header('Expires', '0')
59
+
60
+ # Write the HTML content
61
+ self.write(html_content)
62
+ self.log.info(f"🔐 CALLBACK: Successfully served OAuth callback HTML")
63
+
64
+ except Exception as e:
65
+ self.log.error(f"🔐 CALLBACK: Error serving OAuth callback: {e}")
66
+ self.set_status(500)
67
+ self.finish(f"Error serving OAuth callback: {str(e)}")
68
+
69
+
22
70
  class ProxyHandler(JupyterHandler):
23
71
  """
24
72
  Handler for /proxy endpoint.
@@ -184,16 +232,11 @@ class WebSocketProxyHandler(tornado.websocket.WebSocketHandler):
184
232
  # Determine if we're in Docker
185
233
  is_docker = dockerenv_exists or docker_env or cgroup_docker
186
234
 
187
- print(f"[ESCOBAR-WS] Docker detection indicators: {docker_indicators}")
188
- print(f"[ESCOBAR-WS] Final Docker detection result: {is_docker}")
189
-
190
235
  if not is_docker:
191
- print(f"[ESCOBAR-WS] Not in Docker container, using original URL: {url}")
192
236
  return url
193
237
 
194
238
  # Parse the URL to extract components
195
239
  parsed = urlparse(url)
196
- print(f"[ESCOBAR-WS] Parsed URL - hostname: '{parsed.hostname}', netloc: '{parsed.netloc}'")
197
240
 
198
241
  # Check if hostname is localhost or 127.0.0.1
199
242
  if parsed.hostname in ['localhost', '127.0.0.1']:
@@ -202,10 +245,7 @@ class WebSocketProxyHandler(tornado.websocket.WebSocketHandler):
202
245
  new_parsed = parsed._replace(netloc=new_netloc)
203
246
  new_url = urlunparse(new_parsed)
204
247
 
205
- print(f"[ESCOBAR-WS] Docker hostname resolution: {url} → {new_url}")
206
248
  return new_url
207
- else:
208
- print(f"[ESCOBAR-WS] Docker container detected, but hostname '{parsed.hostname}' is not localhost/127.0.0.1, keeping original: {url}")
209
249
 
210
250
  return url
211
251
 
@@ -238,30 +278,22 @@ class WebSocketProxyHandler(tornado.websocket.WebSocketHandler):
238
278
  Get user-configured Bonnie URL from bonnie_url query parameter.
239
279
  This allows the frontend to override the environment variable.
240
280
  """
241
- print(f"[ESCOBAR-WS] === READING USER BONNIE URL FROM QUERY PARAMETER ===")
242
-
243
281
  try:
244
282
  # Get the bonnieUrl from query parameters
245
283
  bonnie_url = self.get_argument('bonnie_url', None)
246
- print(f"[ESCOBAR-WS] bonnie_url query parameter value: '{bonnie_url}'")
247
284
 
248
285
  if bonnie_url and bonnie_url.strip():
249
286
  # Validate that it's a WebSocket URL
250
287
  if bonnie_url.startswith(('ws://', 'wss://')):
251
- print(f"[ESCOBAR-WS] Valid user Bonnie URL from query parameter: {bonnie_url}")
252
288
  return bonnie_url.strip()
253
289
  else:
254
- print(f"[ESCOBAR-WS] Invalid Bonnie URL format (must start with ws:// or wss://): {bonnie_url}")
255
290
  return None
256
291
  else:
257
- print(f"[ESCOBAR-WS] No bonnie_url query parameter provided")
258
292
  return None
259
293
 
260
294
  except Exception as e:
261
295
  print(f"[ESCOBAR-WS] Error reading bonnie_url query parameter: {e}")
262
296
  return None
263
- finally:
264
- print(f"[ESCOBAR-WS] === END BONNIE URL QUERY PARAMETER READ ===")
265
297
 
266
298
  def _get_target_url(self):
267
299
  """
@@ -375,136 +407,38 @@ class WebSocketProxyHandler(tornado.websocket.WebSocketHandler):
375
407
 
376
408
  async def on_message(self, message):
377
409
  """Called when a message is received from the client"""
378
- print(f"[ESCOBAR-WS] === CLIENT MESSAGE RECEIVED ===")
379
- print(f"[ESCOBAR-WS] Message length: {len(message)}")
380
- print(f"[ESCOBAR-WS] Message type: {type(message)}")
381
- print(f"[ESCOBAR-WS] Message preview: {message[:500]}...")
382
- print(f"[ESCOBAR-WS] Target WS exists: {self.target_ws is not None}")
383
- print(f"[ESCOBAR-WS] Is closing: {self.is_closing}")
384
-
385
410
  if self.target_ws and not self.is_closing:
386
- # Enhanced debugging before forwarding
387
- print(f"[ESCOBAR-WS] === PRE-FORWARD DIAGNOSTICS ===")
388
- print(f"[ESCOBAR-WS] Target WS type: {type(self.target_ws)}")
389
- print(f"[ESCOBAR-WS] Target WS state: {getattr(self.target_ws, 'state', 'NO_STATE_ATTR')}")
390
-
391
- # Check if connection is actually open
392
- try:
393
- is_open = self.target_ws.state.name == 'OPEN'
394
- print(f"[ESCOBAR-WS] Target WS is OPEN: {is_open}")
395
- except AttributeError:
396
- print(f"[ESCOBAR-WS] Cannot check target WS state - no state attribute")
397
- is_open = True # Assume open and let send() fail with proper error
398
-
399
- # Check connection properties
400
- try:
401
- print(f"[ESCOBAR-WS] Target WS closed property: {getattr(self.target_ws, 'closed', 'NO_CLOSED_ATTR')}")
402
- except:
403
- print(f"[ESCOBAR-WS] Cannot access target WS closed property")
404
-
405
- # Message type analysis
406
- if isinstance(message, str):
407
- print(f"[ESCOBAR-WS] Message is TEXT (string)")
408
- elif isinstance(message, bytes):
409
- print(f"[ESCOBAR-WS] Message is BINARY (bytes)")
410
- else:
411
- print(f"[ESCOBAR-WS] Message is UNKNOWN type: {type(message)}")
412
-
413
- print(f"[ESCOBAR-WS] === ATTEMPTING MESSAGE FORWARD ===")
414
-
415
411
  try:
416
412
  # Forward message to target server
417
413
  await self.target_ws.send(message)
418
- print(f"[ESCOBAR-WS] ✅ Message successfully forwarded to target")
419
414
  logging.debug(f"Forwarded message to target: {message[:100]}...")
420
415
  except Exception as e:
421
- print(f"[ESCOBAR-WS] ❌ CRITICAL ERROR forwarding message to target:")
422
- print(f"[ESCOBAR-WS] Exception type: {type(e).__name__}")
423
- print(f"[ESCOBAR-WS] Exception message: {str(e)}")
424
- print(f"[ESCOBAR-WS] Exception args: {getattr(e, 'args', 'NO_ARGS')}")
425
-
426
- # Check target connection state after error
427
- try:
428
- print(f"[ESCOBAR-WS] Target WS state after error: {self.target_ws.state}")
429
- except:
430
- print(f"[ESCOBAR-WS] Cannot check target WS state after error")
431
-
432
- # Additional exception details
433
- if hasattr(e, '__dict__'):
434
- print(f"[ESCOBAR-WS] Exception attributes: {e.__dict__}")
435
- if hasattr(e, 'errno'):
436
- print(f"[ESCOBAR-WS] Errno: {e.errno}")
437
- if hasattr(e, 'strerror'):
438
- print(f"[ESCOBAR-WS] Strerror: {e.strerror}")
439
-
440
- # Import specific exception types for better diagnosis
441
- import websockets.exceptions
442
- if isinstance(e, websockets.exceptions.ConnectionClosed):
443
- print(f"[ESCOBAR-WS] ConnectionClosed - code: {e.code}, reason: {e.reason}")
444
- elif isinstance(e, websockets.exceptions.InvalidState):
445
- print(f"[ESCOBAR-WS] InvalidState - connection in wrong state")
446
- elif isinstance(e, websockets.exceptions.PayloadTooBig):
447
- print(f"[ESCOBAR-WS] PayloadTooBig - message too large")
448
-
416
+ print(f"[ESCOBAR-WS] ERROR forwarding message to target:")
417
+ print(f"[ESCOBAR-WS] Error: {str(e)}")
418
+ print(f"[ESCOBAR-WS] Error type: {type(e).__name__}")
449
419
  logging.error(f"Error forwarding message to target: {str(e)}")
450
-
451
- # Don't close immediately - let's see if we can recover
452
- print(f"[ESCOBAR-WS] Closing client connection due to forward error")
453
- self.close(code=1011, reason=f"Target forward error: {type(e).__name__}")
454
- else:
455
- print(f"[ESCOBAR-WS] Cannot forward message - target_ws: {self.target_ws}, is_closing: {self.is_closing}")
456
- print(f"[ESCOBAR-WS] === END CLIENT MESSAGE ===")
420
+ self.close(code=1011, reason="Target connection error")
457
421
 
458
422
  async def _forward_from_target(self):
459
423
  """Forward messages from target server to client"""
460
- print(f"[ESCOBAR-WS] === STARTING TARGET MESSAGE FORWARDING ===")
461
- print(f"[ESCOBAR-WS] Target WS state: {self.target_ws.state if self.target_ws else 'None'}")
462
-
463
424
  try:
464
- message_count = 0
465
425
  async for message in self.target_ws:
466
- message_count += 1
467
- print(f"[ESCOBAR-WS] === TARGET MESSAGE #{message_count} ===")
468
- print(f"[ESCOBAR-WS] Message length: {len(message)}")
469
- print(f"[ESCOBAR-WS] Message type: {type(message)}")
470
- print(f"[ESCOBAR-WS] Message preview: {message[:500]}...")
471
- print(f"[ESCOBAR-WS] Is closing: {self.is_closing}")
472
-
473
426
  if not self.is_closing:
474
- print(f"[ESCOBAR-WS] Forwarding message to client")
475
427
  # Forward message to client
476
428
  self.write_message(message)
477
- print(f"[ESCOBAR-WS] Message successfully forwarded to client")
478
429
  logging.debug(f"Forwarded message from target: {message[:100]}...")
479
430
  else:
480
- print(f"[ESCOBAR-WS] Breaking forwarding loop - connection is closing")
481
431
  break
482
- print(f"[ESCOBAR-WS] === END TARGET MESSAGE #{message_count} ===")
483
432
 
484
433
  except websockets.exceptions.ConnectionClosed as e:
485
- print(f"[ESCOBAR-WS] === TARGET CONNECTION CLOSED DETAILS ===")
486
- print(f"[ESCOBAR-WS] Target websocket connection closed")
487
- print(f"[ESCOBAR-WS] Close code: {getattr(e, 'code', 'NO_CODE')}")
488
- print(f"[ESCOBAR-WS] Close reason: {getattr(e, 'reason', 'NO_REASON')}")
489
- print(f"[ESCOBAR-WS] Exception type: {type(e).__name__}")
490
- print(f"[ESCOBAR-WS] Exception str: {str(e)}")
491
- print(f"[ESCOBAR-WS] Target WS final state: {getattr(self.target_ws, 'state', 'NO_STATE') if self.target_ws else 'NO_TARGET_WS'}")
492
- print(f"[ESCOBAR-WS] Messages processed before disconnect: {message_count}")
493
- print(f"[ESCOBAR-WS] === END TARGET CONNECTION CLOSED DETAILS ===")
494
-
495
434
  logging.info(f"Target websocket connection closed - code: {getattr(e, 'code', 'NO_CODE')}, reason: {getattr(e, 'reason', 'NO_REASON')}")
496
435
 
497
436
  if not self.is_closing:
498
- print(f"[ESCOBAR-WS] Closing client connection due to target disconnect")
499
437
  self.close(code=1011, reason="Target server disconnected")
500
438
  except Exception as e:
501
439
  print(f"[ESCOBAR-WS] === TARGET MESSAGE FORWARDING ERROR ===")
502
440
  print(f"[ESCOBAR-WS] Error: {str(e)}")
503
441
  print(f"[ESCOBAR-WS] Error type: {type(e).__name__}")
504
- print(f"[ESCOBAR-WS] Messages processed before error: {message_count}")
505
- print(f"[ESCOBAR-WS] Target WS state: {getattr(self.target_ws, 'state', 'NO_STATE') if self.target_ws else 'NO_TARGET_WS'}")
506
- if hasattr(e, '__dict__'):
507
- print(f"[ESCOBAR-WS] Error attributes: {e.__dict__}")
508
442
  if hasattr(e, 'errno'):
509
443
  print(f"[ESCOBAR-WS] Errno: {e.errno}")
510
444
  if hasattr(e, 'strerror'):
@@ -513,66 +447,37 @@ class WebSocketProxyHandler(tornado.websocket.WebSocketHandler):
513
447
 
514
448
  logging.error(f"Error receiving from target websocket: {str(e)}")
515
449
  if not self.is_closing:
516
- print(f"[ESCOBAR-WS] Closing client connection due to target error")
517
450
  self.close(code=1011, reason="Target connection error")
518
-
519
- print(f"[ESCOBAR-WS] === TARGET MESSAGE FORWARDING ENDED ===")
520
451
 
521
452
  def on_close(self):
522
453
  """Called when websocket connection is closed"""
523
- print(f"[ESCOBAR-WS] === CLIENT CONNECTION CLOSED ===")
524
- print(f"[ESCOBAR-WS] Setting is_closing flag to True")
525
454
  self.is_closing = True
526
- print(f"[ESCOBAR-WS] Target WS exists: {self.target_ws is not None}")
527
455
 
528
456
  logging.info("WebSocket connection closed")
529
457
 
530
458
  # Close target connection if it exists
531
459
  if self.target_ws:
532
- print(f"[ESCOBAR-WS] Scheduling target connection cleanup")
533
460
  asyncio.create_task(self._close_target_connection())
534
- else:
535
- print(f"[ESCOBAR-WS] No target connection to clean up")
536
-
537
- print(f"[ESCOBAR-WS] === END CLIENT CONNECTION CLOSED ===")
538
461
 
539
462
  async def _close_target_connection(self):
540
463
  """Safely close the target websocket connection"""
541
- print(f"[ESCOBAR-WS] === CLOSING TARGET CONNECTION ===")
542
464
  try:
543
465
  if self.target_ws:
544
- print(f"[ESCOBAR-WS] Target WS exists, checking state")
545
- print(f"[ESCOBAR-WS] Target WS type: {type(self.target_ws)}")
546
- print(f"[ESCOBAR-WS] Target WS state: {getattr(self.target_ws, 'state', 'NO_STATE_ATTR')}")
547
-
548
466
  # Check if connection is already closed using the correct websockets library API
549
467
  try:
550
468
  is_closed = self.target_ws.state.name == 'CLOSED'
551
- print(f"[ESCOBAR-WS] Target WS is closed: {is_closed}")
552
469
  except AttributeError:
553
470
  # Fallback: just try to close it regardless of state
554
- print(f"[ESCOBAR-WS] Cannot check state, will attempt to close anyway")
555
471
  is_closed = False
556
472
 
557
473
  if not is_closed:
558
- print(f"[ESCOBAR-WS] Target WS is open, closing it")
559
474
  await self.target_ws.close()
560
- print(f"[ESCOBAR-WS] Target connection closed successfully")
561
475
  logging.info("Target websocket connection closed")
562
- else:
563
- print(f"[ESCOBAR-WS] Target WS already closed")
564
- else:
565
- print(f"[ESCOBAR-WS] No target WS to close")
566
476
  except Exception as e:
567
477
  print(f"[ESCOBAR-WS] ERROR closing target connection:")
568
478
  print(f"[ESCOBAR-WS] Error: {str(e)}")
569
479
  print(f"[ESCOBAR-WS] Error type: {type(e).__name__}")
570
- print(f"[ESCOBAR-WS] Target WS type: {type(self.target_ws) if self.target_ws else 'None'}")
571
- if hasattr(e, '__dict__'):
572
- print(f"[ESCOBAR-WS] Error attributes: {e.__dict__}")
573
480
  logging.error(f"Error closing target websocket: {str(e)}")
574
-
575
- print(f"[ESCOBAR-WS] === END CLOSING TARGET CONNECTION ===")
576
481
 
577
482
 
578
483
  def setup_handlers(web_app):
@@ -640,9 +545,14 @@ def setup_handlers(web_app):
640
545
  for pattern in ws_patterns:
641
546
  print(f"[ESCOBAR-WS] - {pattern}")
642
547
 
548
+ # Register the OAuth callback endpoint
549
+ oauth_callback_pattern = url_path_join(base_url, "static", "oauth-callback.html")
550
+ print(f"[ESCOBAR-WS] OAuth callback pattern: {oauth_callback_pattern}")
551
+
643
552
  # Build handlers list with all WebSocket patterns
644
553
  handlers = [
645
554
  (proxy_pattern, ProxyHandler),
555
+ (oauth_callback_pattern, OAuthCallbackHandler),
646
556
  *[(pattern, WebSocketProxyHandler) for pattern in ws_patterns]
647
557
  ]
648
558
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "escobar",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "description": "AI CHAT EXTENSION",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -123,7 +123,7 @@
123
123
  "outputDir": "escobar/labextension",
124
124
  "schemaDir": "schema",
125
125
  "_build": {
126
- "load": "static/remoteEntry.a8ae0b901c4a30b269e4.js",
126
+ "load": "static/remoteEntry.48a97be89d680e21e498.js",
127
127
  "extension": "./extension",
128
128
  "style": "./style"
129
129
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "escobar",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
4
4
  "description": "AI CHAT EXTENSION",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  "googleClientId": {
40
40
  "type": "string",
41
- "title": "Google OAuth Client ID",
42
- "description": "Google OAuth 2.0 Client ID for authentication. Get this from Google Cloud Console.",
41
+ "title": "Google Client ID",
42
+ "description": "Google OAuth Client ID for authentication. Get this from Google Cloud Console.",
43
43
  "default": ""
44
44
  }
45
45
  },