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.
- {hexcore-11.2.0/hexcore.egg-info → hexcore-11.4.0}/PKG-INFO +1 -1
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/config.py +29 -3
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/services.py +14 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/exceptions.py +23 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/ports.py +20 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/dependencies.py +5 -1
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/middlewares.py +75 -7
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/routers.py +62 -1
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/repositories.py +12 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/repositories.py +13 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/fakes.py +9 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/fastapi.py +4 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/fastapi.pyi +3 -1
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/app.py +34 -9
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/middlewares.py +64 -0
- {hexcore-11.2.0 → hexcore-11.4.0/hexcore.egg-info}/PKG-INFO +1 -1
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/SOURCES.txt +1 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/pyproject.toml +1 -1
- hexcore-11.4.0/tests/test_api_cors_dinamico.py +250 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_http.py +354 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/LICENSE +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/MANIFEST.in +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/README.md +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/__main__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/_deprecation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/adapters.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/factory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/in_memory_buses.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/pipeline.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/registry.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/cqrs/scheduler.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/cursor.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/errors.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/dtos/query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/factory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/projector.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/relay.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/eventsourcing/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/use_cases/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/use_cases/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/application/use_cases/query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/capabilities.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/cqrs.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/cqrs.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/__init__.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/authorization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/authz_middleware.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/container.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/hooks.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/application/plugins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/authorization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/context.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/entities.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/events.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/permissions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/plugins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/domain/value_objects.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/api/authorization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/clock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/hashing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/keys.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/lifespan.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/documents.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/beanie/schema.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/selection.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/registry.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/schema.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/revocation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/secretbox.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/tokens.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/transports.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/cache.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/conditions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/pdp.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/pip.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/predicates.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/provider.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/drbac/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/impersonate/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/magic_link/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/magic_link/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/magic_link/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/http.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/providers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/oauth/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/organization/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/passkey/webauthn_adapter.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/cache.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/matcher.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/provider.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/resolver.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/rbac/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/storage.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/crypto.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/domain.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/beanie/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/orms/sqlalchemy/repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/plugins/two_factor/totp.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/fixtures.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/testing/helpers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/buses.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/commands.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/context.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/cron.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/decorators.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/dispatch.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/exceptions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/handlers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/middleware.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/queries.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/resolution.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/serializer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/cqrs/task_queues.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/events.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/aggregate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/exceptions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/projections.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/snapshots.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/eventsourcing/stored.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/exceptions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/services.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/domain/uow.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/eventsourcing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/eventsourcing.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/cqrs.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/eventsourcing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/exception_handlers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/health.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/lifespan.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/rate_limit.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/routing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/streaming.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/api/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/cache_backends/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/cache_backends/memory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cache/cache_backends/redis.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/cron_sql.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/middlewares.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/postgres_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/postgres_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/procrastinate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/pydantic_serializer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/rabbitmq.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/redis_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/cqrs/redis_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/beanie_documents.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/beanie_store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/memory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/redis_store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_models_mixins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/eventsourcing/sqlalchemy_store.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/_beanie_impl.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/_sqlalchemy_impl.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/base.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/decorators.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/implementations.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/beanie/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/beanie/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/id_types.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/session.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/orms/sqlalchemy/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/repositories/utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/task_queues/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/task_queues/celery_adapter.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/task_queues/procrastinate_adapter.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/decorators.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/helpers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/uow/scopes.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/consumer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/rabbitmq_worker.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/infrastructure/workers/runner.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/py.typed +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/sql.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/sql.pyi +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/__init__.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/fakes.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/fixtures.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/helpers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/testing/repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore/types.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/dependency_links.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/entry_points.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/requires.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/hexcore.egg-info/top_level.txt +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/setup.cfg +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_alembic_metadata_completeness.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_anclas_del_repo.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_app_and_lifespan.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_cqrs_providers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_exception_handlers.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_health.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_middlewares_and_routing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_rate_limit.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_api_streaming.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_basic.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_beanie_query_utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_celery_event_loop.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_config_cors_security.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_config_loading.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_consumer_optional_event_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs_envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs_factory.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cqrs_middlewares.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_cron_sql.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_10_4_hardening.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_authorization_engine.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_authorization_http.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_backend_neutrality.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_beanie.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_cli.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_container.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_context.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_custom_plugin.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_conditions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_pdp.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_router.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_drbac_service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_envelope.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_facade_and_events.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_flows.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_hashing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_impersonate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_magic_link.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_models.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_oauth.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_openapi.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_organization.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_passkey.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_permissions.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_plugin_decoupling.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_plugins.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_principals.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_production_defaults.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_rbac.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_rbac_service.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_repositories.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_revocation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_schema_registration.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_storage_selection.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_tokens.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_two_factor.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_username.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_darwin_value_objects.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_deprecations.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_documentation_examples.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_domain_base_contract.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_domain_service_query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_dotted_resolution.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_event_dispatch_mro.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_aggregate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_beanie.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_config.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_ports.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_projections.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_redis.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_relay.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_repository.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_snapshot_checkpoint.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_sql_schema.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_store_contract.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_eventsourcing_uow_integration.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_facades.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_handler_registry.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_infrastructure_query_path.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_lock_error_policy.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_optional_dependencies.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_packaging.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_postgres_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_postgres_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_query_cursor_and_endpoint.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_query_field_validation.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_rabbitmq_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_redis_bus.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_redis_lock.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_repositories_utils.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_scheduler.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_scheduler_catchup.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_smart_routing.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_sql_naming_convention.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_sql_session_layer.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_task_queues_adapters.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_testing_kit.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_testing_utilities.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_typing_gate.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_uow_session_regression.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_use_cases_query.py +0 -0
- {hexcore-11.2.0 → hexcore-11.4.0}/tests/test_worker_bus_integration.py +0 -0
- {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.
|
|
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.
|
|
198
|
-
|
|
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
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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:
|
{hexcore-11.2.0 → hexcore-11.4.0}/hexcore/darwin/infrastructure/orms/sqlalchemy/repositories.py
RENAMED
|
@@ -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
|
|
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.
|
|
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
|