matrx-orm 3.1.38__tar.gz → 3.1.40__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 (317) hide show
  1. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/PKG-INFO +1 -1
  2. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/secrets_battery/FEATURE.md +53 -5
  3. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/secrets_battery/__init__.py +12 -0
  4. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/secrets_battery/items.py +375 -10
  5. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/secrets_battery/org_service.py +38 -3
  6. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/pyproject.toml +1 -1
  7. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/test_secrets_battery.py +193 -0
  8. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/.gitignore +0 -0
  9. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/AGENTS.md +0 -0
  10. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/BASE_CLASS_METHODS.md +0 -0
  11. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/CLAUDE.md +0 -0
  12. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/MODEL_API.md +0 -0
  13. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/README.md +0 -0
  14. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/database/orm/extended/managers/ai_model_base.py +0 -0
  15. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/docs/migrations.md +0 -0
  16. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/MODULE_README.md +0 -0
  17. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/__init__.py +0 -0
  18. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/adapters/MODULE_README.md +0 -0
  19. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/adapters/__init__.py +0 -0
  20. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/adapters/async_postgresql.py +0 -0
  21. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/adapters/base_adapter.py +0 -0
  22. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/adapters/postgresql.py +0 -0
  23. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/adapters/postgrest_client_adapter.py +0 -0
  24. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/adapters/supabase_adapter.py +0 -0
  25. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/admin/__init__.py +0 -0
  26. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/admin/router.py +0 -0
  27. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/api/__init__.py +0 -0
  28. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/api/auth.py +0 -0
  29. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/api/config.py +0 -0
  30. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/api/handlers.py +0 -0
  31. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/api/protocol.py +0 -0
  32. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/api/server.py +0 -0
  33. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/associations.py +0 -0
  34. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/cache_debug.py +0 -0
  35. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/catalog.py +0 -0
  36. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/client/MODULE_README.md +0 -0
  37. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/client/__init__.py +0 -0
  38. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/client/postgres_connection.py +0 -0
  39. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/client/postgrest.py +0 -0
  40. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/client/supabase_auth.py +0 -0
  41. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/client/supabase_config.py +0 -0
  42. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/client/supabase_manager.py +0 -0
  43. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/EXTENDED-TASK.md +0 -0
  44. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/MODULE_README.md +0 -0
  45. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/RELATIONS-TASKS.md +0 -0
  46. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/TASKS.md +0 -0
  47. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/__init__.py +0 -0
  48. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/async_db_manager.py +0 -0
  49. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/base.py +0 -0
  50. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/config.py +0 -0
  51. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/connections.py +0 -0
  52. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/db_function.py +0 -0
  53. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/diagnostics.py +0 -0
  54. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/expressions.py +0 -0
  55. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/extended.py +0 -0
  56. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/fields.py +0 -0
  57. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/introspection.py +0 -0
  58. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/listen.py +0 -0
  59. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/loop_filters.py +0 -0
  60. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/model_dto.py +0 -0
  61. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/model_view.py +0 -0
  62. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/paginator.py +0 -0
  63. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/pool_maintenance.py +0 -0
  64. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/pydantic_bridge.py +0 -0
  65. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/registry.py +0 -0
  66. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/relations.py +0 -0
  67. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/resilience.py +0 -0
  68. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/rls_session.py +0 -0
  69. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/signals.py +0 -0
  70. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/tls.py +0 -0
  71. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/transaction.py +0 -0
  72. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/types.py +0 -0
  73. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/core/write_queue.py +0 -0
  74. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/entity.py +0 -0
  75. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/error_handling.py +0 -0
  76. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/exceptions.py +0 -0
  77. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/extended/__init__.py +0 -0
  78. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/extended/app_error_handler.py +0 -0
  79. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/identity_rule.py +0 -0
  80. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/middleware/__init__.py +0 -0
  81. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/middleware/base.py +0 -0
  82. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/MODULE_README.md +0 -0
  83. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/__init__.py +0 -0
  84. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/cli.py +0 -0
  85. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/ddl.py +0 -0
  86. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/diff.py +0 -0
  87. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/executor.py +0 -0
  88. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/integration.py +0 -0
  89. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/loader.py +0 -0
  90. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/operations.py +0 -0
  91. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/state.py +0 -0
  92. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/migrations/table_filter.py +0 -0
  93. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/MODULE_README.md +0 -0
  94. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/__init__.py +0 -0
  95. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/bulk_update_values.py +0 -0
  96. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/conflict.py +0 -0
  97. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/conflict_writes.py +0 -0
  98. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/create.py +0 -0
  99. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/db_functions.py +0 -0
  100. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/delete.py +0 -0
  101. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/dynamic_admin.py +0 -0
  102. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/dynamic_crud.py +0 -0
  103. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/graph_walk.py +0 -0
  104. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/latest_rows.py +0 -0
  105. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/queue_claim.py +0 -0
  106. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/read.py +0 -0
  107. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/relational_backfill.py +0 -0
  108. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/staging_load.py +0 -0
  109. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/operations/update.py +0 -0
  110. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/py.typed +0 -0
  111. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/python_sql/MODULE_README.md +0 -0
  112. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/python_sql/__init__.py +0 -0
  113. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/python_sql/db_objects.py +0 -0
  114. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/python_sql/table_detailed_relationships.py +0 -0
  115. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/python_sql/table_typescript_relationship.py +0 -0
  116. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/query/MODULE_README.md +0 -0
  117. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/query/__init__.py +0 -0
  118. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/query/builder.py +0 -0
  119. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/query/executor.py +0 -0
  120. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/MODULE_README.md +0 -0
  121. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/__init__.py +0 -0
  122. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/code_handler.py +0 -0
  123. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/codegen_writer.py +0 -0
  124. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/columns.py +0 -0
  125. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/common.py +0 -0
  126. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/diff_preview.py +0 -0
  127. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/entity_capabilities.py +0 -0
  128. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/generator.py +0 -0
  129. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/helpers/__init__.py +0 -0
  130. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/helpers/base_generators.py +0 -0
  131. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/helpers/entity_generators.py +0 -0
  132. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/helpers/git_checker.py +0 -0
  133. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/package_wiring.py +0 -0
  134. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/relationships.py +0 -0
  135. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/runner.py +0 -0
  136. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/schema.py +0 -0
  137. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/schema_manager.py +0 -0
  138. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/tables.py +0 -0
  139. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/schema_builder/views.py +0 -0
  140. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/secrets_battery/crypto.py +0 -0
  141. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/secrets_battery/model.py +0 -0
  142. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/secrets_battery/service.py +0 -0
  143. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/README.md +0 -0
  144. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/__init__.py +0 -0
  145. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/coalesce.py +0 -0
  146. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/dag.py +0 -0
  147. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/errors.py +0 -0
  148. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/fallback.py +0 -0
  149. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/flush.py +0 -0
  150. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/lifecycle.py +0 -0
  151. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/managed.py +0 -0
  152. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/op.py +0 -0
  153. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/reads.py +0 -0
  154. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/session.py +0 -0
  155. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/session/telemetry.py +0 -0
  156. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/sql_executor/MODULE_README.md +0 -0
  157. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/sql_executor/__init__.py +0 -0
  158. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/sql_executor/executor.py +0 -0
  159. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/sql_executor/queries.py +0 -0
  160. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/sql_executor/registry.py +0 -0
  161. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/sql_executor/types.py +0 -0
  162. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/sql_executor/utils.py +0 -0
  163. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/state.py +0 -0
  164. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/utils/__init__.py +0 -0
  165. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/utils/sql_utils.py +0 -0
  166. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/matrx_orm/utils/type_converters.py +0 -0
  167. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/scripts/git-branches.sh +0 -0
  168. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/scripts/publish.sh +0 -0
  169. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/scripts/release.sh +0 -0
  170. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/MODULE_README.md +0 -0
  171. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/__init__.py +0 -0
  172. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/conftest.py +0 -0
  173. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/MODULE_README.md +0 -0
  174. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/__init__.py +0 -0
  175. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_admin_db_columns.py +0 -0
  176. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_admin_search_negation.py +0 -0
  177. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_admin_search_sentinels.py +0 -0
  178. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_advisory_lock.py +0 -0
  179. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_agent_message.py +0 -0
  180. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_api_auth.py +0 -0
  181. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_api_config.py +0 -0
  182. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_api_handlers.py +0 -0
  183. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_api_protocol.py +0 -0
  184. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_array_agg_order_by.py +0 -0
  185. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_associations.py +0 -0
  186. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_bulk_update_by_pk.py +0 -0
  187. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_bulk_upsert_increment_set_fields.py +0 -0
  188. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_cache_debug.py +0 -0
  189. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_call_function.py +0 -0
  190. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_case_when.py +0 -0
  191. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_catalog.py +0 -0
  192. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_composite_pk_filter.py +0 -0
  193. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_composite_pk_write_paths.py +0 -0
  194. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_config.py +0 -0
  195. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_conflict_target.py +0 -0
  196. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_conflict_writes.py +0 -0
  197. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_connection_codecs.py +0 -0
  198. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_connection_poison_guard.py +0 -0
  199. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_connection_primitives.py +0 -0
  200. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_count_composite_distinct.py +0 -0
  201. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_db_function_field.py +0 -0
  202. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_ddl_generator.py +0 -0
  203. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_derived_table.py +0 -0
  204. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_dirty_tracking.py +0 -0
  205. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_dynamic_crud.py +0 -0
  206. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_exceptions.py +0 -0
  207. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_expression_primitives.py +0 -0
  208. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_fields.py +0 -0
  209. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_filter_jsonb_agg_array_index.py +0 -0
  210. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_from_alias.py +0 -0
  211. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_fts.py +0 -0
  212. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_func_expression_args.py +0 -0
  213. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_graph_walk.py +0 -0
  214. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_having_annotate_alias.py +0 -0
  215. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_insert_ignore_and_admin_primitives.py +0 -0
  216. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_introspect_rls_policies.py +0 -0
  217. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_join_and_aggregate.py +0 -0
  218. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_latest_rows.py +0 -0
  219. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_listen_notify.py +0 -0
  220. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_managed_write_guard.py +0 -0
  221. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_matrx_entity.py +0 -0
  222. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_migration_diff_types.py +0 -0
  223. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_migration_loader.py +0 -0
  224. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_model_instance.py +0 -0
  225. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_model_meta.py +0 -0
  226. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_order_by_expression.py +0 -0
  227. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_patch_jsonb_path.py +0 -0
  228. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_pool_loop_recreation.py +0 -0
  229. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_pool_maintenance.py +0 -0
  230. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_postgrest_filters.py +0 -0
  231. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_pydantic_bridge.py +0 -0
  232. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_query_builder.py +0 -0
  233. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_query_error_detail.py +0 -0
  234. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_query_executor_sql.py +0 -0
  235. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_query_timeout.py +0 -0
  236. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_querybuilder_clone_semantics.py +0 -0
  237. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_queue_claim.py +0 -0
  238. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_registry.py +0 -0
  239. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_registry_multi_database.py +0 -0
  240. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_registry_multi_schema.py +0 -0
  241. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_relational_backfill.py +0 -0
  242. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_relations.py +0 -0
  243. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_resilience.py +0 -0
  244. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_schema_exists.py +0 -0
  245. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_session_advisory_lock.py +0 -0
  246. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_state_cache.py +0 -0
  247. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_subquery_filter_raw.py +0 -0
  248. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_subquery_in_filter.py +0 -0
  249. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_supabase_auth.py +0 -0
  250. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_supabase_config.py +0 -0
  251. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_tls.py +0 -0
  252. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_typed_join_projection.py +0 -0
  253. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_typed_predicates.py +0 -0
  254. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_update_case_expression.py +0 -0
  255. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_update_subquery.py +0 -0
  256. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_upsert_default_leak.py +0 -0
  257. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_upsert_with_conflict.py +0 -0
  258. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_values_jsonb_decode.py +0 -0
  259. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_watched_lifecycle.py +0 -0
  260. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level1/test_write_retry_semantics.py +0 -0
  261. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/MODULE_README.md +0 -0
  262. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/__init__.py +0 -0
  263. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/conftest.py +0 -0
  264. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_bulk_ops.py +0 -0
  265. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_cache_integration.py +0 -0
  266. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_crud.py +0 -0
  267. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_foreign_keys.py +0 -0
  268. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_m2m.py +0 -0
  269. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_manager.py +0 -0
  270. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_migrations_live.py +0 -0
  271. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_query_execution.py +0 -0
  272. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/level2/test_schema_diff.py +0 -0
  273. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project/.env.example +0 -0
  274. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project/README.md +0 -0
  275. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project/__init__.py +0 -0
  276. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project/generate.py +0 -0
  277. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project/generated/.gitkeep +0 -0
  278. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project/matrx_orm.yaml +0 -0
  279. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project/test_schema_generation.py +0 -0
  280. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project_desktop/.env.example +0 -0
  281. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project_desktop/README.md +0 -0
  282. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project_desktop/__init__.py +0 -0
  283. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project_desktop/client_example.py +0 -0
  284. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project_desktop/client_example.ts +0 -0
  285. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project_desktop/client_supabase_example.py +0 -0
  286. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/sample_project_desktop/server.py +0 -0
  287. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/entity_tests.py +0 -0
  288. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_base_generation.py +0 -0
  289. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_composite_pk_fk_generation.py +0 -0
  290. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_composite_unique_generation.py +0 -0
  291. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_dto_identity_generation.py +0 -0
  292. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_entity_capability_generation.py +0 -0
  293. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_generate_schema.py +0 -0
  294. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_junction_analysis.py +0 -0
  295. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_multi_schema_output.py +0 -0
  296. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_sql_expression_defaults.py +0 -0
  297. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema/test_view_generation.py +0 -0
  298. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/schema_builder/test_focused_generation_plan.py +0 -0
  299. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/__init__.py +0 -0
  300. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/conftest.py +0 -0
  301. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_cache_no_downgrade.py +0 -0
  302. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_capture_poison_proof.py +0 -0
  303. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_coalesce.py +0 -0
  304. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_dag.py +0 -0
  305. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_disk_spill_fallback.py +0 -0
  306. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_flush_individual_fallback.py +0 -0
  307. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_governed_write.py +0 -0
  308. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_managed.py +0 -0
  309. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_op_enqueue_site.py +0 -0
  310. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_reads.py +0 -0
  311. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/session/test_session.py +0 -0
  312. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/test_database_name_alias.py +0 -0
  313. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/test_db_functions.py +0 -0
  314. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/test_model_cls_refactor.py +0 -0
  315. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/test_no_blocking_asyncpg_tls.py +0 -0
  316. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/test_sql_param_casts.py +0 -0
  317. {matrx_orm-3.1.38 → matrx_orm-3.1.40}/tests/test_sync_twins_guard.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: matrx-orm
