mdb-engine 0.1.6__py3-none-any.whl → 0.2.0__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 (87) hide show
  1. mdb_engine/__init__.py +104 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +648 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +264 -69
  8. mdb_engine/auth/config_helpers.py +7 -6
  9. mdb_engine/auth/cookie_utils.py +3 -7
  10. mdb_engine/auth/csrf.py +373 -0
  11. mdb_engine/auth/decorators.py +3 -10
  12. mdb_engine/auth/dependencies.py +47 -50
  13. mdb_engine/auth/helpers.py +3 -3
  14. mdb_engine/auth/integration.py +53 -80
  15. mdb_engine/auth/jwt.py +2 -6
  16. mdb_engine/auth/middleware.py +77 -34
  17. mdb_engine/auth/oso_factory.py +18 -38
  18. mdb_engine/auth/provider.py +270 -171
  19. mdb_engine/auth/rate_limiter.py +504 -0
  20. mdb_engine/auth/restrictions.py +8 -24
  21. mdb_engine/auth/session_manager.py +14 -29
  22. mdb_engine/auth/shared_middleware.py +600 -0
  23. mdb_engine/auth/shared_users.py +759 -0
  24. mdb_engine/auth/token_store.py +14 -28
  25. mdb_engine/auth/users.py +54 -113
  26. mdb_engine/auth/utils.py +213 -15
  27. mdb_engine/cli/commands/generate.py +545 -9
  28. mdb_engine/cli/commands/validate.py +3 -7
  29. mdb_engine/cli/utils.py +3 -3
  30. mdb_engine/config.py +7 -21
  31. mdb_engine/constants.py +65 -0
  32. mdb_engine/core/README.md +117 -6
  33. mdb_engine/core/__init__.py +39 -7
  34. mdb_engine/core/app_registration.py +22 -41
  35. mdb_engine/core/app_secrets.py +290 -0
  36. mdb_engine/core/connection.py +18 -9
  37. mdb_engine/core/encryption.py +223 -0
  38. mdb_engine/core/engine.py +1057 -93
  39. mdb_engine/core/index_management.py +12 -16
  40. mdb_engine/core/manifest.py +459 -150
  41. mdb_engine/core/ray_integration.py +435 -0
  42. mdb_engine/core/seeding.py +10 -18
  43. mdb_engine/core/service_initialization.py +12 -23
  44. mdb_engine/core/types.py +2 -5
  45. mdb_engine/database/README.md +140 -17
  46. mdb_engine/database/__init__.py +17 -6
  47. mdb_engine/database/abstraction.py +25 -37
  48. mdb_engine/database/connection.py +11 -18
  49. mdb_engine/database/query_validator.py +367 -0
  50. mdb_engine/database/resource_limiter.py +204 -0
  51. mdb_engine/database/scoped_wrapper.py +713 -196
  52. mdb_engine/dependencies.py +426 -0
  53. mdb_engine/di/__init__.py +34 -0
  54. mdb_engine/di/container.py +248 -0
  55. mdb_engine/di/providers.py +205 -0
  56. mdb_engine/di/scopes.py +139 -0
  57. mdb_engine/embeddings/README.md +54 -24
  58. mdb_engine/embeddings/__init__.py +31 -24
  59. mdb_engine/embeddings/dependencies.py +37 -154
  60. mdb_engine/embeddings/service.py +11 -25
  61. mdb_engine/exceptions.py +92 -0
  62. mdb_engine/indexes/README.md +30 -13
  63. mdb_engine/indexes/__init__.py +1 -0
  64. mdb_engine/indexes/helpers.py +1 -1
  65. mdb_engine/indexes/manager.py +50 -114
  66. mdb_engine/memory/README.md +2 -2
  67. mdb_engine/memory/__init__.py +1 -2
  68. mdb_engine/memory/service.py +30 -87
  69. mdb_engine/observability/README.md +4 -2
  70. mdb_engine/observability/__init__.py +26 -9
  71. mdb_engine/observability/health.py +8 -9
  72. mdb_engine/observability/metrics.py +32 -12
  73. mdb_engine/repositories/__init__.py +34 -0
  74. mdb_engine/repositories/base.py +325 -0
  75. mdb_engine/repositories/mongo.py +233 -0
  76. mdb_engine/repositories/unit_of_work.py +166 -0
  77. mdb_engine/routing/README.md +1 -1
  78. mdb_engine/routing/__init__.py +1 -3
  79. mdb_engine/routing/websockets.py +25 -60
  80. mdb_engine-0.2.0.dist-info/METADATA +313 -0
  81. mdb_engine-0.2.0.dist-info/RECORD +96 -0
  82. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  83. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  84. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/WHEEL +0 -0
  85. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/entry_points.txt +0 -0
  86. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/licenses/LICENSE +0 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.2.0.dist-info}/top_level.txt +0 -0
@@ -36,9 +36,14 @@ from typing import Any, Awaitable, Callable, Dict, List, Optional, Tuple
36
36
 
37
37
  from jsonschema import SchemaError, ValidationError, validate
38
38
 
39
- from ..constants import (CURRENT_SCHEMA_VERSION, DEFAULT_SCHEMA_VERSION,
40
- MAX_TTL_SECONDS, MAX_VECTOR_DIMENSIONS,
41
- MIN_TTL_SECONDS, MIN_VECTOR_DIMENSIONS)
39
+ from ..constants import (
40
+ CURRENT_SCHEMA_VERSION,
41
+ DEFAULT_SCHEMA_VERSION,
42
+ MAX_TTL_SECONDS,
43
+ MAX_VECTOR_DIMENSIONS,
44
+ MIN_TTL_SECONDS,
45
+ MIN_VECTOR_DIMENSIONS,
46
+ )
42
47
 
