hexcore 11.2.0__tar.gz → 11.4.0__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.
Files changed (397) hide show
  1. {hexcore-11.2.0/hexcore.egg-info → hexcore-11.4.0}/PKG-INFO +1 -1
  2. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/config.py +29 -3
  3. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/services.py +14 -0
  4. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/exceptions.py +23 -0
  5. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/ports.py +20 -0
  6. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/dependencies.py +5 -1
  7. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/middlewares.py +75 -7
  8. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/routers.py +62 -1
  9. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/repositories.py +12 -0
  10. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/repositories.py +13 -0
  11. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/fakes.py +9 -0
  12. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/fastapi.py +4 -0
  13. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/fastapi.pyi +3 -1
  14. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/app.py +34 -9
  15. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/middlewares.py +64 -0
  16. {hexcore-11.2.0 → hexcore-11.4.0/hexcore.egg-info}/PKG-INFO +1 -1
  17. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/SOURCES.txt +1 -0
  18. {hexcore-11.2.0 → hexcore-11.4.0}/pyproject.toml +1 -1
  19. hexcore-11.4.0/tests/test_api_cors_dinamico.py +250 -0
  20. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_http.py +354 -0
  21. {hexcore-11.2.0 → hexcore-11.4.0}/LICENSE +0 -0
  22. {hexcore-11.2.0 → hexcore-11.4.0}/MANIFEST.in +0 -0
  23. {hexcore-11.2.0 → hexcore-11.4.0}/README.md +0 -0
  24. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/__init__.py +0 -0
  25. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/__main__.py +0 -0
  26. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/_deprecation.py +0 -0
  27. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/__init__.py +0 -0
  28. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/__init__.py +0 -0
  29. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/adapters.py +0 -0
  30. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/config.py +0 -0
  31. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/factory.py +0 -0
  32. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/in_memory_buses.py +0 -0
  33. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/pipeline.py +0 -0
  34. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/registry.py +0 -0
  35. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/scheduler.py +0 -0
  36. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/__init__.py +0 -0
  37. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/base.py +0 -0
  38. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/cursor.py +0 -0
  39. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/errors.py +0 -0
  40. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/query.py +0 -0
  41. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/__init__.py +0 -0
  42. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/config.py +0 -0
  43. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/factory.py +0 -0
  44. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/projector.py +0 -0
  45. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/relay.py +0 -0
  46. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/repository.py +0 -0
  47. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/use_cases/__init__.py +0 -0
  48. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/use_cases/base.py +0 -0
  49. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/use_cases/query.py +0 -0
  50. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/capabilities.py +0 -0
  51. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/cqrs.py +0 -0
  52. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/cqrs.pyi +0 -0
  53. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/__init__.py +0 -0
  54. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/__init__.pyi +0 -0
  55. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/__init__.py +0 -0
  56. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/authorization.py +0 -0
  57. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/authz_middleware.py +0 -0
  58. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/commands.py +0 -0
  59. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/config.py +0 -0
  60. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/container.py +0 -0
  61. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/hooks.py +0 -0
  62. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/plugins.py +0 -0
  63. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/__init__.py +0 -0
  64. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/authorization.py +0 -0
  65. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/context.py +0 -0
  66. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/entities.py +0 -0
  67. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/events.py +0 -0
  68. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/permissions.py +0 -0
  69. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/plugins.py +0 -0
  70. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/value_objects.py +0 -0
  71. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/__init__.py +0 -0
  72. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/__init__.py +0 -0
  73. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/authorization.py +0 -0
  74. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/cli.py +0 -0
  75. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/clock.py +0 -0
  76. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/envelope.py +0 -0
  77. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/hashing.py +0 -0
  78. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/keys.py +0 -0
  79. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/lifespan.py +0 -0
  80. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/__init__.py +0 -0
  81. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/__init__.py +0 -0
  82. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/documents.py +0 -0
  83. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/schema.py +0 -0
  84. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/selection.py +0 -0
  85. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/__init__.py +0 -0
  86. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/models.py +0 -0
  87. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/models_mixins.py +0 -0
  88. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/registry.py +0 -0
  89. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/schema.py +0 -0
  90. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/revocation.py +0 -0
  91. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/secretbox.py +0 -0
  92. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/tokens.py +0 -0
  93. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/transports.py +0 -0
  94. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/__init__.py +0 -0
  95. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/__init__.py +0 -0
  96. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/cache.py +0 -0
  97. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/conditions.py +0 -0
  98. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/domain.py +0 -0
  99. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/__init__.py +0 -0
  100. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/beanie/__init__.py +0 -0
  101. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/beanie/repository.py +0 -0
  102. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/__init__.py +0 -0
  103. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/models.py +0 -0
  104. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/models_mixins.py +0 -0
  105. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/repository.py +0 -0
  106. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/pdp.py +0 -0
  107. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/pip.py +0 -0
  108. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/predicates.py +0 -0
  109. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/provider.py +0 -0
  110. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/router.py +0 -0
  111. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/service.py +0 -0
  112. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/__init__.py +0 -0
  113. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/commands.py +0 -0
  114. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/domain.py +0 -0
  115. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/router.py +0 -0
  116. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/service.py +0 -0
  117. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/magic_link/__init__.py +0 -0
  118. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/magic_link/commands.py +0 -0
  119. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/magic_link/router.py +0 -0
  120. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/__init__.py +0 -0
  121. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/domain.py +0 -0
  122. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/http.py +0 -0
  123. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/__init__.py +0 -0
  124. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/beanie/__init__.py +0 -0
  125. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/beanie/repository.py +0 -0
  126. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/__init__.py +0 -0
  127. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/models.py +0 -0
  128. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/models_mixins.py +0 -0
  129. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/repository.py +0 -0
  130. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/providers.py +0 -0
  131. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/router.py +0 -0
  132. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/service.py +0 -0
  133. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/__init__.py +0 -0
  134. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/domain.py +0 -0
  135. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/__init__.py +0 -0
  136. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/beanie/__init__.py +0 -0
  137. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/beanie/repository.py +0 -0
  138. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/__init__.py +0 -0
  139. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/models.py +0 -0
  140. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/models_mixins.py +0 -0
  141. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/repository.py +0 -0
  142. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/router.py +0 -0
  143. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/service.py +0 -0
  144. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/__init__.py +0 -0
  145. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/domain.py +0 -0
  146. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/__init__.py +0 -0
  147. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/beanie/__init__.py +0 -0
  148. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/beanie/repository.py +0 -0
  149. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/__init__.py +0 -0
  150. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/models.py +0 -0
  151. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/models_mixins.py +0 -0
  152. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/repository.py +0 -0
  153. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/router.py +0 -0
  154. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/service.py +0 -0
  155. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/webauthn_adapter.py +0 -0
  156. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/__init__.py +0 -0
  157. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/cache.py +0 -0
  158. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/cli.py +0 -0
  159. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/domain.py +0 -0
  160. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/matcher.py +0 -0
  161. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/__init__.py +0 -0
  162. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/beanie/__init__.py +0 -0
  163. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/beanie/repository.py +0 -0
  164. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/__init__.py +0 -0
  165. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/models.py +0 -0
  166. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/models_mixins.py +0 -0
  167. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/repository.py +0 -0
  168. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/provider.py +0 -0
  169. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/resolver.py +0 -0
  170. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/router.py +0 -0
  171. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/service.py +0 -0
  172. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/storage.py +0 -0
  173. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/__init__.py +0 -0
  174. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/commands.py +0 -0
  175. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/crypto.py +0 -0
  176. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/domain.py +0 -0
  177. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/__init__.py +0 -0
  178. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/beanie/__init__.py +0 -0
  179. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/beanie/repository.py +0 -0
  180. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/__init__.py +0 -0
  181. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/models.py +0 -0
  182. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/models_mixins.py +0 -0
  183. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/repository.py +0 -0
  184. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/router.py +0 -0
  185. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/service.py +0 -0
  186. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/totp.py +0 -0
  187. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/__init__.py +0 -0
  188. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/fixtures.py +0 -0
  189. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/helpers.py +0 -0
  190. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/__init__.py +0 -0
  191. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/base.py +0 -0
  192. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/__init__.py +0 -0
  193. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/buses.py +0 -0
  194. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/commands.py +0 -0
  195. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/context.py +0 -0
  196. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/cron.py +0 -0
  197. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/decorators.py +0 -0
  198. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/dispatch.py +0 -0
  199. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/envelope.py +0 -0
  200. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/exceptions.py +0 -0
  201. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/handlers.py +0 -0
  202. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/middleware.py +0 -0
  203. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/queries.py +0 -0
  204. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/resolution.py +0 -0
  205. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/serializer.py +0 -0
  206. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/task_queues.py +0 -0
  207. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/events.py +0 -0
  208. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/__init__.py +0 -0
  209. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/aggregate.py +0 -0
  210. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/exceptions.py +0 -0
  211. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/projections.py +0 -0
  212. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/snapshots.py +0 -0
  213. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/store.py +0 -0
  214. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/stored.py +0 -0
  215. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/exceptions.py +0 -0
  216. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/repositories.py +0 -0
  217. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/services.py +0 -0
  218. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/uow.py +0 -0
  219. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/eventsourcing.py +0 -0
  220. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/eventsourcing.pyi +0 -0
  221. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/__init__.py +0 -0
  222. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/__init__.py +0 -0
  223. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/cqrs.py +0 -0
  224. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/eventsourcing.py +0 -0
  225. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/exception_handlers.py +0 -0
  226. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/health.py +0 -0
  227. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/lifespan.py +0 -0
  228. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/rate_limit.py +0 -0
  229. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/routing.py +0 -0
  230. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/streaming.py +0 -0
  231. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/utils.py +0 -0
  232. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/__init__.py +0 -0
  233. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/cache_backends/__init__.py +0 -0
  234. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
  235. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
  236. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cli.py +0 -0
  237. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/__init__.py +0 -0
  238. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/cron_sql.py +0 -0
  239. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/middlewares.py +0 -0
  240. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/postgres_bus.py +0 -0
  241. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/postgres_lock.py +0 -0
  242. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/procrastinate.py +0 -0
  243. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/pydantic_serializer.py +0 -0
  244. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/rabbitmq.py +0 -0
  245. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/redis_bus.py +0 -0
  246. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/redis_lock.py +0 -0
  247. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/__init__.py +0 -0
  248. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/beanie_documents.py +0 -0
  249. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/beanie_store.py +0 -0
  250. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/memory.py +0 -0
  251. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/redis_store.py +0 -0
  252. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_models.py +0 -0
  253. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_models_mixins.py +0 -0
  254. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_store.py +0 -0
  255. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/__init__.py +0 -0
  256. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/_beanie_impl.py +0 -0
  257. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/_sqlalchemy_impl.py +0 -0
  258. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/base.py +0 -0
  259. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
  260. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/implementations.py +0 -0
  261. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/__init__.py +0 -0
  262. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
  263. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/beanie/utils.py +0 -0
  264. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
  265. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/id_types.py +0 -0
  266. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +0 -0
  267. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +0 -0
  268. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/utils.py +0 -0
  269. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/task_queues/__init__.py +0 -0
  270. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/task_queues/celery_adapter.py +0 -0
  271. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/task_queues/procrastinate_adapter.py +0 -0
  272. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/__init__.py +0 -0
  273. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/decorators.py +0 -0
  274. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/helpers.py +0 -0
  275. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/scopes.py +0 -0
  276. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/__init__.py +0 -0
  277. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/consumer.py +0 -0
  278. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/rabbitmq_worker.py +0 -0
  279. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/runner.py +0 -0
  280. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/py.typed +0 -0
  281. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/sql.py +0 -0
  282. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/sql.pyi +0 -0
  283. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/__init__.py +0 -0
  284. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/fakes.py +0 -0
  285. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/fixtures.py +0 -0
  286. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/helpers.py +0 -0
  287. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/repositories.py +0 -0
  288. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/types.py +0 -0
  289. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/dependency_links.txt +0 -0
  290. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/entry_points.txt +0 -0
  291. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/requires.txt +0 -0
  292. {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/top_level.txt +0 -0
  293. {hexcore-11.2.0 → hexcore-11.4.0}/setup.cfg +0 -0
  294. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_alembic_metadata_completeness.py +0 -0
  295. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_anclas_del_repo.py +0 -0
  296. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_app_and_lifespan.py +0 -0
  297. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_cqrs_providers.py +0 -0
  298. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_exception_handlers.py +0 -0
  299. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_health.py +0 -0
  300. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_middlewares_and_routing.py +0 -0
  301. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_rate_limit.py +0 -0
  302. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_streaming.py +0 -0
  303. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_basic.py +0 -0
  304. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_beanie_query_utils.py +0 -0
  305. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_celery_event_loop.py +0 -0
  306. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_config_cors_security.py +0 -0
  307. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_config_loading.py +0 -0
  308. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_consumer_optional_event_bus.py +0 -0
  309. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs.py +0 -0
  310. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs_envelope.py +0 -0
  311. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs_factory.py +0 -0
  312. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs_middlewares.py +0 -0
  313. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cron_sql.py +0 -0
  314. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_10_4_hardening.py +0 -0
  315. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_authorization_engine.py +0 -0
  316. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_authorization_http.py +0 -0
  317. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_backend_neutrality.py +0 -0
  318. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_beanie.py +0 -0
  319. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_cli.py +0 -0
  320. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_container.py +0 -0
  321. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_context.py +0 -0
  322. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_custom_plugin.py +0 -0
  323. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_conditions.py +0 -0
  324. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_pdp.py +0 -0
  325. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_router.py +0 -0
  326. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_service.py +0 -0
  327. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_envelope.py +0 -0
  328. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_facade_and_events.py +0 -0
  329. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_flows.py +0 -0
  330. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_hashing.py +0 -0
  331. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_impersonate.py +0 -0
  332. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_magic_link.py +0 -0
  333. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_models.py +0 -0
  334. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_oauth.py +0 -0
  335. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_openapi.py +0 -0
  336. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_organization.py +0 -0
  337. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_passkey.py +0 -0
  338. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_permissions.py +0 -0
  339. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_plugin_decoupling.py +0 -0
  340. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_plugins.py +0 -0
  341. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_principals.py +0 -0
  342. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_production_defaults.py +0 -0
  343. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_rbac.py +0 -0
  344. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_rbac_service.py +0 -0
  345. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_repositories.py +0 -0
  346. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_revocation.py +0 -0
  347. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_schema_registration.py +0 -0
  348. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_storage_selection.py +0 -0
  349. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_tokens.py +0 -0
  350. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_two_factor.py +0 -0
  351. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_username.py +0 -0
  352. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_value_objects.py +0 -0
  353. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_deprecations.py +0 -0
  354. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_documentation_examples.py +0 -0
  355. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_domain_base_contract.py +0 -0
  356. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_domain_service_query.py +0 -0
  357. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_dotted_resolution.py +0 -0
  358. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_event_dispatch_mro.py +0 -0
  359. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_aggregate.py +0 -0
  360. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_beanie.py +0 -0
  361. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_config.py +0 -0
  362. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_ports.py +0 -0
  363. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_projections.py +0 -0
  364. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_redis.py +0 -0
  365. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_relay.py +0 -0
  366. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_repository.py +0 -0
  367. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_snapshot_checkpoint.py +0 -0
  368. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_sql_schema.py +0 -0
  369. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_store_contract.py +0 -0
  370. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_uow_integration.py +0 -0
  371. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_facades.py +0 -0
  372. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_handler_registry.py +0 -0
  373. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_infrastructure_query_path.py +0 -0
  374. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_lock_error_policy.py +0 -0
  375. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_optional_dependencies.py +0 -0
  376. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_packaging.py +0 -0
  377. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_postgres_bus.py +0 -0
  378. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_postgres_lock.py +0 -0
  379. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_query_cursor_and_endpoint.py +0 -0
  380. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_query_field_validation.py +0 -0
  381. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_rabbitmq_bus.py +0 -0
  382. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_redis_bus.py +0 -0
  383. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_redis_lock.py +0 -0
  384. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_repositories_utils.py +0 -0
  385. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_scheduler.py +0 -0
  386. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_scheduler_catchup.py +0 -0
  387. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_smart_routing.py +0 -0
  388. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_sql_naming_convention.py +0 -0
  389. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_sql_session_layer.py +0 -0
  390. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_task_queues_adapters.py +0 -0
  391. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_testing_kit.py +0 -0
  392. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_testing_utilities.py +0 -0
  393. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_typing_gate.py +0 -0
  394. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_uow_session_regression.py +0 -0
  395. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_use_cases_query.py +0 -0
  396. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_worker_bus_integration.py +0 -0
  397. {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_worker_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexcore
3
- Version: 11.2.0
3
+ Version: 11.4.0
4
4
  Summary: Reusable core for Python projects with hexagonal architecture, DDD, CQRS and background tasks. Ships the abstractions and the infrastructure: SQL session layer, FastAPI factories, worker runner, dynamic cron and identity.
5
5
  Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
6
6
  License-Expression: MIT
@@ -78,6 +78,26 @@ class ServerConfig(BaseModel):
78
78
  allow_methods: list[str] = ["*"]
79
79
  allow_headers: list[str] = ["*"]
80
80
 
81
+ # CORS para una SPA en otro origen con la cookie de sesión. Los tres van con el prefijo
82
+ # `cors_` porque `max_age` y `expose_headers` a secas, en una config que también tiene
83
+ # cookies y TTLs, se leen como otra cosa.
84
+ #
85
+ # `cors_origin_predicate` cubre los orígenes que `allow_origins` no puede enumerar —un
86
+ # subdominio por tenant creado en caliente—, igual que
87
+ # `IdentityConfig.trusted_origin_predicate` hace con el CSRF. **Se suma** a `allow_origins`,
88
+ # no lo reemplaza. Es síncrono, recibe el origen tal cual llega en el header `Origin`
89
+ # (`"https://a.ejemplo.com"`) y si lanza se trata como «no confiable».
90
+ #
91
+ # `cors_expose_headers`: las cabeceras de la respuesta que el JS puede leer entre orígenes.
92
+ # Sin declararlas, el navegador sólo deja leer las «seguras» (`Content-Type`, etc.).
93
+ #
94
+ # `cors_max_age`: cuántos segundos el navegador cachea el preflight. Sin caché, toda
95
+ # escritura JSON con un header propio (`X-CSRF-Token`) cuesta un `OPTIONS` previo. 600 es el
96
+ # default de Starlette; Chrome topea en 7200.
97
+ cors_origin_predicate: t.Callable[[str], bool] | None = None
98
+ cors_expose_headers: list[str] = Field(default_factory=list)
99
+ cors_max_age: int = Field(default=600, ge=0)
100
+
81
101
  # caching
82
102
  cache_backend: ICache = (
83
103
  MemoryCache()
@@ -171,6 +191,9 @@ class ServerConfig(BaseModel):
171
191
  1. Si no pasaste `allow_origins`, se completa: en `debug` queda `["*"]` (comodidad
172
192
  de desarrollo, que era la intención original), y fuera de `debug` queda
173
193
  `["http://localhost:<port>"]`. Un valor explícito —incluso `[]`— se respeta.
194
+ **Con un `cors_origin_predicate` declarado queda `[]`**: el predicado es la lista, y
195
+ derivar `["*"]` bajaría `allow_credentials` a `False` en silencio y las cookies
196
+ dejarían de funcionar sin que nadie lo pidiera.
174
197
  2. **`"*"` con `allow_credentials=True` nunca es válido, y el invariante vale
175
198
  siempre, no sólo fuera de `debug`.** Es que `debug` viene en `True` por
176
199
  defecto, así que condicionarlo al entorno dejaba la combinación peligrosa
@@ -194,9 +217,12 @@ class ServerConfig(BaseModel):
194
217
  lo_pidio_el_usuario = "allow_origins" in self.model_fields_set
195
218
 
196
219
  if not lo_pidio_el_usuario and not self.allow_origins:
197
- self.allow_origins = (
198
- ["*"] if self.debug else [f"http://localhost:{self.port}"]
199
- )
220
+ if self.cors_origin_predicate is not None:
221
+ self.allow_origins = []
222
+ else:
223
+ self.allow_origins = (
224
+ ["*"] if self.debug else [f"http://localhost:{self.port}"]
225
+ )
200
226
 
201
227
  if "*" not in self.allow_origins or not self.allow_credentials:
202
228
  return self
@@ -381,6 +381,20 @@ class SessionService:
381
381
  impersonation=impersonacion,
382
382
  )
383
383
 
384
+ async def previous_session_ids(self, session_id: UUID, *, limit: int = 100) -> list[UUID]:
385
+ """
386
+ Los `sid` de las sesiones **anteriores** de la familia de `session_id` (sin incluirla).
387
+
388
+ Existe para que `CsrfMiddleware` reconozca un token anti-CSRF que fue válido para la
389
+ sesión que el refresh acaba de reemplazar. Lee la base, pero sólo en el camino de
390
+ rechazo y con tope: el camino feliz de un request no lo llama.
391
+ """
392
+ sesion = await self._sessions.get(session_id)
393
+ if sesion is None:
394
+ return []
395
+ ids = await self._sessions.list_family_session_ids(sesion.family_id, limit=limit)
396
+ return [sid for sid in ids if sid != session_id]
397
+
384
398
  # ── Rotar ─────────────────────────────────────────────────────────────────
385
399
  async def refresh(
386
400
  self, refresh_token: str, *, transport: Transport
@@ -36,6 +36,8 @@ __all__ = [
36
36
  "EmailNotVerifiedError",
37
37
  "ImpersonationNotPermittedError",
38
38
  "CsrfValidationError",
39
+ "CSRF_CODE_INVALID",
40
+ "CSRF_CODE_PREVIOUS_SESSION",
39
41
  # ── Estado (409 / 423) ──
40
42
  "EmailAlreadyRegisteredError",
41
43
  "UsernameAlreadyTakenError",
@@ -164,14 +166,35 @@ class ImpersonationNotPermittedError(AuthorizationError):
164
166
  """
165
167
 
166
168
 
169
+ #: `code` de un rechazo de CSRF cuyo token no verifica contra la sesión en curso ni contra
170
+ #: ninguna anterior de su familia: lo mandó alguien que no lo recibió del servidor.
171
+ CSRF_CODE_INVALID = "CsrfTokenInvalid"
172
+
173
+ #: `code` de un rechazo de CSRF cuyo token **sí** fue válido, pero de una sesión anterior de la
174
+ #: misma familia de rotación: el cliente firmó antes de que un refresh rotara el `sid`. Es el
175
+ #: caso que se arregla pidiendo el token vigente (`GET /auth/csrf`) y reintentando.
176
+ CSRF_CODE_PREVIOUS_SESSION = "CsrfTokenFromPreviousSession"
177
+
178
+
167
179
  class CsrfValidationError(AuthorizationError):
168
180
  """
169
181
  Falló el chequeo anti-CSRF del transporte por cookie.
170
182
 
171
183
  Sólo aplica al camino de cookie: un cliente Bearer manda el token a propósito en cada
172
184
  petición, así que no hay nada que falsificar desde otro origen.
185
+
186
+ `code` distingue, **cuando el rechazo es por el token**, entre `CSRF_CODE_INVALID` y
187
+ `CSRF_CODE_PREVIOUS_SESSION`; viaja en el cuerpo del 403 junto a `error`
188
+ (`CsrfValidationError`, que sigue siendo el discriminante). Es `None` cuando el rechazo
189
+ no es por el token (un origen en el que no se confía), porque ahí no hay nada que
190
+ recuperar. No revela nada útil a un atacante: sólo se llega a `CSRF_CODE_PREVIOUS_SESSION`
191
+ mandando un token que el servidor ya emitió para esa misma familia de sesiones.
173
192
  """
174
193
 
194
+ def __init__(self, *args: object, code: str | None = None) -> None:
195
+ super().__init__(*args)
196
+ self.code = code
197
+
175
198
 
176
199
  # ── Estado del recurso ────────────────────────────────────────────────────────
177
200
  class EmailAlreadyRegisteredError(IdentityError):
@@ -187,6 +187,26 @@ class AbstractSessionRepository(abc.ABC):
187
187
  async def revoke(self, session_id: UUID, *, at: datetime, reason: str) -> None:
188
188
  raise NotImplementedError
189
189
 
190
+ async def list_family_session_ids(
191
+ self, family_id: UUID, *, limit: int = 100
192
+ ) -> "list[UUID]":
193
+ """
194
+ Los `id` de las sesiones de una familia de rotación, las **más recientes primero**, con
195
+ tope de `limit`.
196
+
197
+ Sólo lo usa `CsrfMiddleware` para clasificar un rechazo de CSRF: reconocer que el token
198
+ que llegó era de una sesión anterior de la familia (el cliente firmó antes de que un
199
+ refresh rotara el `sid`). Incluye revocadas y consumidas: justamente las anteriores lo
200
+ están.
201
+
202
+ **No es abstracto a propósito**: es un puerto agregado después, y volverlo abstracto
203
+ rompería a todo repositorio propio ya escrito. La implementación por defecto devuelve
204
+ `[]`, y entonces el rechazo se clasifica siempre como token inválido: el comportamiento
205
+ (403) no cambia, sólo se pierde el `code` más específico.
206
+ """
207
+ del family_id, limit
208
+ return []
209
+
190
210
  @abc.abstractmethod
191
211
  async def revoke_family(self, family_id: UUID, *, at: datetime, reason: str) -> int:
192
212
  """
@@ -194,13 +194,17 @@ def identity_exception_payload(exc: Exception) -> t.Mapping[str, t.Any]:
194
194
  Darwin) devuelve sólo lo del núcleo en vez de lanzar — mismo trato que `_mapa_de_plugins()`
195
195
  en `app.py`.
196
196
  """
197
- from hexcore.darwin.domain.exceptions import AccessDeniedError
197
+ from hexcore.darwin.domain.exceptions import AccessDeniedError, CsrfValidationError
198
198
 
199
199
  base: dict[str, t.Any] = {}
200
200
  if isinstance(exc, AccessDeniedError):
201
201
  # Sólo `required`: `Decision.reason` es interno y nunca llega al cliente. Ver el
202
202
  # docstring de `AccessDeniedError`.
203
203
  base["required"] = exc.required
204
+ elif isinstance(exc, CsrfValidationError) and exc.code is not None:
205
+ # El mismo `code` que `CsrfMiddleware` pone en su 403, para que un rechazo de CSRF
206
+ # lanzado desde un handler tenga la misma forma que el del middleware.
207
+ base["code"] = exc.code
204
208
 
205
209
  from hexcore.darwin.application.container import get_identity_container
206
210
 
@@ -25,7 +25,12 @@ from starlette.requests import Request
25
25
  from starlette.responses import JSONResponse, Response
26
26
 
27
27
  from hexcore.darwin.domain.context import AUTH_CONTEXT
28
- from hexcore.darwin.domain.exceptions import CsrfValidationError, IdentityError
28
+ from hexcore.darwin.domain.exceptions import (
29
+ CSRF_CODE_INVALID,
30
+ CSRF_CODE_PREVIOUS_SESSION,
31
+ CsrfValidationError,
32
+ IdentityError,
33
+ )
29
34
 
30
35
  if t.TYPE_CHECKING:
31
36
  from hexcore.darwin.application.config import IdentityConfig
@@ -224,13 +229,74 @@ class CsrfMiddleware(BaseHTTPMiddleware):
224
229
  logger.info(
225
230
  "CSRF rechazado en %s %s: %s", request.method, request.url.path, exc
226
231
  )
227
- return JSONResponse(
228
- status_code=403,
229
- content={"detail": str(exc), "error": type(exc).__name__},
230
- )
232
+ if exc.code == CSRF_CODE_INVALID and await self._es_de_sesion_anterior(
233
+ request, contenedor
234
+ ):
235
+ exc = CsrfValidationError(
236
+ "El token anti-CSRF del header es de una sesión anterior: el refresh "
237
+ "rotó la sesión después de que el cliente lo obtuvo. Pedí el vigente con "
238
+ "`GET /auth/csrf` y repetí la petición.",
239
+ code=CSRF_CODE_PREVIOUS_SESSION,
240
+ )
241
+ content: dict[str, t.Any] = {"detail": str(exc), "error": type(exc).__name__}
242
+ if exc.code is not None:
243
+ content["code"] = exc.code
244
+ return JSONResponse(status_code=403, content=content)
231
245
 
232
246
  return await call_next(request)
233
247
 
248
+ async def _es_de_sesion_anterior(
249
+ self, request: Request, contenedor: t.Any
250
+ ) -> bool:
251
+ """
252
+ Si el token del header fue el de una sesión **anterior de la misma familia** que la de
253
+ la cookie de acceso.
254
+
255
+ Sólo corre al rechazar, así que el camino feliz de un request no paga la lectura de la
256
+ base. Tiene que ser **mejor esfuerzo**: cualquier fallo (credencial que no autentica,
257
+ base caída, un backend sin `list_family_session_ids`) devuelve `False` y el rechazo
258
+ sale como token inválido, que es lo que habría salido sin esta clasificación. Nunca
259
+ convierte un 403 en un 500.
260
+ """
261
+ from hexcore.darwin.infrastructure.hashing import compare_hashes, derive_csrf_token
262
+
263
+ enviado = request.headers.get(CSRF_HEADER)
264
+ clave = contenedor.config.secret_key
265
+ if not enviado or clave is None:
266
+ return False
267
+
268
+ try:
269
+ auth = auth_from_request(request)
270
+ if auth is None:
271
+ from hexcore.darwin.infrastructure.transports import TransportResolver
272
+
273
+ transporte = TransportResolver(cookies=contenedor.config.cookies).resolve(
274
+ request
275
+ )
276
+ token = transporte.extract_access(request)
277
+ if token is None:
278
+ return False
279
+ auth = await contenedor.session_service().authenticate(
280
+ token, transport=transporte.name
281
+ )
282
+
283
+ sid = getattr(auth.actor, "session_id", None)
284
+ if sid is None or auth.transport != "cookie":
285
+ return False
286
+
287
+ secreto = clave.get_secret_value()
288
+ previas = await contenedor.session_service().previous_session_ids(sid)
289
+ # Se recorren **todas** sin cortar en la primera que coincide: el tiempo no tiene
290
+ # que depender de cuál era (aunque `compare_hashes` ya es en tiempo constante).
291
+ coincide = False
292
+ for previa in previas:
293
+ if compare_hashes(enviado, derive_csrf_token(str(previa), secreto)):
294
+ coincide = True
295
+ return coincide
296
+ except Exception:
297
+ logger.debug("No se pudo clasificar el rechazo de CSRF.", exc_info=True)
298
+ return False
299
+
234
300
  def _exento(self, request: Request) -> bool:
235
301
  if request.method.upper() in SAFE_METHODS:
236
302
  return True
@@ -262,14 +328,16 @@ class CsrfMiddleware(BaseHTTPMiddleware):
262
328
  if not enviado or not esperado:
263
329
  raise CsrfValidationError(
264
330
  f"Falta el chequeo anti-CSRF: la petición tiene que mandar el header "
265
- f"'{CSRF_HEADER}' con el mismo valor que la cookie de CSRF."
331
+ f"'{CSRF_HEADER}' con el mismo valor que la cookie de CSRF.",
332
+ code=CSRF_CODE_INVALID,
266
333
  )
267
334
 
268
335
  from hexcore.darwin.infrastructure.hashing import compare_hashes
269
336
 
270
337
  if not compare_hashes(enviado, esperado):
271
338
  raise CsrfValidationError(
272
- "El valor anti-CSRF del header no coincide con el de la cookie."
339
+ "El valor anti-CSRF del header no coincide con el de la cookie.",
340
+ code=CSRF_CODE_INVALID,
273
341
  )
274
342
 
275
343
  @staticmethod
@@ -31,6 +31,7 @@ from hexcore.darwin.infrastructure.api.dependencies import (
31
31
  provide_auth,
32
32
  require_not_impersonated,
33
33
  )
34
+ from hexcore.darwin.infrastructure.api.middlewares import CSRF_HEADER
34
35
 
35
36
  if t.TYPE_CHECKING:
36
37
  from hexcore.darwin.application.config import IdentityConfig
@@ -42,6 +43,7 @@ __all__ = [
42
43
  "VerifyEmailRequest",
43
44
  "SignInRequest",
44
45
  "SessionResponse",
46
+ "CsrfTokenResponse",
45
47
  "MeResponse",
46
48
  "SignedOutResponse",
47
49
  "RevokedResponse",
@@ -122,6 +124,17 @@ class SignInRequest(BaseModel):
122
124
  return t.cast(str, self.identifier or self.email or self.username)
123
125
 
124
126
 
127
+ class CsrfTokenResponse(BaseModel):
128
+ """
129
+ La respuesta de `GET /auth/csrf`: el valor anti-CSRF de la sesión en curso.
130
+
131
+ Es el mismo que trae la cookie de CSRF —`HMAC(secreto, sid)`, ver `derive_csrf_token`— y
132
+ que las respuestas que la emiten repiten en la cabecera `X-CSRF-Token`.
133
+ """
134
+
135
+ csrf_token: str
136
+
137
+
125
138
  class SessionResponse(BaseModel):
126
139
  """
127
140
  La respuesta de un sign-in o un refresh.
@@ -212,6 +225,14 @@ def emit_tokens(
212
225
  a propósito: el cliente tiene que poder leerla para devolverla en el header
213
226
  `X-CSRF-Token`. Que sea legible es lo que obliga a que su valor sea derivado del `sid` y
214
227
  no aleatorio — ver `derive_csrf_token`.
228
+
229
+ **Además va en la cabecera de la respuesta** (`X-CSRF-Token`, la misma que el cliente
230
+ devuelve en cada escritura). Una SPA en otro origen que la API —`app.ejemplo.com` contra
231
+ `api.ejemplo.com`— no puede leer esa cookie con `document.cookie`, porque pertenece al host
232
+ de la API: sin la cabecera no tiene de dónde sacar el valor, y toda escritura responde 403 sin
233
+ que nada apunte a la causa. La cabecera es legible cross-origin sólo si el CORS la expone
234
+ (`create_app` lo hace cuando `AppFeatures(csrf=True)`), o sea que no se la regala a un origen
235
+ que el CORS no permite; y el valor no sirve sin la cookie de sesión que el navegador ya tiene.
215
236
  """
216
237
  transport.emit(response, tokens)
217
238
 
@@ -234,15 +255,17 @@ def emit_tokens(
234
255
  #
235
256
  # El valor es un HMAC del `sid`, así que es válido mientras viva la sesión: alinearlo con
236
257
  # el refresh es lo coherente, y es el mismo número que la cookie de refresh.
258
+ valor = derive_csrf_token(str(tokens.session_id), clave.get_secret_value())
237
259
  response.set_cookie(
238
260
  config.cookies.name_for("csrf"),
239
- derive_csrf_token(str(tokens.session_id), clave.get_secret_value()),
261
+ valor,
240
262
  max_age=int(config.tokens.refresh_ttl.total_seconds()),
241
263
  httponly=False,
242
264
  secure=config.cookies.secure,
243
265
  samesite=config.cookies.same_site,
244
266
  path=config.cookies.path,
245
267
  )
268
+ response.headers[CSRF_HEADER] = valor
246
269
 
247
270
 
248
271
  def session_response_body(
@@ -464,6 +487,44 @@ def build_identity_router(
464
487
  transport.clear(respuesta)
465
488
  return respuesta
466
489
 
490
+ @router.get("/csrf", response_model=CsrfTokenResponse)
491
+ async def csrf(
492
+ response: Response,
493
+ auth: "AuthContext[t.Any]" = Depends(provide_auth),
494
+ ) -> CsrfTokenResponse:
495
+ """
496
+ El valor anti-CSRF de la sesión por cookie en curso.
497
+
498
+ Para el cliente que **no puede leer la cookie de CSRF**: una SPA en otro origen que la
499
+ API (`document.cookie` sólo ve las cookies de su propio host). Lo pide al abrir la
500
+ página con una sesión ya existente y lo guarda **en memoria** — nunca en
501
+ `localStorage`—; el sign-in y el refresh lo entregan además en la cabecera
502
+ `X-CSRF-Token`, así que sólo hace falta acá cuando la página se recarga.
503
+
504
+ Es seguro darlo: sólo un origen que el CORS permita puede leer la respuesta, y el valor
505
+ no sirve sin la cookie de sesión que el navegador ya tiene. Es `HMAC(secreto, sid)`, el
506
+ mismo cálculo que `emit_tokens`.
507
+
508
+ **401 sin sesión por cookie.** Un cliente Bearer no hace CSRF —adjunta su token a
509
+ propósito— y pedirlo es un error de uso, no algo que haya que contestar.
510
+ """
511
+ from hexcore.darwin.application.container import get_identity_container
512
+ from hexcore.darwin.domain.exceptions import UnauthenticatedError
513
+ from hexcore.darwin.infrastructure.hashing import derive_csrf_token
514
+
515
+ config = get_identity_container().config
516
+ session_id = getattr(auth.actor, "session_id", None)
517
+ if auth.transport != "cookie" or session_id is None or config.secret_key is None:
518
+ raise UnauthenticatedError(
519
+ "El token anti-CSRF sólo existe para una sesión por cookie: un cliente Bearer "
520
+ "no hace CSRF."
521
+ )
522
+
523
+ response.headers["Cache-Control"] = "no-store"
524
+ return CsrfTokenResponse(
525
+ csrf_token=derive_csrf_token(str(session_id), config.secret_key.get_secret_value())
526
+ )
527
+
467
528
  @router.get("/me", response_model=MeResponse)
468
529
  async def me(auth: "AuthContext[t.Any]" = Depends(provide_auth)) -> MeResponse:
469
530
  """
@@ -245,6 +245,18 @@ class BeanieSessionRepository(_BaseBeanieRepository, AbstractSessionRepository):
245
245
  {"$set": {"revoked_at": at, "updated_at": datetime.now(UTC)}}
246
246
  )
247
247
 
248
+ async def list_family_session_ids(
249
+ self, family_id: UUID, *, limit: int = 100
250
+ ) -> list[UUID]:
251
+ """Los `id` de la familia, las más recientes primero. Ver el puerto."""
252
+ docs = (
253
+ await self._doc.find(self._doc.family_id == family_id)
254
+ .sort("-created_at")
255
+ .limit(limit)
256
+ .to_list()
257
+ )
258
+ return [doc.entity_id for doc in docs]
259
+
248
260
  async def revoke_family(
249
261
  self, family_id: UUID, *, at: datetime, reason: str
250
262
  ) -> int:
@@ -221,6 +221,19 @@ class SqlAlchemySessionRepository(_BaseIdentityRepository, AbstractSessionReposi
221
221
  )
222
222
  await session.commit()
223
223
 
224
+ async def list_family_session_ids(
225
+ self, family_id: UUID, *, limit: int = 100
226
+ ) -> list[UUID]:
227
+ """Los `id` de la familia, las más recientes primero. Ver el puerto."""
228
+ async with self._session_scope() as session:
229
+ resultado = await session.execute(
230
+ select(self._model.id)
231
+ .where(self._model.family_id == family_id)
232
+ .order_by(self._model.created_at.desc())
233
+ .limit(limit)
234
+ )
235
+ return list(resultado.scalars().all())
236
+
224
237
  async def revoke_family(
225
238
  self, family_id: UUID, *, at: datetime, reason: str
226
239
  ) -> int:
@@ -259,6 +259,15 @@ class FakeSessionRepository(AbstractSessionRepository):
259
259
  )
260
260
  self.reasons[session_id] = reason
261
261
 
262
+ async def list_family_session_ids(
263
+ self, family_id: UUID, *, limit: int = 100
264
+ ) -> list[UUID]:
265
+ """Los `id` de la familia, las más recientes primero. Ver el puerto."""
266
+ with self._lock:
267
+ familia = [s for s in self._por_id.values() if s.family_id == family_id]
268
+ familia.sort(key=lambda s: s.created_at, reverse=True)
269
+ return [s.id for s in familia[:limit]]
270
+
262
271
  async def revoke_family(
263
272
  self, family_id: UUID, *, at: datetime, reason: str
264
273
  ) -> int:
@@ -37,6 +37,10 @@ _EXPORTS: dict[str, tuple[str, str]] = {
37
37
  "RequestIDMiddleware",
38
38
  ),
39
39
  "TimingMiddleware": ("hexcore.infrastructure.api.middlewares", "TimingMiddleware"),
40
+ "PredicateCORSMiddleware": (
41
+ "hexcore.infrastructure.api.middlewares",
42
+ "PredicateCORSMiddleware",
43
+ ),
40
44
  "RequestIDLogFilter": (
41
45
  "hexcore.infrastructure.api.middlewares",
42
46
  "RequestIDLogFilter",
@@ -9,7 +9,7 @@
9
9
  #
10
10
  # Existe porque la fachada resuelve sus exports con `__getattr__` y declara
11
11
  # `__all__ = sorted(_EXPORTS)`: las dos son expresiones de runtime, así que sin este stub
12
- # los 48 símbolos de `hexcore.fastapi` tipan `Any`. El runtime no cambia — Python usa
12
+ # los 49 símbolos de `hexcore.fastapi` tipan `Any`. El runtime no cambia — Python usa
13
13
  # el `.py` y el checker usa el `.pyi`, así que la carga perezosa se mantiene.
14
14
 
15
15
 
@@ -42,6 +42,7 @@ from hexcore.infrastructure.api.lifespan import ProcrastinateStep as Procrastina
42
42
  from hexcore.infrastructure.api.lifespan import SqlEngineStep as SqlEngineStep
43
43
  from hexcore.infrastructure.api.lifespan import StartupStep as StartupStep
44
44
  from hexcore.infrastructure.api.lifespan import build_lifespan as build_lifespan
45
+ from hexcore.infrastructure.api.middlewares import PredicateCORSMiddleware as PredicateCORSMiddleware
45
46
  from hexcore.infrastructure.api.middlewares import RequestIDLogFilter as RequestIDLogFilter
46
47
  from hexcore.infrastructure.api.middlewares import RequestIDMiddleware as RequestIDMiddleware
47
48
  from hexcore.infrastructure.api.middlewares import TimingMiddleware as TimingMiddleware
@@ -74,6 +75,7 @@ __all__ = [
74
75
  "EventBusStep",
75
76
  "HealthReport",
76
77
  "HealthRoutes",
78
+ "PredicateCORSMiddleware",
77
79
  "Probe",
78
80
  "ProcrastinateStep",
79
81
  "RequestIDLogFilter",
@@ -16,7 +16,7 @@ from pydantic import BaseModel
16
16
 
17
17
  from .exception_handlers import HeadersFactory, PayloadFactory, register_exception_handlers
18
18
  from .health import Probe, ResponseFactory, register_health_routes
19
- from .middlewares import RequestIDMiddleware, TimingMiddleware
19
+ from .middlewares import PredicateCORSMiddleware, RequestIDMiddleware, TimingMiddleware
20
20
  from .routing import MountableRouter, mount_routers
21
21
 
22
22
  __all__ = ["AppFeatures", "HealthRoutes", "create_app"]
@@ -139,14 +139,6 @@ def create_app(
139
139
  # Los middlewares de Starlette se ejecutan en orden inverso al de registro, así que
140
140
  # el request-id se añade último para que envuelva a todo lo demás y su ContextVar
141
141
  # esté disponible en el resto de la pila.
142
- if resolved_features.cors:
143
- app.add_middleware(
144
- CORSMiddleware,
145
- allow_origins=list(getattr(config, "allow_origins", ["*"])),
146
- allow_credentials=bool(getattr(config, "allow_credentials", True)),
147
- allow_methods=list(getattr(config, "allow_methods", ["*"])),
148
- allow_headers=list(getattr(config, "allow_headers", ["*"])),
149
- )
150
142
  if resolved_features.timing:
151
143
  app.add_middleware(TimingMiddleware)
152
144
 
@@ -164,6 +156,39 @@ def create_app(
164
156
 
165
157
  app.add_middleware(AuthContextMiddleware)
166
158
 
159
+ # El CORS va **después** de los de Darwin —o sea, por fuera de ellos— y antes del
160
+ # request-id. Un middleware sólo le agrega cabeceras a lo que responde por dentro de él:
161
+ # con el CORS más adentro que el CSRF, el 403 de un `POST` cross-origin sin token salía
162
+ # sin `Access-Control-Allow-Origin`, y el navegador lo convertía en un error de red opaco
163
+ # en vez de dejarle leer a la app *por qué* falló (y reintentar con un token nuevo). Y
164
+ # el preflight se contesta acá, antes de gastar la resolución de la credencial.
165
+ if resolved_features.cors:
166
+ expose_headers = list(getattr(config, "cors_expose_headers", []))
167
+ if resolved_features.csrf:
168
+ # Darwin entrega el token anti-CSRF también en esta cabecera (para la SPA en otro
169
+ # origen, que no puede leer la cookie): sin exponerla, el JS no la ve.
170
+ from hexcore.darwin.infrastructure.api.middlewares import CSRF_HEADER
171
+
172
+ if CSRF_HEADER.lower() not in (h.lower() for h in expose_headers):
173
+ expose_headers.append(CSRF_HEADER)
174
+ cors_kwargs: dict[str, t.Any] = {
175
+ "allow_origins": list(getattr(config, "allow_origins", ["*"])),
176
+ "allow_credentials": bool(getattr(config, "allow_credentials", True)),
177
+ "allow_methods": list(getattr(config, "allow_methods", ["*"])),
178
+ "allow_headers": list(getattr(config, "allow_headers", ["*"])),
179
+ "expose_headers": expose_headers,
180
+ "max_age": int(getattr(config, "cors_max_age", 600)),
181
+ }
182
+ origin_predicate = getattr(config, "cors_origin_predicate", None)
183
+ if origin_predicate is None:
184
+ # Sin predicado, la clase es la de Starlette tal cual: nada cambia para quien no
185
+ # lo pide.
186
+ app.add_middleware(CORSMiddleware, **cors_kwargs)
187
+ else:
188
+ app.add_middleware(
189
+ PredicateCORSMiddleware, origin_predicate=origin_predicate, **cors_kwargs
190
+ )
191
+
167
192
  if resolved_features.request_id:
168
193
  app.add_middleware(RequestIDMiddleware)
169
194
 
@@ -15,12 +15,16 @@ import uuid
15
15
  import warnings
16
16
  from contextvars import ContextVar
17
17
 
18
+ from starlette.datastructures import Headers, MutableHeaders
18
19
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
20
+ from starlette.middleware.cors import CORSMiddleware
19
21
  from starlette.requests import Request
20
22
  from starlette.responses import Response
23
+ from starlette.types import Message, Receive, Scope, Send
21
24
 
22
25
  __all__ = [
23
26
  "REQUEST_ID",
27
+ "PredicateCORSMiddleware",
24
28
  "RequestIDMiddleware",
25
29
  "TimingMiddleware",
26
30
  "RequestIDLogFilter",
@@ -102,6 +106,66 @@ class TimingMiddleware(BaseHTTPMiddleware):
102
106
  return response
103
107
 
104
108
 
109
+ cors_logger = logging.getLogger("hexcore.api.cors")
110
+
111
+
112
+ class PredicateCORSMiddleware(CORSMiddleware):
113
+ """
114
+ `CORSMiddleware` que además acepta los orígenes que un predicado apruebe.
115
+
116
+ `allow_origins` es una lista fija, y un subdominio por tenant creado en caliente no se puede
117
+ enumerar de antemano: sin esto hay que redesplegar por cada alta, o abrir `"*"` — que con
118
+ credenciales no es válido. El predicado se consulta **sólo** cuando el origen no está ya en
119
+ la lista, así que declarar los dos no duplica trabajo.
120
+
121
+ Dos decisiones que no son obvias:
122
+
123
+ * **Falla cerrado.** Un predicado que lanza se trata como «no confiable» y se loguea en
124
+ `hexcore.api.cors`: un chequeo de confianza que explota nunca decide «confío» por default.
125
+ El log importa porque el síntoma —un origen legítimo sin CORS— no apunta a ninguna causa.
126
+ * **`Vary: Origin` siempre.** Con orígenes dinámicos la respuesta depende del `Origin`
127
+ también cuando se rechaza; sin `Vary`, una caché compartida podría servirle a un origen
128
+ aceptado la respuesta que guardó sin `Access-Control-Allow-Origin`. Starlette sólo lo
129
+ agrega cuando el origen es aceptado.
130
+ """
131
+
132
+ def __init__(
133
+ self,
134
+ app: t.Any,
135
+ *,
136
+ origin_predicate: t.Callable[[str], bool],
137
+ **kwargs: t.Any,
138
+ ) -> None:
139
+ super().__init__(app, **kwargs)
140
+ self._origin_predicate = origin_predicate
141
+
142
+ def is_allowed_origin(self, origin: str) -> bool:
143
+ if super().is_allowed_origin(origin):
144
+ return True
145
+ try:
146
+ return bool(self._origin_predicate(origin))
147
+ except Exception:
148
+ cors_logger.exception(
149
+ "cors_origin_predicate lanzó evaluando el origen %r; se lo trata como no "
150
+ "confiable.",
151
+ origin,
152
+ )
153
+ return False
154
+
155
+ async def simple_response(
156
+ self, scope: Scope, receive: Receive, send: Send, request_headers: Headers
157
+ ) -> None:
158
+ async def con_vary(message: Message) -> None:
159
+ if message["type"] == "http.response.start":
160
+ message.setdefault("headers", [])
161
+ headers = MutableHeaders(scope=message)
162
+ if "origin" not in headers.get("vary", "").lower():
163
+ headers.add_vary_header("Origin")
164
+ await send(message)
165
+
166
+ await super().simple_response(scope, receive, con_vary, request_headers)
167
+
168
+
105
169
  class RequestIDLogFilter(logging.Filter):
106
170
  """
107
171
  Inyecta el request-id en cada `LogRecord` como ``record.request_id``.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hexcore
3
- Version: 11.2.0
3
+ Version: 11.4.0
4
4
  Summary: Reusable core for Python projects with hexagonal architecture, DDD, CQRS and background tasks. Ships the abstractions and the infrastructure: SQL session layer, FastAPI factories, worker runner, dynamic cron and identity.
5
5
  Author-email: "David Latosefki (Indroic)" <indroic@outlook.com>
6
6
  License-Expression: MIT
@@ -290,6 +290,7 @@ hexcore/testing/repositories.py
290
290
  tests/test_alembic_metadata_completeness.py
291
291
  tests/test_anclas_del_repo.py
292
292
  tests/test_api_app_and_lifespan.py
293
+ tests/test_api_cors_dinamico.py
293
294
  tests/test_api_cqrs_providers.py
294
295
  tests/test_api_exception_handlers.py
295
296
  tests/test_api_health.py