3
- Version: 3.1.38
3
+ Version: 3.1.40
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
@@ -37,16 +37,60 @@ row whose owner columns don't match its item's owner.
37
37
  `editable=false` blocks human value changes (`FieldNotEditableError`); the owning
38
38
  service rotates via `system_rotation=True` / `rotate_credential_fields`.
39
39
 
40
+ **`inject_into_sandbox=true` REQUIRES an env alias — refused otherwise.** A
41
+ container environment is a NAME→value map, so a field with `key IS NULL` has
42
+ nowhere to land and bulk resolution drops it. Accepting the flag anyway meant a
43
+ user could switch "inject into sandbox" on and get nothing, forever, with no
44
+ error — the silent-failure class this guard ends. Three layers, each sufficient
45
+ alone, each loud:
46
+
47
+ 1. **Write refusal** — `create_credential_bundle` / `add_credential_field` /
48
+ `update_field_metadata` raise `SandboxInjectionWithoutEnvKeyError` (a
49
+ `ValueError`, so hosts answer 422 with the reason). `update_field_metadata`
50
+ judges the POST-patch state, so *clearing the alias of an already-injecting
51
+ field* is refused the same way. Setting `env_key` and the flag in ONE patch
52
+ is the supported fix and always passes.
53
+ 2. **Resolution scream** — `_keyed_for_sandbox` logs an ERROR naming the row ids
54
+ and skips them, so anything that got in around layer 1 is visible.
55
+ 3. **DB CHECK** `user_secrets_sandbox_injection_needs_key` (migration 0265) makes
56
+ the state unrepresentable; the column default moved `true` → `false` so the
57
+ database stops manufacturing violations on keyless inserts.
58
+
40
59
  Item surface: `create_credential_item` · `create_credential_bundle` (item + N
41
60
  fields, ONE transaction) · `get/list/update/delete_credential_item` (delete =
42
61
  soft, atomic across item + fields + grants) · `add_credential_field` ·
