create-caspian-app 1.0.2 → 1.0.4

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/dist/main.py CHANGED
@@ -26,7 +26,6 @@ from fastapi import (
26
26
  )
27
27
  from fastapi.responses import (
28
28
  RedirectResponse,
29
- FileResponse,
30
29
  HTMLResponse,
31
30
  JSONResponse,
32
31
  )
@@ -68,7 +67,8 @@ from casp.runtime_security import (
68
67
  client_error_message,
69
68
  get_session_secret,
70
69
  is_production_environment,
71
- public_file_response,
70
+ PublicFilesMiddleware,
71
+ resolve_safe_public_path,
72
72
  )
73
73
  from contextlib import (
74
74
  asynccontextmanager,
@@ -347,7 +347,6 @@ REQUEST_TIMEOUT_SECONDS = max(
347
347
  STREAMING_PATH_PREFIXES = ('/mcp',)
348
348
  # Public assets: exempt from auth, request logging, and rate limiting, since one
349
349
  # page load pulls many of them.
350
- STATIC_PATH_PREFIXES = ('/css/', '/js/', '/assets/', '/favicon.ico')
351
350
  MAX_CONTENT_LENGTH_BYTES = max(1, MAX_CONTENT_LENGTH_MB) * 1024 * 1024
352
351
 
353
352
 
@@ -410,49 +409,6 @@ SESSION_COOKIE_NAME = _scoped_cookie_name(
410
409
  os.getenv('AUTH_COOKIE_NAME', 'session')
411
410
  )
412
411
 
413
- # ====
414
- # Static File Routes
415
- # ====
416
-
417
-
418
- @app.get('/css/{filename:path}')
419
- async def serve_css(filename: str):
420
- return public_file_response('public/css', filename, media_type='text/css')
421
-
422
-
423
- @app.get('/js/{filename:path}')
424
- async def serve_js(filename: str):
425
- return public_file_response(
426
- 'public/js',
427
- filename,
428
- media_type='application/javascript',
429
- )
430
-
431
-
432
- @app.get('/assets/{filename:path}')
433
- async def serve_assets(filename: str):
434
- return public_file_response('public/assets', filename)
435
-
436
-
437
- @app.get('/uploads/{filename:path}')
438
- async def serve_uploads(filename: str):
439
- # User-supplied content served from the app's own origin: only real images
440
- # render inline, everything else downloads. An uploaded .html or .svg would
441
- # otherwise be stored XSS with full access to the session cookie.
442
- return public_file_response(
443
- 'public/uploads',
444
- filename,
445
- inline_safe_media_types=INLINE_SAFE_UPLOAD_MEDIA_TYPES,
446
- )
447
-
448
-
449
- @app.get('/favicon.ico')
450
- async def favicon():
451
- file_path = Path('public/favicon.ico')
452
- if not file_path.exists():
453
- return Response(status_code=404)
454
- return FileResponse(file_path, media_type='image/x-icon')
455
-
456
412
  # ====
457
413
  # Pure ASGI Middleware Classes
458
414
  # ====
@@ -468,13 +424,6 @@ class CSRFMiddleware:
468
424
  await self.app(scope, receive, send)
469
425
  return
470
426
 
471
- # Static assets carry no session and never issue RPC calls. Attaching a
472
- # Set-Cookie to them makes every asset response uncacheable by shared
473
- # caches and CDNs for no security benefit.
474
- if scope.get("path", "").startswith(STATIC_PATH_PREFIXES):
475
- await self.app(scope, receive, send)
476
- return
477
-
478
427
  request = Request(scope, receive, send)
479
428
  csrf_token = request.session.get("csrf_token")
480
429
  if not csrf_token:
@@ -588,9 +537,6 @@ class AuthMiddleware:
588
537
  return
589
538
  request = Request(scope, receive, send)
590
539
  path = request.url.path
591
- if path.startswith(STATIC_PATH_PREFIXES):
592
- await self.app(scope, receive, send)
593
- return
594
540
  StateManager.init(request)
595
541
  Auth.set_request(request)
596
542
  auth_inst = Auth.get_instance()
@@ -699,7 +645,7 @@ class RateLimitMiddleware:
699
645
  return
700
646
 
701
647
  path = scope.get("path", "")
702
- if path.startswith(STATIC_PATH_PREFIXES) or path == '/health':
648
+ if path == '/health':
703
649
  await self.app(scope, receive, send)
704
650
  return
705
651
 
@@ -735,7 +681,14 @@ class RequestDiagnosticsMiddleware:
735
681
 
736
682
  method = scope.get("method", "GET")
737
683
  path = scope.get("path", "")
738
- should_log = not path.startswith(STATIC_PATH_PREFIXES)
684
+ is_public_file = (
685
+ method in {"GET", "HEAD"}
686
+ and resolve_safe_public_path(
687
+ 'public',
688
+ path.lstrip('/'),
689
+ ) is not None
690
+ )
691
+ should_log = not is_public_file
739
692
  started = time.perf_counter()
740
693
 
741
694
  if should_log and not IS_PRODUCTION:
@@ -1511,6 +1464,16 @@ app.add_middleware(BodySizeLimitMiddleware)
1511
1464
  # Outermost of the security layers: reject flooding before any session
1512
1465
  # decryption, template rendering, or database work is spent on the request.
1513
1466
  app.add_middleware(RateLimitMiddleware)
1467
+ # The public directory is itself the URL contract: any existing nested file is
1468
+ # served without adding a directory-specific route above. Keep upload content
1469
+ # in attachment mode unless its MIME type is explicitly safe to render inline.
1470
+ app.add_middleware(
1471
+ PublicFilesMiddleware,
1472
+ directory='public',
1473
+ inline_safe_subdirectories={
1474
+ 'uploads': INLINE_SAFE_UPLOAD_MEDIA_TYPES,
1475
+ },
1476
+ )
1514
1477
  app.add_middleware(SecurityHeadersMiddleware)
1515
1478
 
1516
1479
  if not IS_PRODUCTION: