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.
Files changed (328) hide show
  1. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/PKG-INFO +1 -1
  2. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/catalog.py +31 -0
  3. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/FEATURE.md +22 -0
  4. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/__init__.py +40 -1
  5. matrx_orm-3.1.55/matrx_orm/secrets_battery/attachments.py +419 -0
  6. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/crypto.py +17 -0
  7. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/items.py +14 -4
  8. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/model.py +41 -1
  9. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/pyproject.toml +1 -1
  10. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_secrets_battery.py +20 -0
  11. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/.gitignore +0 -0
  12. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/AGENTS.md +0 -0
  13. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/BASE_CLASS_METHODS.md +0 -0
  14. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/CLAUDE.md +0 -0
  15. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/MODEL_API.md +0 -0
  16. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/README.md +0 -0
  17. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/database/orm/extended/managers/ai_model_base.py +0 -0
  18. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/docs/migrations.md +0 -0
  19. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/MODULE_README.md +0 -0
  20. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/__init__.py +0 -0
  21. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/MODULE_README.md +0 -0
  22. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/__init__.py +0 -0
  23. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/async_postgresql.py +0 -0
  24. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/base_adapter.py +0 -0
  25. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/postgresql.py +0 -0
  26. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  27. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/adapters/supabase_adapter.py +0 -0
  28. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/admin/__init__.py +0 -0
  29. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/admin/router.py +0 -0
  30. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/__init__.py +0 -0
  31. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/auth.py +0 -0
  32. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/config.py +0 -0
  33. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/handlers.py +0 -0
  34. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/protocol.py +0 -0
  35. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/api/server.py +0 -0
  36. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/associations.py +0 -0
  37. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/cache_debug.py +0 -0
  38. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/MODULE_README.md +0 -0
  39. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/__init__.py +0 -0
  40. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/postgres_connection.py +0 -0
  41. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/postgrest.py +0 -0
  42. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/supabase_auth.py +0 -0
  43. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/supabase_config.py +0 -0
  44. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/client/supabase_manager.py +0 -0
  45. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  46. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/FEATURE.md +0 -0
  47. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/MODULE_README.md +0 -0
  48. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  49. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/TASKS.md +0 -0
  50. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/__init__.py +0 -0
  51. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/async_db_manager.py +0 -0
  52. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/base.py +0 -0
  53. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/config.py +0 -0
  54. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/connections.py +0 -0
  55. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/db_function.py +0 -0
  56. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/diagnostics.py +0 -0
  57. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/expressions.py +0 -0
  58. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/extended.py +0 -0
  59. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/fields.py +0 -0
  60. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/introspection.py +0 -0
  61. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/listen.py +0 -0
  62. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/loop_filters.py +0 -0
  63. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/model_dto.py +0 -0
  64. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/model_view.py +0 -0
  65. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/one_database.py +0 -0
  66. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/paginator.py +0 -0
  67. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/pool_maintenance.py +0 -0
  68. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/pydantic_bridge.py +0 -0
  69. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/registry.py +0 -0
  70. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/relations.py +0 -0
  71. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/resilience.py +0 -0
  72. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/rls_session.py +0 -0
  73. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/signals.py +0 -0
  74. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/tls.py +0 -0
  75. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/transaction.py +0 -0
  76. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/types.py +0 -0
  77. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/core/write_queue.py +0 -0
  78. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/entity.py +0 -0
  79. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/error_handling.py +0 -0
  80. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/exceptions.py +0 -0
  81. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/extended/__init__.py +0 -0
  82. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/extended/app_error_handler.py +0 -0
  83. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/identity_rule.py +0 -0
  84. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/middleware/__init__.py +0 -0
  85. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/middleware/base.py +0 -0
  86. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/MODULE_README.md +0 -0
  87. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/__init__.py +0 -0
  88. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/cli.py +0 -0
  89. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/ddl.py +0 -0
  90. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/diff.py +0 -0
  91. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/executor.py +0 -0
  92. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/integration.py +0 -0
  93. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/loader.py +0 -0
  94. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/operations.py +0 -0
  95. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/state.py +0 -0
  96. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/migrations/table_filter.py +0 -0
  97. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/MODULE_README.md +0 -0
  98. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/__init__.py +0 -0
  99. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/bulk_update_values.py +0 -0
  100. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/conflict.py +0 -0
  101. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/conflict_writes.py +0 -0
  102. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/create.py +0 -0
  103. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/db_functions.py +0 -0
  104. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/delete.py +0 -0
  105. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/dynamic_admin.py +0 -0
  106. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/dynamic_crud.py +0 -0
  107. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/graph_walk.py +0 -0
  108. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/latest_rows.py +0 -0
  109. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/queue_claim.py +0 -0
  110. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/read.py +0 -0
  111. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/relational_backfill.py +0 -0
  112. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/staging_load.py +0 -0
  113. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/operations/update.py +0 -0
  114. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/platform_db.py +0 -0
  115. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/py.typed +0 -0
  116. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/MODULE_README.md +0 -0
  117. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/__init__.py +0 -0
  118. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/db_objects.py +0 -0
  119. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  120. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  121. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/FEATURE.md +0 -0
  122. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/MODULE_README.md +0 -0
  123. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/__init__.py +0 -0
  124. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/builder.py +0 -0
  125. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/query/executor.py +0 -0
  126. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  127. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/__init__.py +0 -0
  128. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/code_handler.py +0 -0
  129. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  130. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/columns.py +0 -0
  131. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/common.py +0 -0
  132. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/diff_preview.py +0 -0
  133. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  134. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/generator.py +0 -0
  135. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  136. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  137. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  138. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  139. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/package_wiring.py +0 -0
  140. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/relationships.py +0 -0
  141. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/runner.py +0 -0
  142. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/schema.py +0 -0
  143. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/schema_manager.py +0 -0
  144. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/tables.py +0 -0
  145. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/schema_builder/views.py +0 -0
  146. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/org_service.py +0 -0
  147. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/secrets_battery/service.py +0 -0
  148. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/README.md +0 -0
  149. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/__init__.py +0 -0
  150. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/coalesce.py +0 -0
  151. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/dag.py +0 -0
  152. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/errors.py +0 -0
  153. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/fallback.py +0 -0
  154. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/flush.py +0 -0
  155. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/lifecycle.py +0 -0
  156. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/managed.py +0 -0
  157. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/op.py +0 -0
  158. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/reads.py +0 -0
  159. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/session.py +0 -0
  160. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/session/telemetry.py +0 -0
  161. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  162. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/__init__.py +0 -0
  163. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/executor.py +0 -0
  164. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/queries.py +0 -0
  165. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/registry.py +0 -0
  166. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/types.py +0 -0
  167. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/sql_executor/utils.py +0 -0
  168. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/state.py +0 -0
  169. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/utils/__init__.py +0 -0
  170. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/utils/sql_utils.py +0 -0
  171. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/matrx_orm/utils/type_converters.py +0 -0
  172. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/scripts/git-branches.sh +0 -0
  173. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/scripts/publish.sh +0 -0
  174. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/scripts/release.sh +0 -0
  175. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/MODULE_README.md +0 -0
  176. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/__init__.py +0 -0
  177. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/conftest.py +0 -0
  178. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/MODULE_README.md +0 -0
  179. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/__init__.py +0 -0
  180. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_admin_db_columns.py +0 -0
  181. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_admin_search_negation.py +0 -0
  182. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_admin_search_sentinels.py +0 -0
  183. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_advisory_lock.py +0 -0
  184. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_agent_message.py +0 -0
  185. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_auth.py +0 -0
  186. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_config.py +0 -0
  187. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_handlers.py +0 -0
  188. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_api_protocol.py +0 -0
  189. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_array_agg_order_by.py +0 -0
  190. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_associations.py +0 -0
  191. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_bulk_update_by_pk.py +0 -0
  192. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  193. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_cache_debug.py +0 -0
  194. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_call_function.py +0 -0
  195. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_case_when.py +0 -0
  196. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_catalog.py +0 -0
  197. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_composite_pk_filter.py +0 -0
  198. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_composite_pk_write_paths.py +0 -0
  199. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_config.py +0 -0
  200. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_conflict_target.py +0 -0
  201. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_conflict_writes.py +0 -0
  202. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_connection_codecs.py +0 -0
  203. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_connection_poison_guard.py +0 -0
  204. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_connection_primitives.py +0 -0
  205. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_count_composite_distinct.py +0 -0
  206. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_db_function_field.py +0 -0
  207. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_ddl_generator.py +0 -0
  208. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_derived_table.py +0 -0
  209. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_dirty_tracking.py +0 -0
  210. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_dynamic_crud.py +0 -0
  211. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_exceptions.py +0 -0
  212. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_execute_query_integrity_mapping.py +0 -0
  213. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_expression_primitives.py +0 -0
  214. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_fields.py +0 -0
  215. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  216. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_from_alias.py +0 -0
  217. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_fts.py +0 -0
  218. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_func_expression_args.py +0 -0
  219. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_graph_walk.py +0 -0
  220. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_having_annotate_alias.py +0 -0
  221. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  222. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_introspect_rls_policies.py +0 -0
  223. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_join_and_aggregate.py +0 -0
  224. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_latest_rows.py +0 -0
  225. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_listen_notify.py +0 -0
  226. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_managed_write_guard.py +0 -0
  227. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_matrx_entity.py +0 -0
  228. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_migration_diff_types.py +0 -0
  229. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_migration_loader.py +0 -0
  230. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_model_instance.py +0 -0
  231. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_model_meta.py +0 -0
  232. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_optimistic_concurrency.py +0 -0
  233. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_order_by_expression.py +0 -0
  234. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_parameter_name_hydration.py +0 -0
  235. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_patch_jsonb_path.py +0 -0
  236. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_pool_loop_recreation.py +0 -0
  237. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_pool_maintenance.py +0 -0
  238. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_postgrest_filters.py +0 -0
  239. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_pydantic_bridge.py +0 -0
  240. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_builder.py +0 -0
  241. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_error_detail.py +0 -0
  242. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_executor_sql.py +0 -0
  243. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_query_timeout.py +0 -0
  244. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  245. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_queue_claim.py +0 -0
  246. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_registry.py +0 -0
  247. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_registry_multi_database.py +0 -0
  248. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_registry_multi_schema.py +0 -0
  249. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_relational_backfill.py +0 -0
  250. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_relations.py +0 -0
  251. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_resilience.py +0 -0
  252. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_schema_exists.py +0 -0
  253. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_session_advisory_lock.py +0 -0
  254. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_state_cache.py +0 -0
  255. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_subquery_filter_raw.py +0 -0
  256. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_subquery_in_filter.py +0 -0
  257. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_supabase_auth.py +0 -0
  258. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_supabase_config.py +0 -0
  259. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_tls.py +0 -0
  260. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_typed_join_projection.py +0 -0
  261. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_typed_predicates.py +0 -0
  262. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_update_case_expression.py +0 -0
  263. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_update_subquery.py +0 -0
  264. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_update_with_rebase.py +0 -0
  265. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_upsert_default_leak.py +0 -0
  266. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_upsert_with_conflict.py +0 -0
  267. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_values_jsonb_decode.py +0 -0
  268. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_watched_lifecycle.py +0 -0
  269. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level1/test_write_retry_semantics.py +0 -0
  270. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/MODULE_README.md +0 -0
  271. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/__init__.py +0 -0
  272. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/conftest.py +0 -0
  273. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_bulk_ops.py +0 -0
  274. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_cache_integration.py +0 -0
  275. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_crud.py +0 -0
  276. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_foreign_keys.py +0 -0
  277. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_m2m.py +0 -0
  278. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_manager.py +0 -0
  279. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_migrations_live.py +0 -0
  280. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_query_execution.py +0 -0
  281. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/level2/test_schema_diff.py +0 -0
  282. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/.env.example +0 -0
  283. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/README.md +0 -0
  284. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/__init__.py +0 -0
  285. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/generate.py +0 -0
  286. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/generated/.gitkeep +0 -0
  287. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/matrx_orm.yaml +0 -0
  288. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project/test_schema_generation.py +0 -0
  289. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/.env.example +0 -0
  290. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/README.md +0 -0
  291. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/__init__.py +0 -0
  292. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/client_example.py +0 -0
  293. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/client_example.ts +0 -0
  294. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  295. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/sample_project_desktop/server.py +0 -0
  296. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/entity_tests.py +0 -0
  297. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_base_generation.py +0 -0
  298. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  299. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_composite_unique_generation.py +0 -0
  300. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_dto_identity_generation.py +0 -0
  301. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_entity_capability_generation.py +0 -0
  302. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_generate_schema.py +0 -0
  303. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_junction_analysis.py +0 -0
  304. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_multi_schema_output.py +0 -0
  305. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_sql_expression_defaults.py +0 -0
  306. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema/test_view_generation.py +0 -0
  307. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  308. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/schema_builder/test_host_models_aggregator.py +0 -0
  309. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/__init__.py +0 -0
  310. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/conftest.py +0 -0
  311. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_cache_no_downgrade.py +0 -0
  312. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_capture_poison_proof.py +0 -0
  313. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_coalesce.py +0 -0
  314. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_dag.py +0 -0
  315. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_disk_spill_fallback.py +0 -0
  316. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_flush_individual_fallback.py +0 -0
  317. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_governed_write.py +0 -0
  318. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_managed.py +0 -0
  319. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_op_enqueue_site.py +0 -0
  320. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_reads.py +0 -0
  321. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/session/test_session.py +0 -0
  322. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_database_name_alias.py +0 -0
  323. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_db_functions.py +0 -0
  324. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_model_cls_refactor.py +0 -0
  325. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  326. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_one_database.py +0 -0
  327. {matrx_orm-3.1.53 → matrx_orm-3.1.55}/tests/test_sql_param_casts.py +0 -0
  328. {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.53
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 PACKAGE_DB_NAME, CredentialItem, UserSecret, UserSecretAudit, UserSecretGrant
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
- credential_item_id=item_id, user_id=user_id
1202
- ).values("id")
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={"credential_item_id": str(new_item.id), "forked_from": item_id},
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