43
62
  `update_credential_field_value` · `update_field_metadata` (metadata-only patch — value/version untouched; sealed is a one-way door: TO sealed ok, FROM sealed never; env-alias regex + per-principal uniqueness) · `rotate_credential_fields` (atomic multi-field)
44
63
  · `delete_credential_field` · stable resolution `resolve_field_by_id` /
45
64
  `resolve_field(item_id, field_key)` / `resolve_item_fields` · `reveal_field` ·
46
- `set_item_grants` / `list_item_grants` · `transfer_credential_item` (owner flip,
47
- no value copy, references survive) · `fork_credential_item` (independent copy) ·
48
- `resolve_legacy_env_key` (compat read that SCREAMS in logs until call sites
49
- migrate to stable references).
65
+ `add_item_grant` / `update_item_grant` / `remove_item_grant` / `list_item_grants`
66
+ · `set_item_grants` · `list_granted_credential_items` ·
67
+ `transfer_credential_item` (owner flip, no value copy, references survive) ·
68
+ `fork_credential_item` (independent copy) · `resolve_legacy_env_key` (compat
69
+ read that SCREAMS in logs until call sites migrate to stable references).
70
+
71
+ **Grants: use the single-grant operations.** `add_item_grant` /
72
+ `update_item_grant` / `remove_item_grant` change ONE recipient and leave the
73
+ rest alone. `set_item_grants` REPLACES the whole set (delete + recreate) and
74
+ drops every grantee when `access_mode='all_members'` — it survives for the org
75
+ access-mode flip, but building new callers on it is how a share edit silently
76
+ revokes someone. `list_granted_credential_items(user_id)` is the
77
+ "shared with me" read.
78
+
79
+ `transfer_credential_item` clears prior grants by default (`clear_grants=True`)
80
+ and writes BOTH `item_transferred` and `item_transferred_out`, so the losing
81
+ owner's audit shows it too. Its `to_user_id` accepts any user — deciding who may
82
+ transfer to whom (resolved from an exact email) is the HOST's job, per the
83
+ battery's no-tenancy rule.
84
+
85
+ **Destination-login metadata (2026-07-26, credential-sharing PLAN).** Items also
86
+ carry PLAINTEXT, deliberately-unencrypted columns: `login_urls` (`text[]`),
87
+ `uri_match_mode` (`host|exact|never`), `notes`, `non_secret_fields` (ordered
88
+ `[{key,label,value}]`), `browser_fill_enabled`. This module validates their
89
+ shape — URLs must be http/https, `non_secret_fields` keys match `FIELD_KEY_RE`
90
+ and are unique, entries carry only `{key,label,value}` — because it is the only
91
+ write path. `handling` does NOT apply to them: whether a value is encrypted is
92
+ decided by the catalog field's `storage_class`, never by handling. Never move a
93
+ secret into these columns to make it "easier to read".
50
94
 
