matrx-orm 3.1.53__tar.gz → 3.1.55__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.
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/PKG-INFO +1 -1
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/catalog.py +31 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/FEATURE.md +22 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/__init__.py +40 -1
- matrx_orm-3.1.55/matrx_orm/secrets_battery/attachments.py +419 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/crypto.py +17 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/items.py +14 -4
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/model.py +41 -1
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/pyproject.toml +1 -1
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_secrets_battery.py +20 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/.gitignore +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/AGENTS.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/BASE_CLASS_METHODS.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/CLAUDE.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/MODEL_API.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/database/orm/extended/managers/ai_model_base.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/docs/migrations.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/async_postgresql.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/base_adapter.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/postgresql.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/supabase_adapter.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/admin/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/admin/router.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/auth.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/config.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/handlers.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/protocol.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/server.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/associations.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/cache_debug.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/postgres_connection.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/postgrest.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/supabase_auth.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/supabase_config.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/supabase_manager.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/EXTENDED-TASK.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/FEATURE.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/TASKS.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/async_db_manager.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/base.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/config.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/connections.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/db_function.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/diagnostics.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/expressions.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/extended.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/fields.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/introspection.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/listen.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/loop_filters.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/model_dto.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/model_view.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/one_database.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/paginator.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/pool_maintenance.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/pydantic_bridge.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/registry.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/relations.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/resilience.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/rls_session.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/signals.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/tls.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/transaction.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/types.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/write_queue.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/entity.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/error_handling.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/exceptions.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/extended/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/extended/app_error_handler.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/identity_rule.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/middleware/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/middleware/base.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/cli.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/ddl.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/diff.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/executor.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/integration.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/loader.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/operations.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/state.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/table_filter.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/bulk_update_values.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/conflict.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/conflict_writes.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/create.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/db_functions.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/delete.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/dynamic_admin.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/dynamic_crud.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/graph_walk.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/latest_rows.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/queue_claim.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/read.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/relational_backfill.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/staging_load.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/update.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/platform_db.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/py.typed +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/db_objects.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/FEATURE.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/builder.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/executor.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/code_handler.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/codegen_writer.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/columns.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/common.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/diff_preview.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/generator.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/package_wiring.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/relationships.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/runner.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/schema.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/schema_manager.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/tables.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/views.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/org_service.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/service.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/coalesce.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/dag.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/errors.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/fallback.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/flush.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/lifecycle.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/managed.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/op.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/reads.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/session.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/telemetry.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/executor.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/queries.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/registry.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/types.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/utils.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/state.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/utils/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/utils/sql_utils.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/utils/type_converters.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/scripts/git-branches.sh +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/scripts/publish.sh +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/scripts/release.sh +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/conftest.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_admin_db_columns.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_admin_search_negation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_admin_search_sentinels.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_advisory_lock.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_agent_message.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_auth.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_config.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_handlers.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_protocol.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_array_agg_order_by.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_associations.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_bulk_update_by_pk.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_cache_debug.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_call_function.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_case_when.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_catalog.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_composite_pk_filter.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_composite_pk_write_paths.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_config.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_conflict_target.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_conflict_writes.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_connection_codecs.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_connection_poison_guard.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_connection_primitives.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_count_composite_distinct.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_db_function_field.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_ddl_generator.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_derived_table.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_dirty_tracking.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_dynamic_crud.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_exceptions.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_expression_primitives.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_fields.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_from_alias.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_fts.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_func_expression_args.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_graph_walk.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_having_annotate_alias.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_introspect_rls_policies.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_join_and_aggregate.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_latest_rows.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_listen_notify.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_managed_write_guard.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_matrx_entity.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_migration_diff_types.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_migration_loader.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_model_instance.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_model_meta.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_optimistic_concurrency.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_order_by_expression.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_parameter_name_hydration.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_patch_jsonb_path.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_pool_loop_recreation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_pool_maintenance.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_postgrest_filters.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_pydantic_bridge.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_builder.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_error_detail.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_executor_sql.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_timeout.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_queue_claim.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_registry.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_registry_multi_database.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_registry_multi_schema.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_relational_backfill.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_relations.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_resilience.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_schema_exists.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_session_advisory_lock.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_state_cache.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_subquery_filter_raw.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_subquery_in_filter.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_supabase_auth.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_supabase_config.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_tls.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_typed_join_projection.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_typed_predicates.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_update_case_expression.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_update_subquery.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_update_with_rebase.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_upsert_default_leak.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_upsert_with_conflict.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_values_jsonb_decode.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_watched_lifecycle.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_write_retry_semantics.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/MODULE_README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/conftest.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_bulk_ops.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_cache_integration.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_crud.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_foreign_keys.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_m2m.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_manager.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_migrations_live.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_query_execution.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_schema_diff.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/.env.example +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/generate.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/generated/.gitkeep +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/matrx_orm.yaml +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/test_schema_generation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/.env.example +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/README.md +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/client_example.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/client_example.ts +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/client_supabase_example.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/server.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/entity_tests.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_base_generation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_composite_pk_fk_generation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_composite_unique_generation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_dto_identity_generation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_entity_capability_generation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_generate_schema.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_junction_analysis.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_multi_schema_output.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_sql_expression_defaults.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_view_generation.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema_builder/test_focused_generation_plan.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema_builder/test_host_models_aggregator.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/__init__.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/conftest.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_cache_no_downgrade.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_capture_poison_proof.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_coalesce.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_dag.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_disk_spill_fallback.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_flush_individual_fallback.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_governed_write.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_managed.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_op_enqueue_site.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_reads.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_session.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_database_name_alias.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_db_functions.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_model_cls_refactor.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_no_blocking_asyncpg_tls.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_one_database.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_sql_param_casts.py +0 -0
- {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_sync_twins_guard.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: matrx-orm
|
|
3
|
-
Version: 3.1.
|
|
3
|
+
Version: 3.1.55
|
|
4
4
|
Summary: Async-first PostgreSQL ORM with bidirectional migrations, schema introspection, many-to-many relationships, and built-in state caching
|
|
5
5
|
Project-URL: Homepage, https://github.com/AI-Matrix-Engine/aidream-current
|
|
6
6
|
Project-URL: Repository, https://github.com/AI-Matrix-Engine/aidream-current
|
|
@@ -292,6 +292,37 @@ async def first_party_function_bodies(
|
|
|
292
292
|
return [{"name": str(row["name"]), "source": str(row["source"] or "")} for row in rows]
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
async def first_party_view_definitions(
|
|
296
|
+
database: str,
|
|
297
|
+
*,
|
|
298
|
+
exclude_schemas: list[str],
|
|
299
|
+
) -> list[dict[str, str]]:
|
|
300
|
+
"""Return live view names and definitions outside excluded schemas.
|
|
301
|
+
|
|
302
|
+
The view twin of :func:`first_party_function_bodies`. Together they are the
|
|
303
|
+
whole surface a "does any live SQL object still inline this rule?" drift
|
|
304
|
+
guard needs — a rule that is COPIED into a view drifts exactly as fast as
|
|
305
|
+
one copied into a function.
|
|
306
|
+
"""
|
|
307
|
+
rows = await _query(
|
|
308
|
+
database,
|
|
309
|
+
"""
|
|
310
|
+
SELECT n.nspname || '.' || c.relname AS name,
|
|
311
|
+
pg_get_viewdef(c.oid, true) AS definition
|
|
312
|
+
FROM pg_class c
|
|
313
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
314
|
+
WHERE c.relkind IN ('v', 'm')
|
|
315
|
+
AND n.nspname <> ALL($1::text[])
|
|
316
|
+
ORDER BY n.nspname, c.relname
|
|
317
|
+
""",
|
|
318
|
+
exclude_schemas,
|
|
319
|
+
)
|
|
320
|
+
return [
|
|
321
|
+
{"name": str(row["name"]), "definition": str(row["definition"] or "")}
|
|
322
|
+
for row in rows
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
|
|
295
326
|
async def relation_privileges(
|
|
296
327
|
database: str,
|
|
297
328
|
*,
|
|
@@ -26,6 +26,16 @@ charset-validated here — `provider_key`, `display_name`, `tags`, `status`,
|
|
|
26
26
|
alias (nullable). A DB trigger (`trg_user_secrets_owner_match`) rejects any field
|
|
27
27
|
row whose owner columns don't match its item's owner.
|
|
28
28
|
|
|
29
|
+
Items may also own N encrypted **file attachments**
|
|
30
|
+
(`users.credential_attachments`). An attachment has its own stable id,
|
|
31
|
+
human label/purpose, filename, media type, byte size, handling mode, version,
|
|
32
|
+
and audit lifecycle; ownership, grants, transfer, fork, and deletion always
|
|
33
|
+
come from the parent item. The bytes are encrypted directly with the same
|
|
34
|
+
Fernet singleton and `CREDENTIALS_ENCRYPTION_KEY` as fields—there is no object
|
|
35
|
+
store copy and no second crypto primitive. Credential files are capped at
|
|
36
|
+
25 MiB; this is for signing keys, certificates, recovery exports, and similar
|
|
37
|
+
artifacts, not general document storage.
|
|
38
|
+
|
|
29
39
|
**Handling doctrine** — every field has `handling` + `editable` + `inject_into_sandbox`:
|
|
30
40
|
|
|
31
41
|
| handling | human-facing read | trusted execution |
|
|
@@ -68,6 +78,14 @@ soft, atomic across item + fields + grants) · `add_credential_field` ·
|
|
|
68
78
|
`fork_credential_item` (independent copy) · `resolve_legacy_env_key` (compat
|
|
69
79
|
read that SCREAMS in logs until call sites migrate to stable references).
|
|
70
80
|
|
|
81
|
+
Attachment surface (`attachments.py`): add/list/get/update metadata/replace/
|
|
82
|
+
soft-delete; `reveal_credential_attachment` is the audited human download and
|
|
83
|
+
refuses sealed bytes; `resolve_credential_attachment` is the trusted in-process
|
|
84
|
+
path. Parent delete soft-deletes attachments atomically; fork decrypts and
|
|
85
|
+
re-encrypts independent child rows; transfer needs no child rewrite because
|
|
86
|
+
the parent is the sole ownership authority. Browser-facing list shapes never
|
|
87
|
+
contain ciphertext or plaintext.
|
|
88
|
+
|
|
71
89
|
**Grants: use the single-grant operations.** `add_item_grant` /
|
|
72
90
|
`update_item_grant` / `remove_item_grant` change ONE recipient and leave the
|
|
73
91
|
rest alone. `set_item_grants` REPLACES the whole set (delete + recreate) and
|
|
@@ -238,6 +256,10 @@ create, reveal, sealed denial, editable gate, rotate, item grants, soft delete).
|
|
|
238
256
|
|
|
239
257
|
## Change Log
|
|
240
258
|
|
|
259
|
+
- **2026-08-11** — Added first-class encrypted credential-file attachments with
|
|
260
|
+
parent-owned access/lifecycle, a 25 MiB cap, one-way sealing, audited human
|
|
261
|
+
download, trusted execution resolution, and fork/delete inheritance.
|
|
262
|
+
|
|
241
263
|
- 2026-07-26 — Closed the silent sandbox-injection gap: `SandboxInjectionWithoutEnvKeyError` at every field write path, a loud ERROR-and-skip in `for_sandbox` bulk resolution, and DB CHECK `user_secrets_sandbox_injection_needs_key` (+ column default `true`→`false`). Live at the time of the fix: 0 broken rows, but 61 keyless item fields were one toggle away from it.
|
|
242
264
|
- 2026-07-23 — `update_field_metadata` primitive (metadata-only, version-preserving; `users.bump_user_secret_value_version` verified value-conditional live — no trigger change needed).
|
|
243
265
|
- 2026-07-23 — Adversarial-review hardening: include_sealed-aware bulk env resolution (+ keyless-row exclusion), ItemManagedSecretError legacy-write guards + legacy audit rows, ItemGrantSpec per-grantee grants, reveal liveness filters, rotate liveness re-check + audits inside owning transactions.
|
|
@@ -24,11 +24,27 @@ Contract details: ``FEATURE.md`` beside this module.
|
|
|
24
24
|
|
|
25
25
|
from __future__ import annotations
|
|
26
26
|
|
|
27
|
+
from .attachments import (
|
|
28
|
+
MAX_VAULT_ATTACHMENT_BYTES,
|
|
29
|
+
CredentialAttachmentRow,
|
|
30
|
+
add_credential_attachment,
|
|
31
|
+
copy_item_attachments,
|
|
32
|
+
delete_credential_attachment,
|
|
33
|
+
get_credential_attachment,
|
|
34
|
+
list_credential_attachments,
|
|
35
|
+
replace_credential_attachment,
|
|
36
|
+
resolve_credential_attachment,
|
|
37
|
+
reveal_credential_attachment,
|
|
38
|
+
soft_delete_item_attachments,
|
|
39
|
+
update_credential_attachment_metadata,
|
|
40
|
+
)
|
|
27
41
|
from .crypto import (
|
|
28
42
|
configure_encryption,
|
|
43
|
+
decrypt_bytes,
|
|
29
44
|
decrypt_payload,
|
|
30
45
|
decrypt_value,
|
|
31
46
|
encrypt_payload,
|
|
47
|
+
encrypt_bytes,
|
|
32
48
|
encrypt_value,
|
|
33
49
|
fernet_for_key,
|
|
34
50
|
get_fernet,
|
|
@@ -75,7 +91,14 @@ from .items import (
|
|
|
75
91
|
update_credential_item,
|
|
76
92
|
update_field_metadata,
|
|
77
93
|
)
|
|
78
|
-
from .model import
|
|
94
|
+
from .model import (
|
|
95
|
+
PACKAGE_DB_NAME,
|
|
96
|
+
CredentialAttachment,
|
|
97
|
+
CredentialItem,
|
|
98
|
+
UserSecret,
|
|
99
|
+
UserSecretAudit,
|
|
100
|
+
UserSecretGrant,
|
|
101
|
+
)
|
|
79
102
|
from .org_service import (
|
|
80
103
|
ACCESS_MODES,
|
|
81
104
|
OrgSecretRow,
|
|
@@ -139,6 +162,20 @@ __all__ = [
|
|
|
139
162
|
"configure_encryption",
|
|
140
163
|
"fernet_for_key",
|
|
141
164
|
"multi_fernet_for_keys",
|
|
165
|
+
# attachment layer
|
|
166
|
+
"MAX_VAULT_ATTACHMENT_BYTES",
|
|
167
|
+
"CredentialAttachment",
|
|
168
|
+
"CredentialAttachmentRow",
|
|
169
|
+
"add_credential_attachment",
|
|
170
|
+
"copy_item_attachments",
|
|
171
|
+
"delete_credential_attachment",
|
|
172
|
+
"get_credential_attachment",
|
|
173
|
+
"list_credential_attachments",
|
|
174
|
+
"replace_credential_attachment",
|
|
175
|
+
"resolve_credential_attachment",
|
|
176
|
+
"reveal_credential_attachment",
|
|
177
|
+
"soft_delete_item_attachments",
|
|
178
|
+
"update_credential_attachment_metadata",
|
|
142
179
|
# item layer
|
|
143
180
|
"FIELD_KEY_RE",
|
|
144
181
|
"HANDLING_MODES",
|
|
@@ -182,6 +219,8 @@ __all__ = [
|
|
|
182
219
|
"get_fernet",
|
|
183
220
|
"encrypt_value",
|
|
184
221
|
"decrypt_value",
|
|
222
|
+
"encrypt_bytes",
|
|
223
|
+
"decrypt_bytes",
|
|
185
224
|
"encrypt_payload",
|
|
186
225
|
"decrypt_payload",
|
|
187
226
|
"mask_value",
|
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
"""Encrypted file attachments for credential items.
|
|
2
|
+
|
|
3
|
+
Attachments are child entities with their own identity and lifecycle, while
|
|
4
|
+
ownership and sharing remain exclusively on the parent credential item. File
|
|
5
|
+
bytes use the existing Vault Fernet primitive directly; no base64 wrapper,
|
|
6
|
+
object-store plaintext, or second encryption path is introduced.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import UTC, datetime
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .crypto import decrypt_bytes, encrypt_bytes
|
|
16
|
+
from .items import CredentialItemNotFoundError, HANDLING_MODES, SealedFieldError
|
|
17
|
+
from .model import CredentialAttachment, CredentialItem
|
|
18
|
+
from .org_service import record_secret_audit
|
|
19
|
+
from .service import SecretDecryptError
|
|
20
|
+
|
|
21
|
+
MAX_VAULT_ATTACHMENT_BYTES = 25 * 1024 * 1024
|
|
22
|
+
|
|
23
|
+
_ATTACHMENT_FIELDS = (
|
|
24
|
+
"id",
|
|
25
|
+
"credential_item_id",
|
|
26
|
+
"label",
|
|
27
|
+
"description",
|
|
28
|
+
"file_name",
|
|
29
|
+
"media_type",
|
|
30
|
+
"size_bytes",
|
|
31
|
+
"handling",
|
|
32
|
+
"value_version",
|
|
33
|
+
"last_used_at",
|
|
34
|
+
"created_at",
|
|
35
|
+
"updated_at",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class CredentialAttachmentRow:
|
|
41
|
+
"""Masked attachment metadata. Never contains ciphertext or plaintext."""
|
|
42
|
+
|
|
43
|
+
id: str
|
|
44
|
+
credential_item_id: str
|
|
45
|
+
label: str
|
|
46
|
+
description: str | None
|
|
47
|
+
file_name: str
|
|
48
|
+
media_type: str
|
|
49
|
+
size_bytes: int
|
|
50
|
+
handling: str
|
|
51
|
+
value_version: int
|
|
52
|
+
last_used_at: str | None
|
|
53
|
+
created_at: str
|
|
54
|
+
updated_at: str
|
|
55
|
+
|
|
56
|
+
def to_dict(self) -> dict[str, Any]:
|
|
57
|
+
return dict(self.__dict__)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _iso(value: Any) -> str:
|
|
61
|
+
if isinstance(value, datetime):
|
|
62
|
+
return value.isoformat()
|
|
63
|
+
return str(value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _row(raw: dict[str, Any]) -> CredentialAttachmentRow:
|
|
67
|
+
return CredentialAttachmentRow(
|
|
68
|
+
id=str(raw["id"]),
|
|
69
|
+
credential_item_id=str(raw["credential_item_id"]),
|
|
70
|
+
label=str(raw["label"]),
|
|
71
|
+
description=raw.get("description"),
|
|
72
|
+
file_name=str(raw["file_name"]),
|
|
73
|
+
media_type=str(raw.get("media_type") or "application/octet-stream"),
|
|
74
|
+
size_bytes=int(raw["size_bytes"]),
|
|
75
|
+
handling=str(raw.get("handling") or "revealable"),
|
|
76
|
+
value_version=int(raw.get("value_version") or 1),
|
|
77
|
+
last_used_at=_iso(raw["last_used_at"]) if raw.get("last_used_at") else None,
|
|
78
|
+
created_at=_iso(raw["created_at"]),
|
|
79
|
+
updated_at=_iso(raw["updated_at"]),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def _require_item(item_id: str) -> None:
|
|
84
|
+
if not await CredentialItem.filter(id=item_id, deleted_at__isnull=True).exists():
|
|
85
|
+
raise CredentialItemNotFoundError(f"credential item {item_id} not found")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
async def _require_raw(attachment_id: str) -> dict[str, Any]:
|
|
89
|
+
rows = await CredentialAttachment.filter(id=attachment_id, deleted_at__isnull=True).values(
|
|
90
|
+
*_ATTACHMENT_FIELDS, "value_encrypted"
|
|
91
|
+
)
|
|
92
|
+
if not rows:
|
|
93
|
+
raise LookupError(f"credential attachment {attachment_id} not found")
|
|
94
|
+
return rows[0]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _validate_text(value: str, *, name: str, maximum: int) -> str:
|
|
98
|
+
if not isinstance(value, str) or not value.strip():
|
|
99
|
+
raise ValueError(f"{name} is required")
|
|
100
|
+
clean = value.strip()
|
|
101
|
+
if len(clean) > maximum:
|
|
102
|
+
raise ValueError(f"{name} must be at most {maximum} characters")
|
|
103
|
+
return clean
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _validate_content(content: bytes) -> None:
|
|
107
|
+
if not isinstance(content, bytes):
|
|
108
|
+
raise TypeError("attachment content must be bytes")
|
|
109
|
+
if not content:
|
|
110
|
+
raise ValueError("attachment file is empty")
|
|
111
|
+
if len(content) > MAX_VAULT_ATTACHMENT_BYTES:
|
|
112
|
+
raise ValueError("attachment exceeds the 25 MiB credential-file limit")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _validate_handling(handling: str) -> None:
|
|
116
|
+
if handling not in HANDLING_MODES:
|
|
117
|
+
raise ValueError(f"handling must be one of {HANDLING_MODES}; got {handling!r}")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _media_type(value: str | None) -> str:
|
|
121
|
+
if not value:
|
|
122
|
+
return "application/octet-stream"
|
|
123
|
+
return _validate_text(value.split(";", 1)[0], name="media_type", maximum=255)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def list_credential_attachments(
|
|
127
|
+
item_ids: list[str],
|
|
128
|
+
) -> dict[str, list[CredentialAttachmentRow]]:
|
|
129
|
+
"""Return masked live attachments grouped by parent item id."""
|
|
130
|
+
if not item_ids:
|
|
131
|
+
return {}
|
|
132
|
+
rows = (
|
|
133
|
+
await CredentialAttachment.filter(credential_item_id__in=item_ids, deleted_at__isnull=True)
|
|
134
|
+
.order_by("created_at", "id")
|
|
135
|
+
.values(*_ATTACHMENT_FIELDS)
|
|
136
|
+
)
|
|
137
|
+
grouped: dict[str, list[CredentialAttachmentRow]] = {}
|
|
138
|
+
for raw in rows:
|
|
139
|
+
grouped.setdefault(str(raw["credential_item_id"]), []).append(_row(raw))
|
|
140
|
+
return grouped
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def get_credential_attachment(attachment_id: str) -> CredentialAttachmentRow | None:
|
|
144
|
+
rows = await CredentialAttachment.filter(id=attachment_id, deleted_at__isnull=True).values(
|
|
145
|
+
*_ATTACHMENT_FIELDS
|
|
146
|
+
)
|
|
147
|
+
return _row(rows[0]) if rows else None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def add_credential_attachment(
|
|
151
|
+
item_id: str,
|
|
152
|
+
*,
|
|
153
|
+
label: str,
|
|
154
|
+
description: str | None,
|
|
155
|
+
file_name: str,
|
|
156
|
+
media_type: str | None,
|
|
157
|
+
content: bytes,
|
|
158
|
+
handling: str = "revealable",
|
|
159
|
+
actor_id: str | None = None,
|
|
160
|
+
) -> CredentialAttachmentRow:
|
|
161
|
+
"""Encrypt and attach one file to an existing credential item."""
|
|
162
|
+
await _require_item(item_id)
|
|
163
|
+
_validate_content(content)
|
|
164
|
+
_validate_handling(handling)
|
|
165
|
+
clean_label = _validate_text(label, name="label", maximum=200)
|
|
166
|
+
clean_name = _validate_text(file_name, name="file_name", maximum=512)
|
|
167
|
+
clean_description = (
|
|
168
|
+
_validate_text(description, name="description", maximum=2000)
|
|
169
|
+
if description and description.strip()
|
|
170
|
+
else None
|
|
171
|
+
)
|
|
172
|
+
instance = await CredentialAttachment.create(
|
|
173
|
+
credential_item_id=item_id,
|
|
174
|
+
label=clean_label,
|
|
175
|
+
description=clean_description,
|
|
176
|
+
file_name=clean_name,
|
|
177
|
+
media_type=_media_type(media_type),
|
|
178
|
+
size_bytes=len(content),
|
|
179
|
+
value_encrypted=encrypt_bytes(content),
|
|
180
|
+
handling=handling,
|
|
181
|
+
created_by=actor_id,
|
|
182
|
+
updated_by=actor_id,
|
|
183
|
+
)
|
|
184
|
+
await record_secret_audit(
|
|
185
|
+
action="attachment_added",
|
|
186
|
+
actor_id=actor_id,
|
|
187
|
+
metadata={
|
|
188
|
+
"credential_item_id": item_id,
|
|
189
|
+
"attachment_id": str(instance.id),
|
|
190
|
+
"file_name": clean_name,
|
|
191
|
+
"size_bytes": len(content),
|
|
192
|
+
"handling": handling,
|
|
193
|
+
},
|
|
194
|
+
)
|
|
195
|
+
row = await get_credential_attachment(str(instance.id))
|
|
196
|
+
if row is None:
|
|
197
|
+
raise RuntimeError(f"credential attachment {instance.id} vanished after create")
|
|
198
|
+
return row
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
_UNSET: Any = object()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
async def update_credential_attachment_metadata(
|
|
205
|
+
attachment_id: str,
|
|
206
|
+
*,
|
|
207
|
+
label: str | None = None,
|
|
208
|
+
description: Any = _UNSET,
|
|
209
|
+
file_name: str | None = None,
|
|
210
|
+
handling: str | None = None,
|
|
211
|
+
actor_id: str | None = None,
|
|
212
|
+
) -> CredentialAttachmentRow:
|
|
213
|
+
"""Patch safe metadata without touching bytes or ``value_version``."""
|
|
214
|
+
raw = await _require_raw(attachment_id)
|
|
215
|
+
if handling is not None:
|
|
216
|
+
_validate_handling(handling)
|
|
217
|
+
if str(raw["handling"]) == "sealed" and handling != "sealed":
|
|
218
|
+
raise SealedFieldError(
|
|
219
|
+
f"attachment {raw['file_name']!r} is sealed — sealing is a one-way door"
|
|
220
|
+
)
|
|
221
|
+
updates: dict[str, Any] = {"updated_by": actor_id, "updated_at": datetime.now(UTC)}
|
|
222
|
+
if label is not None:
|
|
223
|
+
updates["label"] = _validate_text(label, name="label", maximum=200)
|
|
224
|
+
if description is not _UNSET:
|
|
225
|
+
updates["description"] = (
|
|
226
|
+
_validate_text(description, name="description", maximum=2000)
|
|
227
|
+
if isinstance(description, str) and description.strip()
|
|
228
|
+
else None
|
|
229
|
+
)
|
|
230
|
+
if file_name is not None:
|
|
231
|
+
updates["file_name"] = _validate_text(file_name, name="file_name", maximum=512)
|
|
232
|
+
if handling is not None:
|
|
233
|
+
updates["handling"] = handling
|
|
234
|
+
changed = sorted(set(updates) - {"updated_by", "updated_at"})
|
|
235
|
+
if not changed:
|
|
236
|
+
raise ValueError("no attachment metadata changes supplied")
|
|
237
|
+
await CredentialAttachment.update_where({"id": attachment_id}, **updates)
|
|
238
|
+
await record_secret_audit(
|
|
239
|
+
action="attachment_updated",
|
|
240
|
+
actor_id=actor_id,
|
|
241
|
+
metadata={
|
|
242
|
+
"credential_item_id": str(raw["credential_item_id"]),
|
|
243
|
+
"attachment_id": attachment_id,
|
|
244
|
+
"changed": changed,
|
|
245
|
+
},
|
|
246
|
+
)
|
|
247
|
+
row = await get_credential_attachment(attachment_id)
|
|
248
|
+
if row is None:
|
|
249
|
+
raise LookupError(f"credential attachment {attachment_id} not found")
|
|
250
|
+
return row
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
async def replace_credential_attachment(
|
|
254
|
+
attachment_id: str,
|
|
255
|
+
*,
|
|
256
|
+
file_name: str,
|
|
257
|
+
media_type: str | None,
|
|
258
|
+
content: bytes,
|
|
259
|
+
actor_id: str | None = None,
|
|
260
|
+
) -> CredentialAttachmentRow:
|
|
261
|
+
"""Atomically replace the encrypted bytes and file metadata."""
|
|
262
|
+
raw = await _require_raw(attachment_id)
|
|
263
|
+
_validate_content(content)
|
|
264
|
+
clean_name = _validate_text(file_name, name="file_name", maximum=512)
|
|
265
|
+
await CredentialAttachment.update_where(
|
|
266
|
+
{"id": attachment_id, "deleted_at__isnull": True},
|
|
267
|
+
file_name=clean_name,
|
|
268
|
+
media_type=_media_type(media_type),
|
|
269
|
+
size_bytes=len(content),
|
|
270
|
+
value_encrypted=encrypt_bytes(content),
|
|
271
|
+
updated_by=actor_id,
|
|
272
|
+
updated_at=datetime.now(UTC),
|
|
273
|
+
)
|
|
274
|
+
await record_secret_audit(
|
|
275
|
+
action="attachment_replaced",
|
|
276
|
+
actor_id=actor_id,
|
|
277
|
+
metadata={
|
|
278
|
+
"credential_item_id": str(raw["credential_item_id"]),
|
|
279
|
+
"attachment_id": attachment_id,
|
|
280
|
+
"file_name": clean_name,
|
|
281
|
+
"size_bytes": len(content),
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
row = await get_credential_attachment(attachment_id)
|
|
285
|
+
if row is None:
|
|
286
|
+
raise LookupError(f"credential attachment {attachment_id} not found")
|
|
287
|
+
return row
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
async def delete_credential_attachment(attachment_id: str, *, actor_id: str | None = None) -> bool:
|
|
291
|
+
raw = await _require_raw(attachment_id)
|
|
292
|
+
now = datetime.now(UTC)
|
|
293
|
+
result = await CredentialAttachment.update_where(
|
|
294
|
+
{"id": attachment_id, "deleted_at__isnull": True},
|
|
295
|
+
deleted_at=now,
|
|
296
|
+
updated_by=actor_id,
|
|
297
|
+
updated_at=now,
|
|
298
|
+
)
|
|
299
|
+
if result.rows_affected:
|
|
300
|
+
await record_secret_audit(
|
|
301
|
+
action="attachment_deleted",
|
|
302
|
+
actor_id=actor_id,
|
|
303
|
+
metadata={
|
|
304
|
+
"credential_item_id": str(raw["credential_item_id"]),
|
|
305
|
+
"attachment_id": attachment_id,
|
|
306
|
+
"file_name": str(raw["file_name"]),
|
|
307
|
+
},
|
|
308
|
+
)
|
|
309
|
+
return result.rows_affected > 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
async def reveal_credential_attachment(
|
|
313
|
+
attachment_id: str, *, actor_id: str | None = None
|
|
314
|
+
) -> tuple[CredentialAttachmentRow, bytes]:
|
|
315
|
+
"""Human download path. Sealed bytes are structurally refused."""
|
|
316
|
+
raw = await _require_raw(attachment_id)
|
|
317
|
+
if str(raw["handling"]) == "sealed":
|
|
318
|
+
await record_secret_audit(
|
|
319
|
+
action="attachment_download_denied",
|
|
320
|
+
actor_id=actor_id,
|
|
321
|
+
metadata={
|
|
322
|
+
"credential_item_id": str(raw["credential_item_id"]),
|
|
323
|
+
"attachment_id": attachment_id,
|
|
324
|
+
"reason": "sealed",
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
raise SealedFieldError(
|
|
328
|
+
f"attachment {raw['file_name']!r} is sealed — no human-facing API may return it"
|
|
329
|
+
)
|
|
330
|
+
try:
|
|
331
|
+
content = decrypt_bytes(raw["value_encrypted"])
|
|
332
|
+
except Exception as exc:
|
|
333
|
+
raise SecretDecryptError(
|
|
334
|
+
f"credential attachment {attachment_id} does not decrypt with the configured key"
|
|
335
|
+
) from exc
|
|
336
|
+
await CredentialAttachment.update_where({"id": attachment_id}, last_used_at=datetime.now(UTC))
|
|
337
|
+
await record_secret_audit(
|
|
338
|
+
action="attachment_downloaded",
|
|
339
|
+
actor_id=actor_id,
|
|
340
|
+
metadata={
|
|
341
|
+
"credential_item_id": str(raw["credential_item_id"]),
|
|
342
|
+
"attachment_id": attachment_id,
|
|
343
|
+
"file_name": str(raw["file_name"]),
|
|
344
|
+
"size_bytes": int(raw["size_bytes"]),
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
return _row(raw), content
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
async def resolve_credential_attachment(
|
|
351
|
+
attachment_id: str,
|
|
352
|
+
) -> tuple[CredentialAttachmentRow, bytes]:
|
|
353
|
+
"""Trusted execution path, including sealed attachments."""
|
|
354
|
+
raw = await _require_raw(attachment_id)
|
|
355
|
+
try:
|
|
356
|
+
content = decrypt_bytes(raw["value_encrypted"])
|
|
357
|
+
except Exception as exc:
|
|
358
|
+
raise SecretDecryptError(
|
|
359
|
+
f"credential attachment {attachment_id} does not decrypt with the configured key"
|
|
360
|
+
) from exc
|
|
361
|
+
await CredentialAttachment.update_where({"id": attachment_id}, last_used_at=datetime.now(UTC))
|
|
362
|
+
return _row(raw), content
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
async def soft_delete_item_attachments(item_id: str, *, actor_id: str | None = None) -> int:
|
|
366
|
+
"""Soft-delete every live attachment when its parent item is deleted."""
|
|
367
|
+
now = datetime.now(UTC)
|
|
368
|
+
result = await CredentialAttachment.update_where(
|
|
369
|
+
{"credential_item_id": item_id, "deleted_at__isnull": True},
|
|
370
|
+
deleted_at=now,
|
|
371
|
+
updated_by=actor_id,
|
|
372
|
+
updated_at=now,
|
|
373
|
+
)
|
|
374
|
+
return result.rows_affected
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
async def copy_item_attachments(
|
|
378
|
+
source_item_id: str,
|
|
379
|
+
destination_item_id: str,
|
|
380
|
+
*,
|
|
381
|
+
actor_id: str | None = None,
|
|
382
|
+
) -> int:
|
|
383
|
+
"""Re-encrypt independent attachment rows for a forked item."""
|
|
384
|
+
rows = await CredentialAttachment.filter(
|
|
385
|
+
credential_item_id=source_item_id, deleted_at__isnull=True
|
|
386
|
+
).values(*_ATTACHMENT_FIELDS, "value_encrypted")
|
|
387
|
+
copied = 0
|
|
388
|
+
for raw in rows:
|
|
389
|
+
content = decrypt_bytes(raw["value_encrypted"])
|
|
390
|
+
await CredentialAttachment.create(
|
|
391
|
+
credential_item_id=destination_item_id,
|
|
392
|
+
label=raw["label"],
|
|
393
|
+
description=raw.get("description"),
|
|
394
|
+
file_name=raw["file_name"],
|
|
395
|
+
media_type=raw["media_type"],
|
|
396
|
+
size_bytes=raw["size_bytes"],
|
|
397
|
+
value_encrypted=encrypt_bytes(content),
|
|
398
|
+
handling=raw["handling"],
|
|
399
|
+
created_by=actor_id,
|
|
400
|
+
updated_by=actor_id,
|
|
401
|
+
)
|
|
402
|
+
copied += 1
|
|
403
|
+
return copied
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
__all__ = [
|
|
407
|
+
"MAX_VAULT_ATTACHMENT_BYTES",
|
|
408
|
+
"CredentialAttachmentRow",
|
|
409
|
+
"add_credential_attachment",
|
|
410
|
+
"copy_item_attachments",
|
|
411
|
+
"delete_credential_attachment",
|
|
412
|
+
"get_credential_attachment",
|
|
413
|
+
"list_credential_attachments",
|
|
414
|
+
"replace_credential_attachment",
|
|
415
|
+
"resolve_credential_attachment",
|
|
416
|
+
"reveal_credential_attachment",
|
|
417
|
+
"soft_delete_item_attachments",
|
|
418
|
+
"update_credential_attachment_metadata",
|
|
419
|
+
]
|
|
@@ -130,6 +130,21 @@ def decrypt_value(ciphertext: bytes) -> str:
|
|
|
130
130
|
return get_fernet().decrypt(bytes(ciphertext)).decode("utf-8")
|
|
131
131
|
|
|
132
132
|
|
|
133
|
+
# ── Binary secrets (users.credential_attachments) ────────────────────────
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def encrypt_bytes(plaintext: bytes) -> bytes:
|
|
137
|
+
if not isinstance(plaintext, bytes):
|
|
138
|
+
raise TypeError("plaintext must be bytes")
|
|
139
|
+
return get_fernet().encrypt(plaintext)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def decrypt_bytes(ciphertext: bytes) -> bytes:
|
|
143
|
+
if not isinstance(ciphertext, bytes | bytearray | memoryview):
|
|
144
|
+
raise TypeError("ciphertext must be bytes-like")
|
|
145
|
+
return get_fernet().decrypt(bytes(ciphertext))
|
|
146
|
+
|
|
147
|
+
|
|
133
148
|
# ── Dict payloads (tool.mcp_user_conn credential blobs) ──────────────────
|
|
134
149
|
|
|
135
150
|
|
|
@@ -165,6 +180,8 @@ __all__ = [
|
|
|
165
180
|
"get_fernet",
|
|
166
181
|
"encrypt_value",
|
|
167
182
|
"decrypt_value",
|
|
183
|
+
"encrypt_bytes",
|
|
184
|
+
"decrypt_bytes",
|
|
168
185
|
"encrypt_payload",
|
|
169
186
|
"decrypt_payload",
|
|
170
187
|
"mask_value",
|
|
@@ -668,7 +668,10 @@ async def delete_credential_item(
|
|
|
668
668
|
raw = await _require_item_raw(item_id)
|
|
669
669
|
now = datetime.now(UTC)
|
|
670
670
|
async with transaction(PACKAGE_DB_NAME):
|
|
671
|
+
from .attachments import soft_delete_item_attachments
|
|
672
|
+
|
|
671
673
|
await UserSecretGrant.delete_where(credential_item_id=item_id)
|
|
674
|
+
await soft_delete_item_attachments(item_id, actor_id=actor_id)
|
|
672
675
|
await UserSecret.update_where(
|
|
673
676
|
{"credential_item_id": item_id, "deleted_at__isnull": True},
|
|
674
677
|
deleted_at=now,
|
|
@@ -1197,9 +1200,9 @@ async def add_item_grant(
|
|
|
1197
1200
|
if not user_id:
|
|
1198
1201
|
raise ValueError("grant user_id is required")
|
|
1199
1202
|
await _require_item_raw(item_id)
|
|
1200
|
-
existing = await UserSecretGrant.filter(
|
|
1201
|
-
|
|
1202
|
-
)
|
|
1203
|
+
existing = await UserSecretGrant.filter(credential_item_id=item_id, user_id=user_id).values(
|
|
1204
|
+
"id"
|
|
1205
|
+
)
|
|
1203
1206
|
if existing:
|
|
1204
1207
|
grant_id = str(existing[0]["id"])
|
|
1205
1208
|
await UserSecretGrant.update_where(
|
|
@@ -1462,12 +1465,19 @@ async def fork_credential_item(
|
|
|
1462
1465
|
created_by=actor_id,
|
|
1463
1466
|
updated_by=actor_id,
|
|
1464
1467
|
)
|
|
1468
|
+
from .attachments import copy_item_attachments
|
|
1469
|
+
|
|
1470
|
+
attachment_count = await copy_item_attachments(item_id, str(new_item.id), actor_id=actor_id)
|
|
1465
1471
|
await record_secret_audit(
|
|
1466
1472
|
action="item_forked",
|
|
1467
1473
|
actor_id=actor_id,
|
|
1468
1474
|
organization_id=to_organization_id,
|
|
1469
1475
|
user_id=to_user_id,
|
|
1470
|
-
metadata={
|
|
1476
|
+
metadata={
|
|
1477
|
+
"credential_item_id": str(new_item.id),
|
|
1478
|
+
"forked_from": item_id,
|
|
1479
|
+
"attachment_count": attachment_count,
|
|
1480
|
+
},
|
|
1471
1481
|
)
|
|
1472
1482
|
item = await get_credential_item(str(new_item.id))
|
|
1473
1483
|
if item is None: # pragma: no cover
|