mdb-engine 0.4.12__tar.gz → 0.4.14__tar.gz
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.
- {mdb_engine-0.4.12/mdb_engine.egg-info → mdb_engine-0.4.14}/PKG-INFO +1 -1
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/__init__.py +6 -5
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/routing/websockets.py +95 -55
- {mdb_engine-0.4.12 → mdb_engine-0.4.14/mdb_engine.egg-info}/PKG-INFO +1 -1
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/pyproject.toml +1 -1
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/setup.py +1 -1
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/LICENSE +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/MANIFEST.in +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/ARCHITECTURE.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/audit.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/base.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/casbin_factory.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/casbin_models.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/config_defaults.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/config_helpers.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/cookie_utils.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/csrf.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/decorators.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/dependencies.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/helpers.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/integration.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/jwt.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/middleware.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/oso_factory.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/provider.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/rate_limiter.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/restrictions.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/session_manager.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/shared_middleware.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/shared_users.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/token_lifecycle.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/token_store.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/users.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/auth/utils.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/commands/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/commands/generate.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/commands/migrate.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/commands/show.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/commands/validate.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/main.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/cli/utils.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/config.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/constants.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/app_registration.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/app_secrets.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/connection.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/encryption.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/engine.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/index_management.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/manifest.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/ray_integration.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/seeding.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/service_initialization.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/core/types.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/database/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/database/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/database/abstraction.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/database/connection.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/database/query_validator.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/database/resource_limiter.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/database/scoped_wrapper.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/dependencies.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/di/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/di/container.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/di/providers.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/di/scopes.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/embeddings/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/embeddings/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/embeddings/dependencies.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/embeddings/service.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/exceptions.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/indexes/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/indexes/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/indexes/helpers.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/indexes/manager.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/memory/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/memory/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/memory/service.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/observability/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/observability/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/observability/health.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/observability/logging.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/observability/metrics.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/repositories/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/repositories/base.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/repositories/mongo.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/repositories/unit_of_work.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/routing/README.md +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/routing/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/utils/__init__.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine/utils/mongo.py +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine.egg-info/SOURCES.txt +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine.egg-info/dependency_links.txt +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine.egg-info/entry_points.txt +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine.egg-info/requires.txt +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/mdb_engine.egg-info/top_level.txt +0 -0
- {mdb_engine-0.4.12 → mdb_engine-0.4.14}/setup.cfg +0 -0
|
@@ -82,11 +82,12 @@ from .repositories import Entity, MongoRepository, Repository, UnitOfWork
|
|
|
82
82
|
from .utils import clean_mongo_doc, clean_mongo_docs
|
|
83
83
|
|
|
84
84
|
__version__ = (
|
|
85
|
-
"0.4.
|
|
86
|
-
# -
|
|
87
|
-
# -
|
|
88
|
-
# -
|
|
89
|
-
# -
|
|
85
|
+
"0.4.14" # WebSocket security documentation and test updates
|
|
86
|
+
# - Comprehensive security guide (WEBSOCKET_SECURITY_MULTI_APP_SSO.md)
|
|
87
|
+
# - Updated all documentation to reflect subprotocol-only authentication
|
|
88
|
+
# - Added integration tests for subprotocol authentication in multi-app SSO
|
|
89
|
+
# - Enhanced unit test documentation with security-focused explanations
|
|
90
|
+
# - Complete test coverage for WebSocket security scenarios
|
|
90
91
|
)
|
|
91
92
|
|
|
92
93
|
__all__ = [
|
|
@@ -311,10 +311,15 @@ async def authenticate_websocket(
|
|
|
311
311
|
websocket: Any, app_slug: str, require_auth: bool = True
|
|
312
312
|
) -> tuple[str | None, str | None]:
|
|
313
313
|
"""
|
|
314
|
-
Authenticate a WebSocket connection.
|
|
314
|
+
Authenticate a WebSocket connection via Sec-WebSocket-Protocol header.
|
|
315
|
+
|
|
316
|
+
Uses subprotocol tunneling to pass JWT tokens securely:
|
|
317
|
+
- Client: new WebSocket(url, [token])
|
|
318
|
+
- Server: Extracts token from sec-websocket-protocol header
|
|
319
|
+
- Bypasses CSRF issues and avoids URL logging risks
|
|
315
320
|
|
|
316
321
|
Args:
|
|
317
|
-
websocket: FastAPI WebSocket instance
|
|
322
|
+
websocket: FastAPI WebSocket instance (can access headers before accept)
|
|
318
323
|
app_slug: App slug for context
|
|
319
324
|
require_auth: Whether authentication is required
|
|
320
325
|
|
|
@@ -335,59 +340,57 @@ async def authenticate_websocket(
|
|
|
335
340
|
return None, None
|
|
336
341
|
|
|
337
342
|
try:
|
|
338
|
-
# Try to get token from query params or cookies
|
|
339
343
|
token = None
|
|
340
|
-
|
|
341
|
-
cookie_token = None
|
|
342
|
-
|
|
343
|
-
# Check query parameters
|
|
344
|
-
# FastAPI WebSocket query params are accessed via websocket.query_params
|
|
345
|
-
if hasattr(websocket, "query_params"):
|
|
346
|
-
if websocket.query_params:
|
|
347
|
-
query_token = websocket.query_params.get("token")
|
|
348
|
-
logger.info(
|
|
349
|
-
f"WebSocket query_params for app '{app_slug}': {dict(websocket.query_params)}"
|
|
350
|
-
)
|
|
351
|
-
if query_token:
|
|
352
|
-
token = query_token
|
|
353
|
-
logger.info(
|
|
354
|
-
f"WebSocket token found in query params for app "
|
|
355
|
-
f"'{app_slug}' (length: {len(query_token)})"
|
|
356
|
-
)
|
|
357
|
-
else:
|
|
358
|
-
logger.info(f"WebSocket query_params is empty for app '{app_slug}'")
|
|
359
|
-
else:
|
|
360
|
-
logger.warning(f"WebSocket has no query_params attribute for app '{app_slug}'")
|
|
344
|
+
selected_subprotocol = None
|
|
361
345
|
|
|
362
|
-
#
|
|
363
|
-
#
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
346
|
+
# Sec-WebSocket-Protocol (Subprotocol Tunneling)
|
|
347
|
+
# Browsers allow: new WebSocket(url, ["token", "THE_JWT_STRING"])
|
|
348
|
+
# This bypasses CSRF issues and avoids URL logging risks
|
|
349
|
+
try:
|
|
350
|
+
protocol_header = None
|
|
351
|
+
# Try multiple ways to access headers (FastAPI WebSocket compatibility)
|
|
352
|
+
if hasattr(websocket, "headers") and websocket.headers is not None:
|
|
353
|
+
# FastAPI WebSocket.headers is case-insensitive dict-like
|
|
354
|
+
protocol_header = websocket.headers.get("sec-websocket-protocol")
|
|
355
|
+
|
|
356
|
+
# Fallback: access via scope (ASGI standard) if headers not available
|
|
357
|
+
if not protocol_header and hasattr(websocket, "scope") and "headers" in websocket.scope:
|
|
358
|
+
headers_dict = dict(websocket.scope["headers"])
|
|
359
|
+
# Headers are bytes in ASGI, need to decode
|
|
360
|
+
protocol_header_bytes = headers_dict.get(b"sec-websocket-protocol")
|
|
361
|
+
if protocol_header_bytes:
|
|
362
|
+
protocol_header = protocol_header_bytes.decode("utf-8")
|
|
363
|
+
|
|
364
|
+
if protocol_header:
|
|
365
|
+
# Header format: "protocol1, protocol2, ..." or just "token"
|
|
366
|
+
protocols = [p.strip() for p in protocol_header.split(",")]
|
|
367
|
+
logger.debug(f"WebSocket subprotocols for app '{app_slug}': {protocols}")
|
|
368
|
+
|
|
369
|
+
# Look for a JWT-like token in the protocols
|
|
370
|
+
# JWTs are typically long (>20 chars) and don't contain spaces
|
|
371
|
+
for protocol in protocols:
|
|
372
|
+
if len(protocol) > 20 and " " not in protocol:
|
|
373
|
+
token = protocol
|
|
374
|
+
selected_subprotocol = protocol
|
|
375
|
+
logger.info(
|
|
376
|
+
f"WebSocket token found in subprotocol for app '{app_slug}' "
|
|
377
|
+
f"(length: {len(protocol)})"
|
|
378
|
+
)
|
|
379
|
+
break
|
|
380
|
+
except (AttributeError, TypeError, KeyError, UnicodeDecodeError) as e:
|
|
381
|
+
logger.debug(f"Could not access WebSocket headers for subprotocol check: {e}")
|
|
379
382
|
|
|
380
383
|
if not token:
|
|
381
384
|
logger.warning(
|
|
382
|
-
f"No token found for WebSocket connection to app
|
|
383
|
-
f"(require_auth={require_auth})"
|
|
385
|
+
f"No token found in subprotocol header for WebSocket connection to app "
|
|
386
|
+
f"'{app_slug}' (require_auth={require_auth}). "
|
|
387
|
+
f"Use: new WebSocket(url, [token]) to pass JWT token as subprotocol."
|
|
384
388
|
)
|
|
385
389
|
if require_auth:
|
|
386
|
-
# Don't close before accepting - return error info instead
|
|
387
|
-
# The caller will handle closing after accept
|
|
388
390
|
return None, None # Signal auth failure
|
|
389
391
|
return None, None
|
|
390
392
|
|
|
393
|
+
# Decode and validate token
|
|
391
394
|
import jwt
|
|
392
395
|
|
|
393
396
|
from ..auth.dependencies import SECRET_KEY
|
|
@@ -398,6 +401,13 @@ async def authenticate_websocket(
|
|
|
398
401
|
user_id = payload.get("sub") or payload.get("user_id")
|
|
399
402
|
user_email = payload.get("email")
|
|
400
403
|
|
|
404
|
+
# Store selected subprotocol on websocket scope for accept() to use
|
|
405
|
+
if selected_subprotocol:
|
|
406
|
+
# Store in scope so _accept_websocket_connection can access it
|
|
407
|
+
if hasattr(websocket, "scope"):
|
|
408
|
+
websocket.scope["_selected_subprotocol"] = selected_subprotocol
|
|
409
|
+
logger.debug(f"Stored subprotocol '{selected_subprotocol}' for WebSocket accept")
|
|
410
|
+
|
|
401
411
|
logger.info(f"WebSocket authenticated successfully for app '{app_slug}': {user_email}")
|
|
402
412
|
return user_id, user_email
|
|
403
413
|
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError) as decode_error:
|
|
@@ -409,7 +419,6 @@ async def authenticate_websocket(
|
|
|
409
419
|
except (ValueError, TypeError, AttributeError, KeyError, RuntimeError) as e:
|
|
410
420
|
logger.error(f"WebSocket authentication failed for app '{app_slug}': {e}", exc_info=True)
|
|
411
421
|
if require_auth:
|
|
412
|
-
# Don't close before accepting - return error info instead
|
|
413
422
|
return None, None # Signal auth failure
|
|
414
423
|
return None, None
|
|
415
424
|
|
|
@@ -470,11 +479,32 @@ def get_message_handler(
|
|
|
470
479
|
|
|
471
480
|
|
|
472
481
|
async def _accept_websocket_connection(websocket: Any, app_slug: str) -> None:
|
|
473
|
-
"""
|
|
482
|
+
"""
|
|
483
|
+
Accept WebSocket connection with subprotocol support.
|
|
484
|
+
|
|
485
|
+
If authentication found a token in the Sec-WebSocket-Protocol header,
|
|
486
|
+
we must accept with that specific subprotocol or the browser will reject
|
|
487
|
+
the connection.
|
|
488
|
+
"""
|
|
474
489
|
try:
|
|
475
|
-
|
|
490
|
+
# Check if auth stored a selected subprotocol
|
|
491
|
+
selected_subprotocol = None
|
|
492
|
+
if hasattr(websocket, "scope"):
|
|
493
|
+
selected_subprotocol = websocket.scope.get("_selected_subprotocol")
|
|
494
|
+
|
|
495
|
+
if selected_subprotocol:
|
|
496
|
+
# Accept with the specific subprotocol the client requested
|
|
497
|
+
await websocket.accept(subprotocol=selected_subprotocol)
|
|
498
|
+
logger.info(
|
|
499
|
+
f"✅ WebSocket accepted for app '{app_slug}' "
|
|
500
|
+
f"with subprotocol '{selected_subprotocol}'"
|
|
501
|
+
)
|
|
502
|
+
else:
|
|
503
|
+
# Standard accept without subprotocol
|
|
504
|
+
await websocket.accept()
|
|
505
|
+
logger.info(f"✅ WebSocket accepted for app '{app_slug}'")
|
|
506
|
+
|
|
476
507
|
print(f"✅ [WEBSOCKET ACCEPTED] App: '{app_slug}'")
|
|
477
|
-
logger.info(f"✅ WebSocket accepted for app '{app_slug}'")
|
|
478
508
|
except (RuntimeError, ConnectionError, OSError) as accept_error:
|
|
479
509
|
print(f"❌ [WEBSOCKET ACCEPT FAILED] App: '{app_slug}', Error: {accept_error}")
|
|
480
510
|
logger.error(
|
|
@@ -654,13 +684,23 @@ def create_websocket_endpoint(
|
|
|
654
684
|
f"(require_auth={require_auth}, query_params={query_str})"
|
|
655
685
|
)
|
|
656
686
|
|
|
657
|
-
#
|
|
658
|
-
|
|
687
|
+
# CRITICAL: Authenticate BEFORE accepting connection
|
|
688
|
+
# This prevents CSRF middleware from rejecting established connections
|
|
689
|
+
# We can access headers/query_params before accept() is called
|
|
690
|
+
user_id, user_email = await authenticate_websocket(websocket, app_slug, require_auth)
|
|
659
691
|
|
|
660
|
-
#
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
692
|
+
# Handle authentication failure
|
|
693
|
+
if require_auth and not user_id:
|
|
694
|
+
logger.warning(
|
|
695
|
+
f"WebSocket authentication failed for app '{app_slug}' - rejecting connection"
|
|
696
|
+
)
|
|
697
|
+
# Reject without accepting - FastAPI will send 403 if accept() not called
|
|
698
|
+
# We can't call websocket.close() before accept(), so we just return
|
|
699
|
+
# The connection will be rejected by the server
|
|
700
|
+
return
|
|
701
|
+
|
|
702
|
+
# Accept connection (with subprotocol if token was in protocol header)
|
|
703
|
+
await _accept_websocket_connection(websocket, app_slug)
|
|
664
704
|
|
|
665
705
|
# Connect with metadata (websocket already accepted)
|
|
666
706
|
connection = await manager.connect(websocket, user_id=user_id, user_email=user_email)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|