51
95
  ## Consumer contract
52
96
 
@@ -93,7 +137,10 @@ EXCLUDES `sealed` fields, so a browser-facing caller can never receive sealed
93
137
  plaintext through an env dump. Only trusted execution (service-token sandbox
94
138
  injection, in-process context hydration, provider adapters) passes `True`.
95
139
  Rows with `key IS NULL` (item fields without an env alias) never resolve into
96
- an environment in either mode.
140
+ an environment in either mode — silently when nothing promised otherwise (a
141
+ keyless item field is normal), but **LOUDLY when `for_sandbox=True`**: such a row
142
+ is flagged for injection and cannot deliver, which is a defect (see the
143
+ handling doctrine above).
97
144
 
98
145
  **Legacy key-based writes respect item discipline:** `create_user_secret(upsert=True)`,
99
146
  `update_user_secret`, and `delete_user_secret` REFUSE a target row that is
@@ -191,6 +238,7 @@ create, reveal, sealed denial, editable gate, rotate, item grants, soft delete).
191
238
 
192
239
  ## Change Log
193
240
 
241
+ - 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.
194
242
  - 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).
195
243
  - 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.
196
244
  - 2026-07-23 — Phase 2: item layer (`items.py` — bundles, handling modes, stable resolution, item grants, transfer/fork), rotation primitives (`fernet_for_key`/`multi_fernet_for_keys`) + `scripts/rotate_credentials_key.py` (closes the no-rotation-script ticket), migration 0236 grants fixup (item-level grants possible).