43
48
  logger = logging.getLogger(__name__)
44
49
 
@@ -87,9 +92,7 @@ def _get_manifest_hash(manifest_data: Dict[str, Any]) -> str:
87
92
 
88
93
  # Normalize manifest by removing metadata fields that don't affect validation
89
94
  normalized = {
90
- k: v
91
- for k, v in manifest_data.items()
92
- if k not in ["_id", "_updated", "_created", "url"]
95
+ k: v for k, v in manifest_data.items() if k not in ["_id", "_updated", "_created", "url"]
93
96
  }
94
97
  normalized_str = json.dumps(normalized, sort_keys=True)
95
98
  return hashlib.sha256(normalized_str.encode()).hexdigest()[:16]
@@ -137,6 +140,49 @@ MANIFEST_SCHEMA_V2 = {
137
140
  "auth": {
138
141
  "type": "object",
139
142
  "properties": {
143
+ "mode": {
144
+ "type": "string",
145
+ "enum": ["app", "shared"],
146
+ "default": "app",
147
+ "description": (
148
+ "Authentication mode: 'app' for per-app tokens "
149
+ "(isolated auth per app, default), 'shared' for "
150
+ "shared user pool with SSO across all apps. "
151
+ "Modes are mutually exclusive."
152
+ ),
153
+ },
154
+ "roles": {
155
+ "type": "array",
156
+ "items": {"type": "string"},
157
+ "description": (
158
+ "Available roles for this app (shared mode only). "
159
+ "Example: ['viewer', 'editor', 'admin']"
160
+ ),
161
+ },
162
+ "default_role": {
163
+ "type": "string",
164
+ "description": (
165
+ "Default role assigned to new users for this app "
166
+ "(shared mode only). Must be one of the defined roles."
167
+ ),
168
+ },
169
+ "require_role": {
170
+ "type": "string",
171
+ "description": (
172
+ "Minimum role required to access this app "
173
+ "(shared mode only). Users without this role "
174
+ "will be denied access."
175
+ ),
176
+ },
177
+ "public_routes": {
178
+ "type": "array",
179
+ "items": {"type": "string"},
180
+ "description": (
181
+ "Routes that don't require authentication. "
182
+ "Supports wildcards, e.g., ['/health', '/api/public/*']. "
183
+ "Works in both auth modes."
184
+ ),
185
+ },
140
186
  "policy": {
141
187
  "type": "object",
142
188
  "properties": {
@@ -254,24 +300,44 @@ MANIFEST_SCHEMA_V2 = {
254
300
  "initial_policies": {
255
301
  "type": "array",
256
302
  "items": {
257
- "type": "object",
258
- "properties": {
259
- "role": {"type": "string"},
260
- "resource": {
261
- "type": "string",
262
- "default": "documents",
303
+ "oneOf": [
304
+ {
305
+ "type": "array",
306
+ "items": {"type": "string"},
307
+ "minItems": 3,
308
+ "maxItems": 3,
309
+ "description": (
310
+ "Casbin policy as array: "
311
+ '["role", "resource", "action"]'
312
+ ),
263
313
  },
264
- "action": {"type": "string"},
265
- },
266
- "required": ["role", "action"],
267
- "additionalProperties": False,
314
+ {
315
+ "type": "object",
316
+ "properties": {
317
+ "role": {"type": "string"},
318
+ "resource": {
319
+ "type": "string",
320
+ "default": "documents",
321
+ },
322
+ "action": {"type": "string"},
323
+ },
324
+ "required": ["role", "action"],
325
+ "additionalProperties": False,
326
+ "description": (
327
+ "OSO policy as object: "
328
+ '{"role": "admin", "resource": "documents", '
329
+ '"action": "read"}'
330
+ ),
331
+ },
332
+ ],
268
333
  },
269
334
  "description": (
270
- "Initial permission policies to set up "
271
- "in OSO Cloud on startup. Only used "
272
- "when provider is 'oso'. Example: "
273
- '[{"role": "admin", "resource": '
274
- '"documents", "action": "read"}]'
335
+ "Initial permission policies to set up on startup. "
336
+ "For Casbin provider: use arrays like "
337
+ '["admin", "clicks", "read"]. '
338
+ "For OSO Cloud provider: use objects like "
339
+ '{"role": "admin", "resource": "documents", '
340
+ '"action": "read"}.'
275
341
  ),
276
342
  },
277
343
  },
@@ -484,8 +550,7 @@ MANIFEST_SCHEMA_V2 = {
484
550
  "type": "string",
485
551
  "default": "user",
486
552
  "description": (
487
- "Role for demo user in app "
488
- "(default: 'user')"
553
+ "Role for demo user in app " "(default: 'user')"
489
554
  ),
490
555
  },
491
556
  "auto_create": {
@@ -567,6 +632,292 @@ MANIFEST_SCHEMA_V2 = {
567
632
  "independent of platform authentication."
568
633
  ),
569
634
  },
635
+ "rate_limits": {
636
+ "type": "object",
637
+ "additionalProperties": {
638
+ "type": "object",
639
+ "properties": {
640
+ "max_attempts": {
641
+ "type": "integer",
642
+ "minimum": 1,
643
+ "default": 5,
644
+ "description": (
645
+ "Maximum attempts allowed in the time window " "(default: 5)."
646
+ ),
647
+ },
648
+ "window_seconds": {
649
+ "type": "integer",
650
+ "minimum": 1,
651
+ "default": 300,
652
+ "description": (
653
+ "Time window in seconds for rate limiting "
654
+ "(default: 300 = 5 minutes)."
655
+ ),
656
+ },
657
+ },
658
+ "additionalProperties": False,
659
+ },
660
+ "description": (
661
+ "Rate limiting configuration for auth endpoints. "
662
+ "Keys are endpoint paths (e.g., '/login', '/register'). "
663
+ "Example: {'/login': {'max_attempts': 5, 'window_seconds': 300}}"
664
+ ),
665
+ },
666
+ "audit": {
667
+ "type": "object",
668
+ "properties": {
669
+ "enabled": {
670
+ "type": "boolean",
671
+ "default": True,
672
+ "description": (
673
+ "Enable audit logging for authentication events "
674
+ "(default: true for shared auth mode)."
675
+ ),
676
+ },
677
+ "retention_days": {
678
+ "type": "integer",
679
+ "minimum": 1,
680
+ "default": 90,
681
+ "description": (
682
+ "Number of days to retain audit logs " "(default: 90 days)."
683
+ ),
684
+ },
685
+ },
686
+ "additionalProperties": False,
687
+ "description": (
688
+ "Audit logging configuration for authentication events. "
689
+ "Logs are stored in MongoDB with automatic TTL cleanup."
690
+ ),
691
+ },
692
+ "csrf_protection": {
693
+ "oneOf": [
694
+ {"type": "boolean"},
695
+ {
696
+ "type": "object",
697
+ "properties": {
698
+ "enabled": {
699
+ "type": "boolean",
700
+ "default": True,
701
+ "description": (
702
+ "Enable CSRF protection "
703
+ "(default: true for shared auth mode)."
704
+ ),
705
+ },
706
+ "exempt_routes": {
707
+ "type": "array",
708
+ "items": {"type": "string"},
709
+ "description": (
710
+ "Routes exempt from CSRF validation. "
711
+ "Supports wildcards (e.g., '/api/*'). "
712
+ "Defaults to public_routes if not specified."
713
+ ),
714
+ },
715
+ "rotate_tokens": {
716
+ "type": "boolean",
717
+ "default": False,
718
+ "description": (
719
+ "Rotate CSRF token on each request "
720
+ "(more secure, less convenient). Default: false."
721
+ ),
722
+ },
723
+ "token_ttl": {
724
+ "type": "integer",
725
+ "minimum": 60,
726
+ "default": 3600,
727
+ "description": (
728
+ "CSRF token TTL in seconds " "(default: 3600 = 1 hour)."
729
+ ),
730
+ },
731
+ },
732
+ "additionalProperties": False,
733
+ },
734
+ ],
735
+ "default": True,
736
+ "description": (
737
+ "CSRF protection configuration. Auto-enabled for shared "
738
+ "auth mode. Set to false to disable, or provide object "
739
+ "for detailed configuration. Uses double-submit cookie "
740
+ "pattern with SameSite=Lax cookies."
741
+ ),
742
+ },
743
+ "security": {
744
+ "type": "object",
745
+ "properties": {
746
+ "hsts": {
747
+ "type": "object",
748
+ "properties": {
749
+ "enabled": {
750
+ "type": "boolean",
751
+ "default": True,
752
+ "description": (
753
+ "Enable HSTS header in production " "(default: true)."
754
+ ),
755
+ },
756
+ "max_age": {
757
+ "type": "integer",
758
+ "minimum": 0,
759
+ "default": 31536000,
760
+ "description": (
761
+ "HSTS max-age in seconds " "(default: 31536000 = 1 year)."
762
+ ),
763
+ },
764
+ "include_subdomains": {
765
+ "type": "boolean",
766
+ "default": True,
767
+ "description": (
768
+ "Include subdomains in HSTS policy " "(default: true)."
769
+ ),
770
+ },
771
+ "preload": {
772
+ "type": "boolean",
773
+ "default": False,
774
+ "description": (
775
+ "Add preload directive for HSTS preload "
776
+ "list submission (default: false). Only "
777
+ "enable if you're ready for permanent HTTPS."
778
+ ),
779
+ },
780
+ },
781
+ "additionalProperties": False,
782
+ "description": (
783
+ "HTTP Strict Transport Security configuration. "
784
+ "Forces HTTPS connections in production."
785
+ ),
786
+ },
787
+ },
788
+ "additionalProperties": False,
789
+ "description": (
790
+ "Security settings including HSTS, headers, and other " "security controls."
791
+ ),
792
+ },
793
+ "jwt": {
794
+ "type": "object",
795
+ "properties": {
796
+ "algorithm": {
797
+ "type": "string",
798
+ "enum": ["HS256", "RS256", "ES256"],
799
+ "default": "HS256",
800
+ "description": (
801
+ "JWT signing algorithm. HS256 (HMAC, default) "
802
+ "uses symmetric secret. RS256 (RSA) and ES256 "
803
+ "(ECDSA) use asymmetric key pairs for better "
804
+ "security in distributed systems."
805
+ ),
806
+ },
807
+ "token_expiry_hours": {
808
+ "type": "integer",
809
+ "minimum": 1,
810
+ "default": 24,
811
+ "description": ("JWT token expiry in hours (default: 24)."),
812
+ },
813
+ },
814
+ "additionalProperties": False,
815
+ "description": (
816
+ "JWT configuration for shared auth mode. "
817
+ "Controls algorithm and token lifetime."
818
+ ),
819
+ },
820
+ "password_policy": {
821
+ "type": "object",
822
+ "properties": {
823
+ "min_length": {
824
+ "type": "integer",
825
+ "minimum": 6,
826
+ "default": 12,
827
+ "description": ("Minimum password length (default: 12)."),
828
+ },
829
+ "min_entropy_bits": {
830
+ "type": "integer",
831
+ "minimum": 0,
832
+ "default": 50,
833
+ "description": (
834
+ "Minimum password entropy in bits "
835
+ "(default: 50). Set to 0 to disable."
836
+ ),
837
+ },
838
+ "require_uppercase": {
839
+ "type": "boolean",
840
+ "default": True,
841
+ "description": (
842
+ "Require at least one uppercase letter " "(default: true)."
843
+ ),
844
+ },
845
+ "require_lowercase": {
846
+ "type": "boolean",
847
+ "default": True,
848
+ "description": (
849
+ "Require at least one lowercase letter " "(default: true)."
850
+ ),
851
+ },
852
+ "require_numbers": {
853
+ "type": "boolean",
854
+ "default": True,
855
+ "description": ("Require at least one number " "(default: true)."),
856
+ },
857
+ "require_special": {
858
+ "type": "boolean",
859
+ "default": False,
860
+ "description": (
861
+ "Require at least one special character " "(default: false)."
862
+ ),
863
+ },
864
+ "check_common_passwords": {
865
+ "type": "boolean",
866
+ "default": True,
867
+ "description": (
868
+ "Check against common password list " "(default: true)."
869
+ ),
870
+ },
871
+ "check_breaches": {
872
+ "type": "boolean",
873
+ "default": False,
874
+ "description": (
875
+ "Check against HaveIBeenPwned breach database "
876
+ "(default: false). Requires network access."
877
+ ),
878
+ },
879
+ },
880
+ "additionalProperties": False,
881
+ "description": (
882
+ "Password policy configuration for shared auth mode. "
883
+ "Enforces password strength requirements."
884
+ ),
885
+ },
886
+ "session_binding": {
887
+ "type": "object",
888
+ "properties": {
889
+ "bind_ip": {
890
+ "type": "boolean",
891
+ "default": False,
892
+ "description": (
893
+ "Bind sessions to IP address. Strict mode: "
894
+ "reject if IP changes (default: false)."
895
+ ),
896
+ },
897
+ "bind_fingerprint": {
898
+ "type": "boolean",
899
+ "default": True,
900
+ "description": (
901
+ "Bind sessions to device fingerprint. Soft mode: "
902
+ "log warning if fingerprint changes (default: true)."
903
+ ),
904
+ },
905
+ "allow_ip_change_with_reauth": {
906
+ "type": "boolean",
907
+ "default": True,
908
+ "description": (
909
+ "Allow IP change if user re-authenticates "
910
+ "(default: true). Only applies when bind_ip=true."
911
+ ),
912
+ },
913
+ },
914
+ "additionalProperties": False,
915
+ "description": (
916
+ "Session binding configuration. Ties sessions to client "
917
+ "characteristics for additional security against session "
918
+ "hijacking."
919
+ ),
920
+ },
570
921
  },
571
922
  "additionalProperties": False,
572
923
  "description": (
@@ -590,17 +941,13 @@ MANIFEST_SCHEMA_V2 = {
590
941
  "type": "integer",
591
942
  "minimum": 60,
592
943
  "default": 900,
593
- "description": (
594
- "Access token TTL in seconds " "(default: 900 = 15 minutes)."
595
- ),
944
+ "description": ("Access token TTL in seconds " "(default: 900 = 15 minutes)."),
596
945
  },
597
946
  "refresh_token_ttl": {
598
947
  "type": "integer",
599
948
  "minimum": 3600,
600
949
  "default": 604800,
601
- "description": (
602
- "Refresh token TTL in seconds " "(default: 604800 = 7 days)."
603
- ),
950
+ "description": ("Refresh token TTL in seconds " "(default: 604800 = 7 days)."),
604
951
  },
605
952
  "token_rotation": {
606
953
  "type": "boolean",
@@ -615,8 +962,7 @@ MANIFEST_SCHEMA_V2 = {
615
962
  "minimum": 1,
616
963
  "default": 10,
617
964
  "description": (
618
- "Maximum number of concurrent sessions per user "
619
- "(default: 10)."
965
+ "Maximum number of concurrent sessions per user " "(default: 10)."
620
966
  ),
621
967
  },
622
968
  "session_inactivity_timeout": {
@@ -635,8 +981,7 @@ MANIFEST_SCHEMA_V2 = {
635
981
  "type": "boolean",
636
982
  "default": False,
637
983
  "description": (
638
- "Require HTTPS in production "
639
- "(default: false, auto-detected)."
984
+ "Require HTTPS in production " "(default: false, auto-detected)."
640
985
  ),
641
986
  },
642
987
  "cookie_secure": {
@@ -718,9 +1063,7 @@ MANIFEST_SCHEMA_V2 = {
718
1063
  },
719
1064
  },
720
1065
  "additionalProperties": False,
721
- "description": (
722
- "Rate limiting configuration per endpoint type."
723
- ),
1066
+ "description": ("Rate limiting configuration per endpoint type."),
724
1067
  },
725
1068
  "password_policy": {
726
1069
  "type": "object",
@@ -747,9 +1090,7 @@ MANIFEST_SCHEMA_V2 = {
747
1090
  "require_lowercase": {
748
1091
  "type": "boolean",
749
1092
  "default": True,
750
- "description": (
751
- "Require lowercase letters " "(default: true)"
752
- ),
1093
+ "description": ("Require lowercase letters " "(default: true)"),
753
1094
  },
754
1095
  "require_numbers": {
755
1096
  "type": "boolean",
@@ -774,24 +1115,21 @@ MANIFEST_SCHEMA_V2 = {
774
1115
  "type": "boolean",
775
1116
  "default": True,
776
1117
  "description": (
777
- "Enable session fingerprinting "
778
- "(default: true)"
1118
+ "Enable session fingerprinting " "(default: true)"
779
1119
  ),
780
1120
  },
781
1121
  "validate_on_login": {
782
1122
  "type": "boolean",
783
1123
  "default": True,
784
1124
  "description": (
785
- "Validate fingerprint on login "
786
- "(default: true)"
1125
+ "Validate fingerprint on login " "(default: true)"
787
1126
  ),
788
1127
  },
789
1128
  "validate_on_refresh": {
790
1129
  "type": "boolean",
791
1130
  "default": True,
792
1131
  "description": (
793
- "Validate fingerprint on token refresh "
794
- "(default: true)"
1132
+ "Validate fingerprint on token refresh " "(default: true)"
795
1133
  ),
796
1134
  },
797
1135
  "validate_on_request": {
@@ -837,8 +1175,7 @@ MANIFEST_SCHEMA_V2 = {
837
1175
  "minimum": 1,
838
1176
  "default": 900,
839
1177
  "description": (
840
- "Lockout duration in seconds "
841
- "(default: 900 = 15 minutes)"
1178
+ "Lockout duration in seconds " "(default: 900 = 15 minutes)"
842
1179
  ),
843
1180
  },
844
1181
  "reset_on_success": {
@@ -860,8 +1197,7 @@ MANIFEST_SCHEMA_V2 = {
860
1197
  "type": "boolean",
861
1198
  "default": False,
862
1199
  "description": (
863
- "Enable IP address validation "
864
- "(default: false)"
1200
+ "Enable IP address validation " "(default: false)"
865
1201
  ),
866
1202
  },
867
1203
  "strict": {
@@ -876,8 +1212,7 @@ MANIFEST_SCHEMA_V2 = {
876
1212
  "type": "boolean",
877
1213
  "default": True,
878
1214
  "description": (
879
- "Allow IP address changes during session "
880
- "(default: true)"
1215
+ "Allow IP address changes during session " "(default: true)"
881
1216
  ),
882
1217
  },
883
1218
  },
@@ -897,9 +1232,7 @@ MANIFEST_SCHEMA_V2 = {
897
1232
  "bind_to_device": {
898
1233
  "type": "boolean",
899
1234
  "default": True,
900
- "description": (
901
- "Bind tokens to device ID " "(default: true)"
902
- ),
1235
+ "description": ("Bind tokens to device ID " "(default: true)"),
903
1236
  },
904
1237
  },
905
1238
  "additionalProperties": False,
@@ -913,8 +1246,7 @@ MANIFEST_SCHEMA_V2 = {
913
1246
  "type": "boolean",
914
1247
  "default": True,
915
1248
  "description": (
916
- "Automatically set up token management on app startup "
917
- "(default: true)."
1249
+ "Automatically set up token management on app startup " "(default: true)."
918
1250
  ),
919
1251
  },
920
1252
  },
@@ -946,9 +1278,7 @@ MANIFEST_SCHEMA_V2 = {
946
1278
  },
947
1279
  "collection_settings": {
948
1280
  "type": "object",
949
- "patternProperties": {
950
- "^[a-zA-Z0-9_]+$": {"$ref": "#/definitions/collectionSettings"}
951
- },
1281
+ "patternProperties": {"^[a-zA-Z0-9_]+$": {"$ref": "#/definitions/collectionSettings"}},
952
1282
  "description": "Collection name -> collection settings",
953
1283
  },
954
1284
  "websockets": {
@@ -996,8 +1326,7 @@ MANIFEST_SCHEMA_V2 = {
996
1326
  "description": {
997
1327
  "type": "string",
998
1328
  "description": (
999
- "Description of what this WebSocket endpoint "
1000
- "is used for"
1329
+ "Description of what this WebSocket endpoint " "is used for"
1001
1330
  ),
1002
1331
  },
1003
1332
  "ping_interval": {
@@ -1210,8 +1539,7 @@ MANIFEST_SCHEMA_V2 = {
1210
1539
  "type": "boolean",
1211
1540
  "default": False,
1212
1541
  "description": (
1213
- "Allow credentials (cookies, authorization headers) "
1214
- "in CORS requests"
1542
+ "Allow credentials (cookies, authorization headers) " "in CORS requests"
1215
1543
  ),
1216
1544
  },
1217
1545
  "allow_methods": {
@@ -1256,6 +1584,40 @@ MANIFEST_SCHEMA_V2 = {
1256
1584
  "additionalProperties": False,
1257
1585
  "description": "CORS (Cross-Origin Resource Sharing) configuration for web apps",
1258
1586
  },
1587
+ "data_access": {
1588
+ "type": "object",
1589
+ "properties": {
1590
+ "read_scopes": {
1591
+ "type": "array",
1592
+ "items": {"type": "string"},
1593
+ "description": (
1594
+ "List of app slugs this app can read from. "
1595
+ "Defaults to [app_slug] if not specified."
1596
+ ),
1597
+ },
1598
+ "write_scope": {
1599
+ "type": "string",
1600
+ "description": (
1601
+ "App slug this app writes to. " "Defaults to app_slug if not specified."
1602
+ ),
1603
+ },
1604
+ "cross_app_policy": {
1605
+ "type": "string",
1606
+ "enum": ["explicit", "deny_all"],
1607
+ "default": "explicit",
1608
+ "description": (
1609
+ "Policy for cross-app access. 'explicit' allows access "
1610
+ "to apps listed in read_scopes. 'deny_all' blocks all "
1611
+ "cross-app access regardless of read_scopes."
1612
+ ),
1613
+ },
1614
+ },
1615
+ "additionalProperties": False,
1616
+ "description": (
1617
+ "Data access configuration defining which apps this app can "
1618
+ "read from and write to. Used for cross-app data access control."
1619
+ ),
1620
+ },
1259
1621
  "observability": {
1260
1622
  "type": "object",
1261
1623
  "properties": {
@@ -1295,8 +1657,7 @@ MANIFEST_SCHEMA_V2 = {
1295
1657
  "type": "boolean",
1296
1658
  "default": True,
1297
1659
  "description": (
1298
- "Collect operation-level metrics "
1299
- "(duration, errors, etc.)"
1660
+ "Collect operation-level metrics " "(duration, errors, etc.)"
1300
1661
  ),
1301
1662
  },
1302
1663
  "collect_performance_metrics": {
@@ -1456,8 +1817,7 @@ MANIFEST_SCHEMA_V2 = {
1456
1817
  "definition": {
1457
1818
  "type": "object",
1458
1819
  "description": (
1459
- "Index definition (required for vectorSearch and "
1460
- "search indexes)"
1820
+ "Index definition (required for vectorSearch and " "search indexes)"
1461
1821
  ),
1462
1822
  },
1463
1823
  "hybrid": {
@@ -1470,8 +1830,7 @@ MANIFEST_SCHEMA_V2 = {
1470
1830
  "type": "string",
1471
1831
  "pattern": "^[a-zA-Z0-9_]+$",
1472
1832
  "description": (
1473
- "Name for the vector index "
1474
- "(defaults to '{name}_vector')"
1833
+ "Name for the vector index " "(defaults to '{name}_vector')"
1475
1834
  ),
1476
1835
  },
1477
1836
  "definition": {
@@ -1493,8 +1852,7 @@ MANIFEST_SCHEMA_V2 = {
1493
1852
  "type": "string",
1494
1853
  "pattern": "^[a-zA-Z0-9_]+$",
1495
1854
  "description": (
1496
- "Name for the text index "
1497
- "(defaults to '{name}_text')"
1855
+ "Name for the text index " "(defaults to '{name}_text')"
1498
1856
  ),
1499
1857
  },
1500
1858
  "definition": {
@@ -1575,9 +1933,7 @@ MANIFEST_SCHEMA_V2 = {
1575
1933
  "then": {"required": ["keys", "options"]},
1576
1934
  "else": {
1577
1935
  "properties": {
1578
- "options": {
1579
- "not": {"required": ["partialFilterExpression"]}
1580
- }
1936
+ "options": {"not": {"required": ["partialFilterExpression"]}}
1581
1937
  }
1582
1938
  },
1583
1939
  },
@@ -1803,9 +2159,7 @@ def migrate_manifest(
1803
2159
 
1804
2160
  # No data transformation needed - V2.0 is backward compatible
1805
2161
  # New fields (auth, etc.) are optional
1806
- logger.debug(
1807
- f"Migrated manifest from 1.0 to 2.0: {migrated.get('slug', 'unknown')}"
1808
- )
2162
+ logger.debug(f"Migrated manifest from 1.0 to 2.0: {migrated.get('slug', 'unknown')}")
1809
2163
 
1810
2164
  # Future: Add more migration paths as needed
1811
2165
  # Example: 2.0 -> 3.0, etc.
@@ -1841,8 +2195,7 @@ def get_schema_for_version(version: str) -> Dict[str, Any]:
1841
2195
 
1842
2196
  # Fallback to current
1843
2197
  logger.warning(
1844
- f"Schema version {version} not found, using current version "
1845
- f"{CURRENT_SCHEMA_VERSION}"
2198
+ f"Schema version {version} not found, using current version " f"{CURRENT_SCHEMA_VERSION}"
1846
2199
  )
1847
2200
  return SCHEMA_REGISTRY[CURRENT_SCHEMA_VERSION]
1848
2201
 
@@ -1873,12 +2226,11 @@ async def _validate_manifest_async(
1873
2226
  Use validate_manifest_with_db() for database validation.
1874
2227
  """
1875
2228
  # Check cache first
2229
+ cache_key = None
1876
2230
  if use_cache:
1877
- cache_key = (
1878
- _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1879
- )
1880
- if cache_key in _validation_cache:
1881
- return _validation_cache[cache_key]
2231
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
2232
+ if cache_key in _validation_cache:
2233
+ return _validation_cache[cache_key]
1882
2234
 
1883
2235
  try:
1884
2236
  # Get schema version
@@ -1935,11 +2287,7 @@ async def _validate_manifest_async(
1935
2287
  error_message = f"Invalid schema definition: {e.message}"
1936
2288
  result = (False, error_message, ["schema"])
1937
2289
  if use_cache:
1938
- cache_key = (
1939
- _get_manifest_hash(manifest_data)
1940
- + "_"
1941
- + get_schema_version(manifest_data)
1942
- )
2290
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1943
2291
  _validation_cache[cache_key] = result
1944
2292
 
1945
2293
  return result
@@ -1949,9 +2297,7 @@ async def _validate_manifest_async(
1949
2297
  error_paths = []
1950
2298
  error_messages = []
1951
2299
  if isinstance(e, ValidationError):
1952
- error_paths = [
1953
- f".{'.'.join(str(p) for p in error.path)}" for error in e.context or [e]
1954
- ]
2300
+ error_paths = [f".{'.'.join(str(p) for p in error.path)}" for error in e.context or [e]]
1955
2301
  error_messages = [error.message for error in e.context or [e]]
1956
2302
  else:
1957
2303
  error_messages = [str(e)]
@@ -1959,11 +2305,7 @@ async def _validate_manifest_async(
1959
2305
  error_message = "; ".join(error_messages) if error_messages else str(e)
1960
2306
  result = (False, error_message, error_paths if error_paths else None)
1961
2307
  if use_cache:
1962
- cache_key = (
1963
- _get_manifest_hash(manifest_data)
1964
- + "_"
1965
- + get_schema_version(manifest_data)
1966
- )
2308
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1967
2309
  _validation_cache[cache_key] = result
1968
2310
 
1969
2311
  return result
@@ -1973,11 +2315,7 @@ async def _validate_manifest_async(
1973
2315
  logger.exception("Unexpected error during manifest validation")
1974
2316
  result = (False, error_message, None)
1975
2317
  if use_cache:
1976
- cache_key = (
1977
- _get_manifest_hash(manifest_data)
1978
- + "_"
1979
- + get_schema_version(manifest_data)
1980
- )
2318
+ cache_key = _get_manifest_hash(manifest_data) + "_" + get_schema_version(manifest_data)
1981
2319
  _validation_cache[cache_key] = result
1982
2320
 
1983
2321
  return result
@@ -2006,27 +2344,21 @@ async def validate_manifests_parallel(
2006
2344
  """
2007
2345
 
2008
2346
  async def validate_one(
2009
- manifest: Dict[str, Any]
2347
+ manifest: Dict[str, Any],
2010
2348
  ) -> Tuple[bool, Optional[str], Optional[List[str]], Optional[str]]:
2011
2349
  slug = manifest.get("slug", "unknown")
2012
- is_valid, error, paths = await _validate_manifest_async(
2013
- manifest, use_cache=use_cache
2014
- )
2350
+ is_valid, error, paths = await _validate_manifest_async(manifest, use_cache=use_cache)
2015
2351
  return (is_valid, error, paths, slug)
2016
2352
 
2017
2353
  # Run validations in parallel
2018
- results = await asyncio.gather(
2019
- *[validate_one(m) for m in manifests], return_exceptions=True
2020
- )
2354
+ results = await asyncio.gather(*[validate_one(m) for m in manifests], return_exceptions=True)
2021
2355
 
2022
2356
  # Handle exceptions
2023
2357
  validated_results = []
2024
2358
  for i, result in enumerate(results):
2025
2359
  if isinstance(result, Exception):
2026
2360
  slug = manifests[i].get("slug", "unknown")
2027
- validated_results.append(
2028
- (False, f"Validation error: {str(result)}", None, slug)
2029
- )
2361
+ validated_results.append((False, f"Validation error: {str(result)}", None, slug))
2030
2362
  else:
2031
2363
  validated_results.append(result)
2032
2364
 
@@ -2072,9 +2404,7 @@ async def validate_developer_id(
2072
2404
  f"developer_id '{developer_id}' does not exist or does not have developer role",
2073
2405
  )
2074
2406
  except (ValueError, TypeError, AttributeError) as e:
2075
- logger.exception(
2076
- f"Validation error validating developer_id '{developer_id}'"
2077
- )
2407
+ logger.exception(f"Validation error validating developer_id '{developer_id}'")
2078
2408
  return False, f"Error validating developer_id: {e}"
2079
2409
 
2080
2410
  return True, None
@@ -2154,15 +2484,11 @@ def validate_manifest(
2154
2484
 
2155
2485
  with concurrent.futures.ThreadPoolExecutor() as executor:
2156
2486
  future = executor.submit(
2157
- lambda: asyncio.run(
2158
- _validate_manifest_async(manifest_data, use_cache)
2159
- )
2487
+ lambda: asyncio.run(_validate_manifest_async(manifest_data, use_cache))
2160
2488
  )
2161
2489
  return future.result()
2162
2490
  else:
2163
- return loop.run_until_complete(
2164
- _validate_manifest_async(manifest_data, use_cache)
2165
- )
2491
+ return loop.run_until_complete(_validate_manifest_async(manifest_data, use_cache))
2166
2492
  except RuntimeError:
2167
2493
  # No event loop, create one
2168
2494
  return asyncio.run(_validate_manifest_async(manifest_data, use_cache))
@@ -2186,8 +2512,7 @@ def _validate_regular_index(
2186
2512
  ):
2187
2513
  return (
2188
2514
  False,
2189
- f"Regular index '{index_name}' in collection "
2190
- f"'{collection_name}' has empty 'keys'",
2515
+ f"Regular index '{index_name}' in collection " f"'{collection_name}' has empty 'keys'",
2191
2516
  )
2192
2517
 
2193
2518
  # Check for _id index
@@ -2214,8 +2539,7 @@ def _validate_ttl_index(
2214
2539
  if "keys" not in index_def:
2215
2540
  return (
2216
2541
  False,
2217
- f"TTL index '{index_name}' in collection '{collection_name}' "
2218
- f"requires 'keys' field",
2542
+ f"TTL index '{index_name}' in collection '{collection_name}' " f"requires 'keys' field",
2219
2543
  )
2220
2544
  options = index_def.get("options", {})
2221
2545
  if "expireAfterSeconds" not in options:
@@ -2305,9 +2629,7 @@ def _validate_geospatial_index(
2305
2629
  if isinstance(keys, dict):
2306
2630
  has_geo = any(v in ["2dsphere", "2d", "geoHaystack"] for v in keys.values())
2307
2631
  elif isinstance(keys, list):
2308
- has_geo = any(
2309
- len(k) >= 2 and k[1] in ["2dsphere", "2d", "geoHaystack"] for k in keys
2310
- )
2632
+ has_geo = any(len(k) >= 2 and k[1] in ["2dsphere", "2d", "geoHaystack"] for k in keys)
2311
2633
  if not has_geo:
2312
2634
  return (
2313
2635
  False,
@@ -2448,8 +2770,7 @@ def validate_index_definition(
2448
2770
  if not index_type:
2449
2771
  return (
2450
2772
  False,
2451
- f"Index '{index_name}' in collection '{collection_name}' "
2452
- f"is missing 'type' field",
2773
+ f"Index '{index_name}' in collection '{collection_name}' " f"is missing 'type' field",
2453
2774
  )
2454
2775
 
2455
2776
  # Type-specific validation
@@ -2464,9 +2785,7 @@ def validate_index_definition(
2464
2785
  elif index_type == "geospatial":
2465
2786
  return _validate_geospatial_index(index_def, collection_name, index_name)
2466
2787
  elif index_type in ("vectorSearch", "search"):
2467
- return _validate_vector_search_index(
2468
- index_def, collection_name, index_name, index_type
2469
- )
2788
+ return _validate_vector_search_index(index_def, collection_name, index_name, index_type)
2470
2789
  elif index_type == "hybrid":
2471
2790
  return _validate_hybrid_index(index_def, collection_name, index_name)
2472
2791
  else:
@@ -2478,7 +2797,7 @@ def validate_index_definition(
2478
2797
 
2479
2798
 
2480
2799
  def validate_managed_indexes(
2481
- managed_indexes: Dict[str, List[Dict[str, Any]]]
2800
+ managed_indexes: Dict[str, List[Dict[str, Any]]],
2482
2801
  ) -> Tuple[bool, Optional[str]]:
2483
2802
  """
2484
2803
  Validate all managed indexes with collection and index context.
@@ -2516,9 +2835,7 @@ def validate_managed_indexes(
2516
2835
  )
2517
2836
 
2518
2837
  index_name = index_def.get("name", f"index_{idx}")
2519
- is_valid, error_msg = validate_index_definition(
2520
- index_def, collection_name, index_name
2521
- )
2838
+ is_valid, error_msg = validate_index_definition(index_def, collection_name, index_name)
2522
2839
  if not is_valid:
2523
2840
  return False, error_msg
2524
2841
 
@@ -2600,13 +2917,11 @@ class ManifestValidator:
2600
2917
  Returns:
2601
2918
  Tuple of (is_valid, error_message, error_paths)
2602
2919
  """
2603
- return await validate_manifest_with_db(
2604
- manifest, db_validator, use_cache=use_cache
2605
- )
2920
+ return await validate_manifest_with_db(manifest, db_validator, use_cache=use_cache)
2606
2921
 
2607
2922
  @staticmethod
2608
2923
  def validate_managed_indexes(
2609
- managed_indexes: Dict[str, List[Dict[str, Any]]]
2924
+ managed_indexes: Dict[str, List[Dict[str, Any]]],
2610
2925
  ) -> Tuple[bool, Optional[str]]:
2611
2926
  """
2612
2927
  Validate managed indexes configuration.
@@ -2720,17 +3035,13 @@ class ManifestParser:
2720
3035
  if validate:
2721
3036
  is_valid, error, paths = ManifestValidator.validate(manifest_data)
2722
3037
  if not is_valid:
2723
- error_path_str = (
2724
- f" (errors in: {', '.join(paths[:3])})" if paths else ""
2725
- )
3038
+ error_path_str = f" (errors in: {', '.join(paths[:3])})" if paths else ""
2726
3039
  raise ValueError(f"Manifest validation failed: {error}{error_path_str}")
2727
3040
 
2728
3041
  return manifest_data
2729
3042
 
2730
3043
  @staticmethod
2731
- async def load_from_dict(
2732
- data: Dict[str, Any], validate: bool = True
2733
- ) -> Dict[str, Any]:
3044
+ async def load_from_dict(data: Dict[str, Any], validate: bool = True) -> Dict[str, Any]:
2734
3045
  """
2735
3046
  Load and validate manifest from dictionary.
2736
3047
 
@@ -2748,9 +3059,7 @@ class ManifestParser:
2748
3059
  if validate:
2749
3060
  is_valid, error, paths = ManifestValidator.validate(data)
2750
3061
  if not is_valid:
2751
- error_path_str = (
2752
- f" (errors in: {', '.join(paths[:3])})" if paths else ""
2753
- )
3062
+ error_path_str = f" (errors in: {', '.join(paths[:3])})" if paths else ""
2754
3063
  raise ValueError(f"Manifest validation failed: {error}{error_path_str}")
2755
3064
 
2756
3065
  return data.copy()