@@ -41,14 +41,17 @@ from .items import (
41
41
  HANDLING_MODES,
42
42
  ITEM_SOURCES,
43
43
  ITEM_STATUSES,
44
+ URI_MATCH_MODES,
44
45
  CredentialFieldRow,
45
46
  CredentialItemNotFoundError,
46
47
  CredentialItemRow,
47
48
  FieldNotEditableError,
48
49
  FieldSpec,
49
50
  ItemGrantSpec,
51
+ SandboxInjectionWithoutEnvKeyError,
50
52
  SealedFieldError,
51
53
  add_credential_field,
54
+ add_item_grant,
52
55
  create_credential_bundle,
53
56
  create_credential_item,
54
57
  delete_credential_field,
@@ -56,7 +59,10 @@ from .items import (
56
59
  fork_credential_item,
57
60
  get_credential_item,
58
61
  list_credential_items,
62
+ list_granted_credential_items,
59
63
  list_item_grants,
64
+ remove_item_grant,
65
+ update_item_grant,
60
66
  resolve_field,
61
67
  resolve_field_by_id,
62
68
  resolve_item_fields,
@@ -145,8 +151,11 @@ __all__ = [
145
151
  "FieldNotEditableError",
146
152
  "FieldSpec",
147
153
  "ItemGrantSpec",
154
+ "SandboxInjectionWithoutEnvKeyError",
148
155
  "SealedFieldError",
156
+ "URI_MATCH_MODES",
149
157
  "add_credential_field",
158
+ "add_item_grant",
150
159
  "create_credential_bundle",
151
160
  "create_credential_item",
152
161
  "delete_credential_field",
@@ -154,7 +163,10 @@ __all__ = [
154
163
  "fork_credential_item",
155
164
  "get_credential_item",
156
165
  "list_credential_items",
166
+ "list_granted_credential_items",
157
167
  "list_item_grants",
168
+ "remove_item_grant",
169
+ "update_item_grant",
158
170
  "resolve_field",
159
171
  "resolve_field_by_id",
160
172
  "resolve_item_fields",
@@ -42,6 +42,7 @@ snake, stable semantic identity of a field within its item."""
42
42
 
43
43
  HANDLING_MODES: tuple[str, ...] = ("visible", "revealable", "sealed")
44
44
  ITEM_STATUSES: tuple[str, ...] = ("active", "disabled", "expired", "pending_verification")
45
+ URI_MATCH_MODES: tuple[str, ...] = ("host", "exact", "never")
45
46
  ITEM_SOURCES: tuple[str, ...] = (
46
47
  "manual",
47
48
  "env_import",
@@ -68,6 +69,18 @@ class FieldNotEditableError(PermissionError):
68
69
  (``rotate_credential_fields``), never by hand."""
69
70
 
70
71
 
72
+ class SandboxInjectionWithoutEnvKeyError(ValueError):
73
+ """``inject_into_sandbox=true`` on a field that has no environment-variable
74
+ alias (``key``). A container environment is a NAME→value map: with no name
75
+ the value has nowhere to land, so the flag is a promise the platform cannot
76
+ keep — the field would silently never appear in any sandbox.
77
+
78
+ Refused LOUDLY at every write path instead of accepted-and-dropped (hosts
79
+ map ``ValueError`` → 422). Set an env alias in the same request (the vault
80
+ field-metadata patch takes ``env_key`` and ``inject_into_sandbox``
81
+ together) or leave injection off."""
82
+
83
+
71
84
  # ── Shapes ────────────────────────────────────────────────────────────────
72
85
 
73
86
 
@@ -137,6 +150,13 @@ class CredentialItemRow:
137
150
  source: str
138
151
  access_mode: str
139
152
  lifecycle: dict[str, Any]
153
+ # Destination-login PLAINTEXT metadata (credential-sharing PLAN, 2026-07-26).
154
+ # Deliberately unencrypted; protected by the same ownership/grant/org RLS.
155
+ login_urls: list[str]
156
+ uri_match_mode: str
157
+ notes: str | None
158
+ non_secret_fields: list[dict[str, str]]
159
+ browser_fill_enabled: bool
140
160
  created_by: str | None
141
161
  updated_by: str | None
142
162
  created_at: str
@@ -167,6 +187,66 @@ def _validate_handling(handling: str) -> None:
167
187
  raise ValueError(f"handling must be one of {HANDLING_MODES}; got {handling!r}")
168
188
 
169
189
 
190
+ def _validate_sandbox_injectable(
191
+ *,
192
+ field_key: str | None,
193
+ env_key: str | None,
194
+ inject_into_sandbox: bool,
195
+ ) -> None:
196
+ """LAYER 1 of the no-silent-injection guard: a field may only be flagged
197
+ for sandbox injection while it carries an env alias. See
198
+ :class:`SandboxInjectionWithoutEnvKeyError`. Layer 2 is the loud skip in
199
+ ``org_service`` bulk resolution; layer 3 is the DB CHECK
200
+ ``user_secrets_sandbox_injection_needs_key``."""
201
+ if inject_into_sandbox and not env_key:
202
+ raise SandboxInjectionWithoutEnvKeyError(
203
+ f"field {field_key or '<unnamed>'!r} cannot be injected into sandboxes without "
204
+ "an environment-variable alias (env_key) — a container env has no name to bind "
205
+ "the value to, so the flag would silently do nothing. Set env_key in the same "
206
+ "request, or leave inject_into_sandbox off."
207
+ )
208
+
209
+
210
+ def _validate_uri_match_mode(mode: str) -> None:
211
+ if mode not in URI_MATCH_MODES:
212
+ raise ValueError(f"uri_match_mode must be one of {URI_MATCH_MODES}; got {mode!r}")
213
+
214
+
215
+ def _validate_login_urls(urls: list[str]) -> None:
216
+ if not isinstance(urls, list):
217
+ raise ValueError("login_urls must be a list of URLs")
218
+ for url in urls:
219
+ if not isinstance(url, str) or not url.strip():
220
+ raise ValueError("login_urls entries must be non-empty strings")
221
+ lowered = url.strip().lower()
222
+ if not (lowered.startswith("https://") or lowered.startswith("http://")):
223
+ raise ValueError(f"login URL must start with https:// or http://; got {url!r}")
224
+
225
+
226
+ def _validate_non_secret_fields(entries: list[dict[str, str]]) -> None:
227
+ """Ordered plaintext ``{key, label, value}`` entries. The DB pins the array
228
+ container; the exact entry shape is pinned here (the only write path)."""
229
+ if not isinstance(entries, list):
230
+ raise ValueError("non_secret_fields must be a list")
231
+ seen: set[str] = set()
232
+ for entry in entries:
233
+ if not isinstance(entry, dict):
234
+ raise ValueError("each non_secret_fields entry must be an object")
235
+ extra = set(entry) - {"key", "label", "value"}
236
+ if extra:
237
+ raise ValueError(f"non_secret_fields entry has unknown keys {sorted(extra)}")
238
+ key = entry.get("key")
239
+ if not isinstance(key, str) or not re.match(FIELD_KEY_RE, key):
240
+ raise ValueError(f"non_secret_fields entry key must match {FIELD_KEY_RE}; got {key!r}")
241
+ if key in seen:
242
+ raise ValueError(f"duplicate non_secret_fields key {key!r}")
243
+ seen.add(key)
244
+ if not isinstance(entry.get("label"), str) or not entry["label"]:
245
+ raise ValueError(f"non_secret_fields entry {key!r} needs a non-empty label")
246
+ if not isinstance(entry.get("value"), str):
247
+ raise ValueError(f"non_secret_fields entry {key!r} value must be a string")
248
+
249
+
170
250
  def _iso(value: Any) -> str:
171
251
  if isinstance(value, datetime):
172
252
  return value.isoformat()
@@ -187,6 +267,11 @@ _ITEM_FIELDS = (
187
267
  "source",
188
268
  "access_mode",
189
269
  "lifecycle",
270
+ "login_urls",
271
+ "uri_match_mode",
272
+ "notes",
273
+ "non_secret_fields",
274
+ "browser_fill_enabled",
190
275
  "created_by",
191
276
  "updated_by",
192
277
  "created_at",
@@ -225,6 +310,11 @@ def _item_row(raw: dict[str, Any], fields: list[CredentialFieldRow]) -> Credenti
225
310
  source=str(raw["source"]),
226
311
  access_mode=str(raw.get("access_mode") or "all_members"),
227
312
  lifecycle=dict(raw.get("lifecycle") or {}),
313
+ login_urls=list(raw.get("login_urls") or []),
314
+ uri_match_mode=str(raw.get("uri_match_mode") or "host"),
315
+ notes=raw.get("notes"),
316
+ non_secret_fields=list(raw.get("non_secret_fields") or []),
317
+ browser_fill_enabled=bool(raw.get("browser_fill_enabled", False)),
228
318
  created_by=str(raw["created_by"]) if raw.get("created_by") else None,
229
319
  updated_by=str(raw["updated_by"]) if raw.get("updated_by") else None,
230
320
  created_at=_iso(raw["created_at"]),
@@ -275,6 +365,33 @@ async def _require_item_raw(item_id: str) -> dict[str, Any]:
275
365
  # ── Item CRUD ─────────────────────────────────────────────────────────────
276
366
 
277
367
 
368
+ def _metadata_kwargs(
369
+ *,
370
+ login_urls: list[str] | None,
371
+ uri_match_mode: str | None,
372
+ notes: str | None,
373
+ non_secret_fields: list[dict[str, str]] | None,
374
+ browser_fill_enabled: bool | None,
375
+ ) -> dict[str, Any]:
376
+ """Validate + shape the destination-login plaintext metadata for a create.
377
+ Omitted (None) values fall back to the column defaults."""
378
+ out: dict[str, Any] = {}
379
+ if login_urls is not None:
380
+ _validate_login_urls(login_urls)
381
+ out["login_urls"] = [u.strip() for u in login_urls]
382
+ if uri_match_mode is not None:
383
+ _validate_uri_match_mode(uri_match_mode)
384
+ out["uri_match_mode"] = uri_match_mode
385
+ if notes is not None:
386
+ out["notes"] = notes
387
+ if non_secret_fields is not None:
388
+ _validate_non_secret_fields(non_secret_fields)
389
+ out["non_secret_fields"] = non_secret_fields
390
+ if browser_fill_enabled is not None:
391
+ out["browser_fill_enabled"] = bool(browser_fill_enabled)
392
+ return out
393
+
394
+
278
395
  async def create_credential_item(
279
396
  *,
280
397
  user_id: str | None = None,
@@ -288,6 +405,11 @@ async def create_credential_item(
288
405
  source: str = "manual",
289
406
  access_mode: str = "all_members",
290
407
  lifecycle: dict[str, Any] | None = None,
408
+ login_urls: list[str] | None = None,
409
+ uri_match_mode: str | None = None,
410
+ notes: str | None = None,
411
+ non_secret_fields: list[dict[str, str]] | None = None,
412
+ browser_fill_enabled: bool | None = None,
291
413
  actor_id: str | None = None,
292
414
  ) -> CredentialItemRow:
293
415
  """Create an empty item (no fields). Prefer :func:`create_credential_bundle`
@@ -310,6 +432,13 @@ async def create_credential_item(
310
432
  source=source,
311
433
  access_mode=access_mode,
312
434
  lifecycle=lifecycle or {},
435
+ **_metadata_kwargs(
436
+ login_urls=login_urls,
437
+ uri_match_mode=uri_match_mode,
438
+ notes=notes,
439
+ non_secret_fields=non_secret_fields,
440
+ browser_fill_enabled=browser_fill_enabled,
441
+ ),
313
442
  created_by=actor_id,
314
443
  updated_by=actor_id,
315
444
  )
@@ -340,6 +469,11 @@ async def create_credential_bundle(
340
469
  source: str = "manual",
341
470
  access_mode: str = "all_members",
342
471
  lifecycle: dict[str, Any] | None = None,
472
+ login_urls: list[str] | None = None,
473
+ uri_match_mode: str | None = None,
474
+ notes: str | None = None,
475
+ non_secret_fields: list[dict[str, str]] | None = None,
476
+ browser_fill_enabled: bool | None = None,
343
477
  actor_id: str | None = None,
344
478
  ) -> CredentialItemRow:
345
479
  """**The atomic create path**: item + N encrypted fields in ONE
@@ -355,6 +489,11 @@ async def create_credential_bundle(
355
489
  for spec in fields:
356
490
  _validate_field_key(spec.field_key)
357
491
  _validate_handling(spec.handling)
492
+ _validate_sandbox_injectable(
493
+ field_key=spec.field_key,
494
+ env_key=spec.env_key,
495
+ inject_into_sandbox=spec.inject_into_sandbox,
496
+ )
358
497
  if not isinstance(spec.value, str):
359
498
  raise ValueError(f"field {spec.field_key!r}: value must be a string")
360
499
  if spec.field_key in seen:
@@ -375,6 +514,13 @@ async def create_credential_bundle(
375
514
  source=source,
376
515
  access_mode=access_mode,
377
516
  lifecycle=lifecycle or {},
517
+ **_metadata_kwargs(
518
+ login_urls=login_urls,
519
+ uri_match_mode=uri_match_mode,
520
+ notes=notes,
521
+ non_secret_fields=non_secret_fields,
522
+ browser_fill_enabled=browser_fill_enabled,
523
+ ),
378
524
  created_by=actor_id,
379
525
  updated_by=actor_id,
380
526
  )
@@ -457,9 +603,16 @@ async def update_credential_item(
457
603
  status: str | None = None,
458
604
  provider_key: str | None = None,
459
605
  lifecycle: dict[str, Any] | None = None,
606
+ login_urls: list[str] | None = None,
607
+ uri_match_mode: str | None = None,
608
+ notes: Any = None,
609
+ clear_notes: bool = False,
610
+ non_secret_fields: list[dict[str, str]] | None = None,
611
+ browser_fill_enabled: bool | None = None,
460
612
  ) -> CredentialItemRow:
461
- """Patch item metadata. ``None`` fields are left alone. Values live on the
462
- field rows — this never touches ciphertext."""
613
+ """Patch item metadata. ``None`` fields are left alone (``clear_notes=True``
614
+ nulls the plaintext notes). Values live on the field rows — this never
615
+ touches ciphertext."""
463
616
  if status is not None and status not in ITEM_STATUSES:
464
617
  raise ValueError(f"status must be one of {ITEM_STATUSES}; got {status!r}")
465
618
  updates: dict[str, Any] = {"updated_by": actor_id, "updated_at": datetime.now(UTC)}
@@ -475,6 +628,17 @@ async def update_credential_item(
475
628
  updates["provider_key"] = provider_key
476
629
  if lifecycle is not None:
477
630
  updates["lifecycle"] = lifecycle
631
+ updates.update(
632
+ _metadata_kwargs(
633
+ login_urls=login_urls,
634
+ uri_match_mode=uri_match_mode,
635
+ notes=None if clear_notes else notes,
636
+ non_secret_fields=non_secret_fields,
637
+ browser_fill_enabled=browser_fill_enabled,
638
+ )
639
+ )
640
+ if clear_notes:
641
+ updates["notes"] = None
478
642
  result = await CredentialItem.update_where(
479
643
  {"id": item_id, "deleted_at__isnull": True}, **updates
480
644
  )
@@ -540,6 +704,11 @@ async def add_credential_field(
540
704
  the item so field-level scoping stays consistent)."""
541
705
  _validate_field_key(spec.field_key)
542
706
  _validate_handling(spec.handling)
707
+ _validate_sandbox_injectable(
708
+ field_key=spec.field_key,
709
+ env_key=spec.env_key,
710
+ inject_into_sandbox=spec.inject_into_sandbox,
711
+ )
543
712
  if not isinstance(spec.value, str):
544
713
  raise ValueError("value must be a string")
545
714
  raw = await _require_item_raw(item_id)
@@ -710,6 +879,19 @@ async def update_field_metadata(
710
879
  "a one-way door; rotate/replace the field instead of unsealing it"
711
880
  )
712
881
 
882
+ # The POST-patch state is what matters: enabling injection on a keyless
883
+ # field AND clearing the alias of an already-injecting field are the
884
+ # same silent breakage from opposite directions. Both are refused.
885
+ _validate_sandbox_injectable(
886
+ field_key=raw.get("field_key") or raw.get("key"),
887
+ env_key=(raw.get("key") if env_key is _UNSET else env_key),
888
+ inject_into_sandbox=(
889
+ bool(raw.get("inject_into_sandbox"))
890
+ if inject_into_sandbox is None
891
+ else inject_into_sandbox
892
+ ),
893
+ )
894
+
713
895
  updates: dict[str, Any] = {"updated_by": actor_id, "updated_at": datetime.now(UTC)}
714
896
  if inject_into_sandbox is not None:
715
897
  updates["inject_into_sandbox"] = inject_into_sandbox
@@ -1000,20 +1182,188 @@ async def list_item_grants(item_id: str) -> list[dict[str, Any]]:
1000
1182
  ]
1001
1183
 
1002
1184
 
1185
+ async def add_item_grant(
1186
+ item_id: str,
1187
+ *,
1188
+ user_id: str,
1189
+ can_use: bool = True,
1190
+ can_manage: bool = False,
1191
+ actor_id: str | None = None,
1192
+ ) -> dict[str, Any]:
1193
+ """Add (or refresh) ONE item-level grant without touching any other
1194
+ recipient — the explicit-operation replacement for the destructive
1195
+ ``set_item_grants`` overwrite. The HOST verifies the actor may share and
1196
+ that the recipient is legitimate first."""
1197
+ if not user_id:
1198
+ raise ValueError("grant user_id is required")
1199
+ 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
+ if existing:
1204
+ grant_id = str(existing[0]["id"])
1205
+ await UserSecretGrant.update_where(
1206
+ {"id": grant_id},
1207
+ can_use=can_use,
1208
+ can_manage=can_manage,
1209
+ granted_by=actor_id,
1210
+ )
1211
+ action = "grant_changed"
1212
+ else:
1213
+ instance = await UserSecretGrant.create(
1214
+ credential_item_id=item_id,
1215
+ user_secret_id=None,
1216
+ user_id=user_id,
1217
+ can_use=can_use,
1218
+ can_manage=can_manage,
1219
+ granted_by=actor_id,
1220
+ )
1221
+ grant_id = str(instance.id)
1222
+ action = "grant_added"
1223
+ await record_secret_audit(
1224
+ action=action,
1225
+ actor_id=actor_id,
1226
+ metadata={
1227
+ "credential_item_id": item_id,
1228
+ "grant_id": grant_id,
1229
+ "recipient_user_id": user_id,
1230
+ "can_use": can_use,
1231
+ "can_manage": can_manage,
1232
+ },
1233
+ )
1234
+ rows = await UserSecretGrant.filter(id=grant_id).values(
1235
+ "id", "user_id", "can_use", "can_manage", "granted_by", "created_at"
1236
+ )
1237
+ r = rows[0]
1238
+ return {
1239
+ "id": str(r["id"]),
1240
+ "user_id": str(r["user_id"]),
1241
+ "can_use": bool(r["can_use"]),
1242
+ "can_manage": bool(r["can_manage"]),
1243
+ "granted_by": str(r["granted_by"]) if r.get("granted_by") else None,
1244
+ "created_at": _iso(r["created_at"]),
1245
+ }
1246
+
1247
+
1248
+ async def update_item_grant(
1249
+ item_id: str,
1250
+ grant_id: str,
1251
+ *,
1252
+ can_use: bool | None = None,
1253
+ can_manage: bool | None = None,
1254
+ actor_id: str | None = None,
1255
+ ) -> dict[str, Any]:
1256
+ """Change ONE grant's capabilities. The grant must belong to the item
1257
+ (path binding — same rule as fields)."""
1258
+ rows = await UserSecretGrant.filter(id=grant_id, credential_item_id=item_id).values(
1259
+ "id", "user_id", "can_use", "can_manage"
1260
+ )
1261
+ if not rows:
1262
+ raise LookupError(f"grant {grant_id} not found")
1263
+ updates: dict[str, Any] = {}
1264
+ if can_use is not None:
1265
+ updates["can_use"] = can_use
1266
+ if can_manage is not None:
1267
+ updates["can_manage"] = can_manage
1268
+ if updates:
1269
+ await UserSecretGrant.update_where({"id": grant_id}, **updates)
1270
+ await record_secret_audit(
1271
+ action="grant_changed",
1272
+ actor_id=actor_id,
1273
+ metadata={
1274
+ "credential_item_id": item_id,
1275
+ "grant_id": grant_id,
1276
+ "recipient_user_id": str(rows[0]["user_id"]),
1277
+ "can_use": updates.get("can_use", bool(rows[0]["can_use"])),
1278
+ "can_manage": updates.get("can_manage", bool(rows[0]["can_manage"])),
1279
+ },
1280
+ )
1281
+ out = await UserSecretGrant.filter(id=grant_id).values(
1282
+ "id", "user_id", "can_use", "can_manage", "granted_by", "created_at"
1283
+ )
1284
+ r = out[0]
1285
+ return {
1286
+ "id": str(r["id"]),
1287
+ "user_id": str(r["user_id"]),
1288
+ "can_use": bool(r["can_use"]),
1289
+ "can_manage": bool(r["can_manage"]),
1290
+ "granted_by": str(r["granted_by"]) if r.get("granted_by") else None,
1291
+ "created_at": _iso(r["created_at"]),
1292
+ }
1293
+
1294
+
1295
+ async def remove_item_grant(
1296
+ item_id: str,
1297
+ grant_id: str,
1298
+ *,
1299
+ actor_id: str | None = None,
1300
+ ) -> None:
1301
+ """Revoke ONE grant. Effective immediately — the RLS grantee policies and
1302
+ every capability check key on the row's existence."""
1303
+ rows = await UserSecretGrant.filter(id=grant_id, credential_item_id=item_id).values(
1304
+ "id", "user_id"
1305
+ )
1306
+ if not rows:
1307
+ raise LookupError(f"grant {grant_id} not found")
1308
+ await UserSecretGrant.delete_where(id=grant_id)
1309
+ await record_secret_audit(
1310
+ action="revoked",
1311
+ actor_id=actor_id,
1312
+ metadata={
1313
+ "credential_item_id": item_id,
1314
+ "grant_id": grant_id,
1315
+ "recipient_user_id": str(rows[0]["user_id"]),
1316
+ },
1317
+ )
1318
+
1319
+
1320
+ async def list_granted_credential_items(user_id: str) -> list[CredentialItemRow]:
1321
+ """Items shared TO ``user_id`` through item-level grants (``can_use``),
1322
+ with masked fields — the backend twin of the frontend's grants-driven
1323
+ "Shared with me" query."""
1324
+ if not user_id:
1325
+ raise ValueError("user_id is required")
1326
+ grant_rows = await UserSecretGrant.filter(
1327
+ user_id=user_id, credential_item_id__isnull=False, can_use=True
1328
+ ).values("credential_item_id")
1329
+ item_ids = sorted({str(r["credential_item_id"]) for r in grant_rows})
1330
+ if not item_ids:
1331
+ return []
1332
+ rows = (
1333
+ await CredentialItem.filter(id__in=item_ids, deleted_at__isnull=True)
1334
+ .order_by("display_name")
1335
+ .values(*_ITEM_FIELDS)
1336
+ )
1337
+ if not rows:
1338
+ return []
1339
+ fields = await _fields_for_items([str(r["id"]) for r in rows])
1340
+ return [_item_row(r, fields.get(str(r["id"]), [])) for r in rows]
1341
+
1342
+
1003
1343
  async def transfer_credential_item(
1004
1344
  item_id: str,
1005
1345
  *,
1006
1346
  to_user_id: str | None = None,
1007
1347
  to_organization_id: str | None = None,
1008
1348
  actor_id: str | None = None,
1349
+ clear_grants: bool = True,
1009
1350
  ) -> CredentialItemRow:
1010
1351
  """Move ownership in ONE audited transaction — item and every field flip
1011
1352
  owner together, and every stable reference (item id, field ids, field
1012
- keys) survives unchanged. No value is copied or re-encrypted."""
1353
+ keys) survives unchanged. No value is copied or re-encrypted. Prior
1354
+ personal grants are CLEARED by default (a transfer is a clean handover,
1355
+ not a share), and both the old and the new owner get an audit entry."""
1013
1356
  _validate_owner(to_user_id, to_organization_id)
1014
1357
  raw = await _require_item_raw(item_id)
1358
+ from_user_id = str(raw["user_id"]) if raw.get("user_id") else None
1359
+ from_organization_id = str(raw["organization_id"]) if raw.get("organization_id") else None
1015
1360
  now = datetime.now(UTC)
1016
1361
  async with transaction(PACKAGE_DB_NAME):
1362
+ cleared = 0
1363
+ if clear_grants:
1364
+ grant_rows = await UserSecretGrant.filter(credential_item_id=item_id).values("id")
1365
+ cleared = len(grant_rows)
1366
+ await UserSecretGrant.delete_where(credential_item_id=item_id)
1017
1367
  await CredentialItem.update_where(
1018
1368
  {"id": item_id, "deleted_at__isnull": True},
1019
1369
  user_id=to_user_id,
@@ -1028,18 +1378,27 @@ async def transfer_credential_item(
1028
1378
  updated_by=actor_id,
1029
1379
  updated_at=now,
1030
1380
  )
1381
+ transfer_meta = {
1382
+ "credential_item_id": item_id,
1383
+ "from_user_id": from_user_id,
1384
+ "from_organization_id": from_organization_id,
1385
+ "to_user_id": to_user_id,
1386
+ "to_organization_id": to_organization_id,
1387
+ "grants_cleared": cleared,
1388
+ }
1031
1389
  await record_secret_audit(
1032
1390
  action="item_transferred",
1033
1391
  actor_id=actor_id,
1034
1392
  organization_id=to_organization_id,
1035
1393
  user_id=to_user_id,
1036
- metadata={
1037
- "credential_item_id": item_id,
1038
- "from_user_id": str(raw["user_id"]) if raw.get("user_id") else None,
1039
- "from_organization_id": str(raw["organization_id"])
1040
- if raw.get("organization_id")
1041
- else None,
1042
- },
1394
+ metadata=transfer_meta,
1395
+ )
1396
+ await record_secret_audit(
1397
+ action="item_transferred_out",
1398
+ actor_id=actor_id,
1399
+ organization_id=from_organization_id,
1400
+ user_id=from_user_id,
1401
+ metadata=transfer_meta,
1043
1402
  )
1044
1403
  item = await get_credential_item(item_id)
1045
1404
  if item is None: # pragma: no cover
@@ -1077,6 +1436,11 @@ async def fork_credential_item(
1077
1436
  source=raw["source"],
1078
1437
  access_mode="all_members",
1079
1438
  lifecycle={**dict(raw.get("lifecycle") or {}), "forked_from": item_id},
1439
+ login_urls=list(raw.get("login_urls") or []),
1440
+ uri_match_mode=str(raw.get("uri_match_mode") or "host"),
1441
+ notes=raw.get("notes"),
1442
+ non_secret_fields=list(raw.get("non_secret_fields") or []),
1443
+ browser_fill_enabled=bool(raw.get("browser_fill_enabled", False)),
1080
1444
  created_by=actor_id,
1081
1445
  updated_by=actor_id,
1082
1446
  )
@@ -1148,6 +1512,7 @@ __all__ = [
1148
1512
  "FieldNotEditableError",
1149
1513
  "FieldSpec",
1150
1514
  "ItemGrantSpec",
1515
+ "SandboxInjectionWithoutEnvKeyError",
1151
1516
  "SealedFieldError",
1152
1517
  "add_credential_field",
1153
1518
  "create_credential_bundle",