@wildo-ai/saas-backend-lib 1.1.12 → 1.1.13

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 (226) hide show
  1. package/dist/esm/base-classes/models/manual-controllers.backend.definition.d.ts.map +1 -1
  2. package/dist/esm/base-classes/models/manual-controllers.backend.definition.js +2 -0
  3. package/dist/esm/base-classes/models/manual-controllers.backend.definition.js.map +1 -1
  4. package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.d.ts +4 -0
  5. package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.d.ts.map +1 -1
  6. package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.js +20 -5
  7. package/dist/esm/base-classes/services/repositories/stored-field-omission.backend.js.map +1 -1
  8. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.d.ts +3 -0
  9. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.d.ts.map +1 -1
  10. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.js +10 -1
  11. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation-registry.backend.service.js.map +1 -1
  12. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.d.ts +22 -0
  13. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.d.ts.map +1 -1
  14. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.js +25 -1
  15. package/dist/esm/base-classes/services/services/resource-operation-custom-implementation.factory.backend.js.map +1 -1
  16. package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.d.ts +38 -3
  17. package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.d.ts.map +1 -1
  18. package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.js +68 -5
  19. package/dist/esm/base-classes/services/services/resource-operation-transaction-persistence.backend.utils.js.map +1 -1
  20. package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.d.ts +2 -2
  21. package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.d.ts.map +1 -1
  22. package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.js +6 -3
  23. package/dist/esm/base-classes/services/services/resource-status-transition.custom-impl.backend.service.js.map +1 -1
  24. package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.d.ts +83 -0
  25. package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.d.ts.map +1 -1
  26. package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.js +217 -14
  27. package/dist/esm/base-classes/services/services/services-registry-handler.backend.service.js.map +1 -1
  28. package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.d.ts +32 -0
  29. package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.d.ts.map +1 -0
  30. package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.js +36 -0
  31. package/dist/esm/compliance/approval-seals/approval-seal-currency.backend.js.map +1 -0
  32. package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.d.ts +38 -0
  33. package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.d.ts.map +1 -0
  34. package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.js +83 -0
  35. package/dist/esm/compliance/approval-seals/approval-seal-on-record.backend.js.map +1 -0
  36. package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.d.ts +36 -0
  37. package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.d.ts.map +1 -0
  38. package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.js +51 -0
  39. package/dist/esm/compliance/approval-seals/approval-seal-presentation-read.backend.js.map +1 -0
  40. package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.d.ts +46 -0
  41. package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.d.ts.map +1 -1
  42. package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.js +68 -0
  43. package/dist/esm/compliance/approval-seals/approval-seal-relay-store.backend.js.map +1 -1
  44. package/dist/esm/compliance/approval-seals/approval-seal-store.backend.d.ts +27 -0
  45. package/dist/esm/compliance/approval-seals/approval-seal-store.backend.d.ts.map +1 -1
  46. package/dist/esm/compliance/approval-seals/approval-seal-store.backend.js +42 -3
  47. package/dist/esm/compliance/approval-seals/approval-seal-store.backend.js.map +1 -1
  48. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.d.ts +26 -1
  49. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.d.ts.map +1 -1
  50. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.js +10 -0
  51. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.contracts.js.map +1 -1
  52. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.d.ts +10 -1
  53. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.d.ts.map +1 -1
  54. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.js +60 -28
  55. package/dist/esm/compliance/approval-seals/approval-seals-admin.backend.controller.js.map +1 -1
  56. package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.d.ts +90 -0
  57. package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.d.ts.map +1 -0
  58. package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.js +373 -0
  59. package/dist/esm/compliance/approval-seals/approval-seals-on-record.backend.controller.js.map +1 -0
  60. package/dist/esm/compliance/approval-seals/decision-presentation.backend.d.ts +29 -2
  61. package/dist/esm/compliance/approval-seals/decision-presentation.backend.d.ts.map +1 -1
  62. package/dist/esm/compliance/approval-seals/decision-presentation.backend.js +40 -3
  63. package/dist/esm/compliance/approval-seals/decision-presentation.backend.js.map +1 -1
  64. package/dist/esm/compliance/electronic-signatures/pdf-signature-structure.backend.d.ts.map +1 -1
  65. package/dist/esm/compliance/electronic-signatures/pdf-signature-structure.backend.js +34 -6
  66. package/dist/esm/compliance/electronic-signatures/pdf-signature-structure.backend.js.map +1 -1
  67. package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.d.ts +30 -6
  68. package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.d.ts.map +1 -1
  69. package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.js +67 -22
  70. package/dist/esm/compliance/electronic-signatures/signature-request-evidence-files.backend.js.map +1 -1
  71. package/dist/esm/compliance/electronic-signatures/signature-request-poll.batch.backend.service.d.ts.map +1 -1
  72. package/dist/esm/compliance/electronic-signatures/signature-request-poll.batch.backend.service.js +18 -1
  73. package/dist/esm/compliance/electronic-signatures/signature-request-poll.batch.backend.service.js.map +1 -1
  74. package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.d.ts +66 -1
  75. package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.d.ts.map +1 -1
  76. package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.js +161 -14
  77. package/dist/esm/compliance/electronic-signatures/signature-requests.backend.service.js.map +1 -1
  78. package/dist/esm/compliance/electronic-signatures/signed-document-validation.backend.d.ts +47 -0
  79. package/dist/esm/compliance/electronic-signatures/signed-document-validation.backend.d.ts.map +1 -1
  80. package/dist/esm/compliance/electronic-signatures/signed-document-validation.backend.js.map +1 -1
  81. package/dist/esm/external-providers/services/content-safety.backend.service.d.ts +27 -2
  82. package/dist/esm/external-providers/services/content-safety.backend.service.d.ts.map +1 -1
  83. package/dist/esm/external-providers/services/content-safety.backend.service.js +46 -1
  84. package/dist/esm/external-providers/services/content-safety.backend.service.js.map +1 -1
  85. package/dist/esm/external-providers/services/embeddings.backend.service.d.ts.map +1 -1
  86. package/dist/esm/external-providers/services/embeddings.backend.service.js +2 -0
  87. package/dist/esm/external-providers/services/embeddings.backend.service.js.map +1 -1
  88. package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts +11 -0
  89. package/dist/esm/external-providers/services/external-providers-registry.backend.service.d.ts.map +1 -1
  90. package/dist/esm/external-providers/services/external-providers-registry.backend.service.js +23 -0
  91. package/dist/esm/external-providers/services/external-providers-registry.backend.service.js.map +1 -1
  92. package/dist/esm/external-providers/services/provider-service-account-token.backend.d.ts +19 -1
  93. package/dist/esm/external-providers/services/provider-service-account-token.backend.d.ts.map +1 -1
  94. package/dist/esm/external-providers/services/provider-service-account-token.backend.js +41 -1
  95. package/dist/esm/external-providers/services/provider-service-account-token.backend.js.map +1 -1
  96. package/dist/esm/files/utils/file-parent-read-context.backend.d.ts +27 -2
  97. package/dist/esm/files/utils/file-parent-read-context.backend.d.ts.map +1 -1
  98. package/dist/esm/files/utils/file-parent-read-context.backend.js +23 -4
  99. package/dist/esm/files/utils/file-parent-read-context.backend.js.map +1 -1
  100. package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.d.ts +26 -0
  101. package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.d.ts.map +1 -1
  102. package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.js +100 -8
  103. package/dist/esm/flows-actors/content-guardrails/classifier-availability.backend.utils.js.map +1 -1
  104. package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.d.ts +3 -1
  105. package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.d.ts.map +1 -1
  106. package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.js +8 -2
  107. package/dist/esm/flows-actors/content-guardrails/record-owner-policy.backend.js.map +1 -1
  108. package/dist/esm/flows-actors/models/function-tools.backend.definitions.d.ts +43 -0
  109. package/dist/esm/flows-actors/models/function-tools.backend.definitions.d.ts.map +1 -1
  110. package/dist/esm/flows-actors/models/function-tools.backend.definitions.js +37 -0
  111. package/dist/esm/flows-actors/models/function-tools.backend.definitions.js.map +1 -1
  112. package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.d.ts +7 -0
  113. package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.d.ts.map +1 -1
  114. package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.js +8 -3
  115. package/dist/esm/flows-actors/services/agent-work-decisions.backend.service.js.map +1 -1
  116. package/dist/esm/flows-actors/services/agents.backend.service.d.ts +31 -6
  117. package/dist/esm/flows-actors/services/agents.backend.service.d.ts.map +1 -1
  118. package/dist/esm/flows-actors/services/agents.backend.service.js +78 -30
  119. package/dist/esm/flows-actors/services/agents.backend.service.js.map +1 -1
  120. package/dist/esm/flows-actors/services/autonomous-agent-turn.backend.service.d.ts.map +1 -1
  121. package/dist/esm/flows-actors/services/autonomous-agent-turn.backend.service.js +4 -0
  122. package/dist/esm/flows-actors/services/autonomous-agent-turn.backend.service.js.map +1 -1
  123. package/dist/esm/flows-actors/services/conversational-execution.backend.service.d.ts +28 -2
  124. package/dist/esm/flows-actors/services/conversational-execution.backend.service.d.ts.map +1 -1
  125. package/dist/esm/flows-actors/services/conversational-execution.backend.service.js +132 -22
  126. package/dist/esm/flows-actors/services/conversational-execution.backend.service.js.map +1 -1
  127. package/dist/esm/flows-actors/services/flow-actor-executions.custom-impl.backend.service.d.ts.map +1 -1
  128. package/dist/esm/flows-actors/services/flow-actor-executions.custom-impl.backend.service.js +12 -8
  129. package/dist/esm/flows-actors/services/flow-actor-executions.custom-impl.backend.service.js.map +1 -1
  130. package/dist/esm/flows-actors/services/flows-actors-registry.service.d.ts +5 -0
  131. package/dist/esm/flows-actors/services/flows-actors-registry.service.d.ts.map +1 -1
  132. package/dist/esm/flows-actors/services/flows-actors-registry.service.js +18 -2
  133. package/dist/esm/flows-actors/services/flows-actors-registry.service.js.map +1 -1
  134. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts +33 -0
  135. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.d.ts.map +1 -1
  136. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js +58 -0
  137. package/dist/esm/flows-actors/utils/acp-session-runner.backend.utils.js.map +1 -1
  138. package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.d.ts +12 -1
  139. package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.d.ts.map +1 -1
  140. package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.js +23 -4
  141. package/dist/esm/inbound/services/inbound-delivery-dispatch.backend.service.js.map +1 -1
  142. package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.d.ts +12 -1
  143. package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.d.ts.map +1 -1
  144. package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.js +55 -17
  145. package/dist/esm/inbound/services/inbound-delivery-receiver.backend.service.js.map +1 -1
  146. package/dist/esm/inbound/services/inbound-delivery-store.backend.service.d.ts +22 -1
  147. package/dist/esm/inbound/services/inbound-delivery-store.backend.service.d.ts.map +1 -1
  148. package/dist/esm/inbound/services/inbound-delivery-store.backend.service.js +52 -9
  149. package/dist/esm/inbound/services/inbound-delivery-store.backend.service.js.map +1 -1
  150. package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.d.ts +79 -0
  151. package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.d.ts.map +1 -0
  152. package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.js +211 -0
  153. package/dist/esm/inbound/services/inbound-delivery-stored-body.backend.service.js.map +1 -0
  154. package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.d.ts +2 -2
  155. package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.d.ts.map +1 -1
  156. package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.js +9 -1
  157. package/dist/esm/inbound/services/inbound-delivery-subject-erasure-severance.backend.js.map +1 -1
  158. package/dist/esm/index.d.ts +1 -0
  159. package/dist/esm/index.d.ts.map +1 -1
  160. package/dist/esm/index.js +1 -0
  161. package/dist/esm/index.js.map +1 -1
  162. package/dist/esm/inject/models/container.d.ts.map +1 -1
  163. package/dist/esm/inject/models/container.js +6 -0
  164. package/dist/esm/inject/models/container.js.map +1 -1
  165. package/dist/esm/inject/models/service-types.d.ts +5 -0
  166. package/dist/esm/inject/models/service-types.d.ts.map +1 -1
  167. package/dist/esm/inject/models/service-types.js +3 -0
  168. package/dist/esm/inject/models/service-types.js.map +1 -1
  169. package/dist/esm/monitoring/observability/models/observability.backend.schemas.d.ts +5 -0
  170. package/dist/esm/monitoring/observability/models/observability.backend.schemas.d.ts.map +1 -1
  171. package/dist/esm/monitoring/observability/models/observability.backend.schemas.js.map +1 -1
  172. package/dist/esm/monitoring/observability/services/metrics.service.d.ts +34 -1
  173. package/dist/esm/monitoring/observability/services/metrics.service.d.ts.map +1 -1
  174. package/dist/esm/monitoring/observability/services/metrics.service.js +49 -0
  175. package/dist/esm/monitoring/observability/services/metrics.service.js.map +1 -1
  176. package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.d.ts +6 -1
  177. package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.d.ts.map +1 -1
  178. package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.js +15 -4
  179. package/dist/esm/notifications/webhooks/controllers/webhook-auto-controller.backend.service.js.map +1 -1
  180. package/dist/esm/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.custom-impl.backend.service.d.ts.map +1 -1
  181. package/dist/esm/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.custom-impl.backend.service.js +3 -1
  182. package/dist/esm/notifications/webhooks/outbound/services/m2m-webhook-delivery-log.custom-impl.backend.service.js.map +1 -1
  183. package/dist/esm/runtime/services/http-server.backend.service.d.ts +3 -1
  184. package/dist/esm/runtime/services/http-server.backend.service.d.ts.map +1 -1
  185. package/dist/esm/runtime/services/http-server.backend.service.js +11 -3
  186. package/dist/esm/runtime/services/http-server.backend.service.js.map +1 -1
  187. package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.d.ts +22 -0
  188. package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.d.ts.map +1 -1
  189. package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.js +54 -0
  190. package/dist/esm/runtime/services/utils/inbound-endpoint-raw-body.backend.utils.js.map +1 -1
  191. package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.d.ts +51 -0
  192. package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.d.ts.map +1 -1
  193. package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.js +87 -15
  194. package/dist/esm/security/authentications/services/auth-methods/auth-password-reset.backend.service.js.map +1 -1
  195. package/dist/esm/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.d.ts.map +1 -1
  196. package/dist/esm/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.js +5 -2
  197. package/dist/esm/security/authentications/services/custom-impls/siem-delivery-dlq.custom-impl.backend.service.js.map +1 -1
  198. package/dist/esm/security/authentications/services/custom-impls/sso-config-domain-verification.custom-impl.backend.service.d.ts.map +1 -1
  199. package/dist/esm/security/authentications/services/custom-impls/sso-config-domain-verification.custom-impl.backend.service.js +65 -24
  200. package/dist/esm/security/authentications/services/custom-impls/sso-config-domain-verification.custom-impl.backend.service.js.map +1 -1
  201. package/dist/esm/security/authorizations/super-admin-floor.backend.utils.d.ts +64 -1
  202. package/dist/esm/security/authorizations/super-admin-floor.backend.utils.d.ts.map +1 -1
  203. package/dist/esm/security/authorizations/super-admin-floor.backend.utils.js +84 -2
  204. package/dist/esm/security/authorizations/super-admin-floor.backend.utils.js.map +1 -1
  205. package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.d.ts +14 -0
  206. package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.d.ts.map +1 -1
  207. package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.js +13 -5
  208. package/dist/esm/security/oauth-clients/oauth-clients.custom-impl.backend.service.js.map +1 -1
  209. package/dist/esm/users/services/anonymous-consent.custom-impl.backend.service.d.ts.map +1 -1
  210. package/dist/esm/users/services/anonymous-consent.custom-impl.backend.service.js +6 -2
  211. package/dist/esm/users/services/anonymous-consent.custom-impl.backend.service.js.map +1 -1
  212. package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.d.ts +7 -0
  213. package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.d.ts.map +1 -1
  214. package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.js +65 -20
  215. package/dist/esm/users/services/user-force-password-reset.custom-impl.backend.service.js.map +1 -1
  216. package/dist/esm/users/services/user-role-administration.custom-impl.backend.service.d.ts.map +1 -1
  217. package/dist/esm/users/services/user-role-administration.custom-impl.backend.service.js +11 -4
  218. package/dist/esm/users/services/user-role-administration.custom-impl.backend.service.js.map +1 -1
  219. package/dist/esm/users/services/user-self-custom-implementations.backend.d.ts.map +1 -1
  220. package/dist/esm/users/services/user-self-custom-implementations.backend.js +4 -2
  221. package/dist/esm/users/services/user-self-custom-implementations.backend.js.map +1 -1
  222. package/dist/esm/users/services/user-super-admin-floor.custom-impl.backend.service.d.ts.map +1 -1
  223. package/dist/esm/users/services/user-super-admin-floor.custom-impl.backend.service.js +16 -0
  224. package/dist/esm/users/services/user-super-admin-floor.custom-impl.backend.service.js.map +1 -1
  225. package/dist/tsconfig.build.tsbuildinfo +1 -1
  226. package/package.json +10 -10
@@ -33,6 +33,8 @@
33
33
  */
34
34
  import { CoreResourceType, ErrorType, ErrorCustomMessageReference, ResourceOperationVariantType, Users_Operations, } from '@wildo-ai/saas-models';
35
35
  import { createResourceCustomServiceImplementation, ResourceCustomServiceImplementationHookTransactionPosture, } from '../../base-classes/services/services/resource-operation-custom-implementation.factory.backend.js';
36
+ import { ResourceOperationBeforeTransactionalCoreDisposition } from '../../base-classes/services/services/resource-operation-custom-implementation-base.backend.service.js';
37
+ import { BackendResourceType } from '../../resources/models/resources-types.backend.schemas.js';
36
38
  import { SAAS_SERVICE_TYPES } from '../../inject/models/service-types.js';
37
39
  import { describeUnaddressableShape, resolveSingleSubjectId, } from '../../base-classes/utils/single-subject-addressing.backend.utils.js';
38
40
  /**
@@ -105,6 +107,25 @@ function resolveValidatedForcedResetRequest(_id, input, executionContext, utils)
105
107
  }
106
108
  return { subjectUserId, actorUserId, reason };
107
109
  }
110
+ /**
111
+ * The END-USER frontend's public URL, or a CONFIGURATION refusal. Asked in `beforeTransactionalCore` too, so an
112
+ * environment that could never build the link refuses BEFORE the sessions are revoked rather than after.
113
+ *
114
+ * The END-USER frontend, never the admin console the caller is sitting in: the link is consumed by the account owner.
115
+ * Same resolution the notification dispatcher uses for token-bearing emails.
116
+ */
117
+ function resolveForcedResetFrontendUrl(container, executionContext, utils) {
118
+ const frontendUrl = container.get(SAAS_SERVICE_TYPES.AppConfigService).resolveAppPublicBaseUrl();
119
+ if (!frontendUrl) {
120
+ throw utils.errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {
121
+ context: {
122
+ message: 'Cannot resolve the default frontend public URL required to build the password-reset link. '
123
+ + 'Configure `runtime.endPoints.<default frontend>.publicUrl` for this environment.',
124
+ },
125
+ });
126
+ }
127
+ return frontendUrl;
128
+ }
108
129
  /**
109
130
  * FORCE_PASSWORD_RESET — mint a single-use reset token for the target, mail it to the TARGET,
110
131
  * evict their sessions, and record who forced it and why.
@@ -130,6 +151,13 @@ function resolveValidatedForcedResetRequest(_id, input, executionContext, utils)
130
151
  *
131
152
  * The validation is re-derived in REPLACE rather than carried over. It is pure, so this costs
132
153
  * nothing, and it keeps REPLACE correct on its own terms instead of trusting that PREFIX ran.
154
+ *
155
+ * # And why the revocation runs in `beforeTransactionalCore` (#2297)
156
+ *
157
+ * The operation is transactional, so its audit row is kept with the commit. The session revocation is a Redis write no
158
+ * rollback can undo, and it must still precede the token (a failed revocation mints nothing), so it runs in the phase
159
+ * that runs once, OUTSIDE the transaction and before it: the same validation first, then the revocation. Only the token
160
+ * write runs inside; the email and the forced-reset record run after the commit.
133
161
  */
134
162
  export function createUserForcePasswordResetFactories() {
135
163
  const factory = (_resourcesRegistryService, container) => createResourceCustomServiceImplementation({
@@ -141,37 +169,54 @@ export function createUserForcePasswordResetFactories() {
141
169
  },
142
170
  debugLabel: 'users.force_password_reset.credential-reset',
143
171
  /*
144
- * #2297: stays outside a transaction, for now. Its REPLACE revokes the user's sessions in Redis, mints a reset token
145
- * through a door it does not pass the transaction to, emails the raw token, and awaits its audit row so a failed
146
- * record fails the operation. To join, the token write must take utils.serviceOptions and the session revocation,
147
- * the email and the record move after the commit (which ends the record's fail-the-operation posture: a decision).
172
+ * #2297: split so its database part runs in the operation's transaction and its effects outside the database do not.
173
+ * - `beforeTransactionalCore` (outside any transaction): every refusal, then the account lookup and the session
174
+ * revocation, in that order, exactly as before. A refusal or a failed revocation still mints nothing, and the Redis
175
+ * revocation is an effect no rollback could undo, so it never runs inside the transaction.
176
+ * - REPLACE (inside): the reset token is written through `utils.serviceOptions`, so it commits with the attempt and a
177
+ * rolled-back attempt leaves no live link. The email (it carries the token's raw value, so it is never kept in an
178
+ * outbox) and the `USER_PASSWORD_RESET_FORCED` record (a capturable recording) run after the commit.
179
+ * The framework's own `RESOURCE_OPERATION_PERFORMED` row is then kept with the commit like every transactional
180
+ * write's. The consumable-token store is declared a participant, so startup proves it shares the users adapter.
148
181
  */
149
- hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.STAYS_OUTSIDE_TRANSACTION,
182
+ transactionalCore: true,
183
+ transactionParticipants: { consumableToken: BackendResourceType.CONSUMABLE_TOKENS },
184
+ hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,
150
185
  handlers: {
151
- // PREFIX owns every refusal — it is the only phase that runs BEFORE the core write, so it is
152
- // the only phase where throwing leaves the row untouched. Returns `input` unchanged: this
153
- // operation contributes no field patch.
186
+ // Every refusal, then the session revocation, before the transaction opens (see above). The revoked account
187
+ // rides to REPLACE as the phase's frozen continuation.
188
+ beforeTransactionalCore: async (_id, input, executionContext, _operationPath, utils) => {
189
+ const { subjectUserId } = resolveValidatedForcedResetRequest(_id, input, executionContext, utils);
190
+ resolveForcedResetFrontendUrl(container, executionContext, utils);
191
+ const passwordResetService = container.get(SAAS_SERVICE_TYPES.AuthPasswordResetService);
192
+ const revokedUser = await passwordResetService.revokeAccessForForcedPasswordReset(executionContext, { targetUserId: subjectUserId });
193
+ return {
194
+ disposition: ResourceOperationBeforeTransactionalCoreDisposition.CONTINUE,
195
+ coreInputPatch: {},
196
+ continuation: { revokedUser },
197
+ };
198
+ },
199
+ // PREFIX owns every refusal inside the core phase — it is the only core phase that runs BEFORE the core write,
200
+ // so it is the only one where throwing leaves the row untouched. Returns `input` unchanged: this operation
201
+ // contributes no field patch.
154
202
  prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {
155
203
  resolveValidatedForcedResetRequest(_id, input, executionContext, utils);
156
204
  return input;
157
205
  },
158
206
  replaceCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {
159
207
  const { subjectUserId, actorUserId, reason } = resolveValidatedForcedResetRequest(_id, input, executionContext, utils);
160
- const appConfigService = container.get(SAAS_SERVICE_TYPES.AppConfigService);
161
- // The END-USER frontend, never the admin console the caller is sitting in: the link is
162
- // consumed by the account owner. Same resolution the notification dispatcher uses for
163
- // token-bearing emails.
164
- const frontendUrl = appConfigService.resolveAppPublicBaseUrl();
165
- if (!frontendUrl) {
166
- throw utils.errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {
167
- context: {
168
- message: 'Cannot resolve the default frontend public URL required to build the password-reset link. '
169
- + 'Configure `runtime.endPoints.<default frontend>.publicUrl` for this environment.',
170
- },
208
+ const continuation = utils.beforeTransactionalCore?.continuation;
209
+ // The account whose sessions were revoked before the transaction opened, and only that one: a REPLACE reached
210
+ // without the revocation (or for another subject) must not mint a link for an account whose sessions still live.
211
+ if (!continuation?.revokedUser || continuation.revokedUser._id !== subjectUserId) {
212
+ throw utils.errorBuilder.buildError(ErrorType.INTERNAL, executionContext, {
213
+ context: { reason: 'force_password_reset reached its token write without the session revocation for this subject' },
171
214
  });
172
215
  }
216
+ const frontendUrl = resolveForcedResetFrontendUrl(container, executionContext, utils);
173
217
  const passwordResetService = container.get(SAAS_SERVICE_TYPES.AuthPasswordResetService);
174
- const { expiresAt, sessionsRevoked } = await passwordResetService.forcePasswordResetByAdministrator(executionContext, { targetUserId: subjectUserId, actorUserId, reason, frontendUrl });
218
+ const { expiresAt } = await passwordResetService.issueForcedPasswordReset(executionContext, { user: continuation.revokedUser, actorUserId, reason, frontendUrl }, utils.serviceOptions);
219
+ const sessionsRevoked = true;
175
220
  // Deliberately NOT the token or the reset URL. Handing the caller a link that sets
176
221
  // someone else's password is an account-takeover primitive: they could complete it and
177
222
  // then authenticate as that user, unnoticed by the owner and — with no actor claim on a
@@ -1 +1 @@
1
- {"version":3,"file":"user-force-password-reset.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/users/services/user-force-password-reset.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `users | force_password_reset` — administrator-compelled credential reset.\n *\n * # What this replaced\n *\n * The operation was ROUTED but never implemented. It declared\n * `{ reason, notifyUser, resetExpirationHours }` and promised\n * `{ resetToken, resetUrl, expiresAt }`, but nothing minted a token: `resourceOperationLike:\n * UPDATE` fell through to the generic core write, which persists only fields the resource schema\n * declares. None of the three is a `users` schema path, so the operation answered **HTTP 200**\n * having done nothing at all — while telling the operator a reset had been forced. Caught by the\n * 2026-08-01 routed-unimplemented sweep; see\n * the 2026-08-01 routed-but-unimplemented operation sweep (#1275).\n *\n * # Why a custom implementation rather than declarative `tokenGeneration`\n *\n * A declarative `tokenGeneration` block (the `organizationMembers | resend` shape) mints and\n * mails a token with no code, and it WOULD satisfy the startup coverage guard — `tokenGeneration`\n * alone flips the verdict to SERVICEABLE. It was rejected because it cannot do two things this\n * verb exists for:\n *\n * - **revoke the target's live sessions.** The operation's own `whenToUse` names suspected\n * compromise. Minting a reset link while the attacker's session keeps working makes the 200\n * protective in name only.\n * - **emit an actor-attributed audit event.** Declarative token generation emits none, and the\n * pre-existing `USER_PASSWORD_RESET_REQUESTED` event carries no actor field — routing an admin\n * action through it would record the account owner as the person who acted.\n *\n * Taking the declarative route would therefore have bought a green guard over a contract that\n * still lies in both directions. The real work lives in\n * `AuthPasswordResetBackendService.forcePasswordResetByAdministrator`, which owns credential\n * semantics; this file is the thin resource-operation entry point onto it.\n */\n\nimport {\n CoreResourceType,\n ErrorType,\n ErrorCustomMessageReference,\n ResourceOperationVariantType,\n Users_Operations,\n} from '@wildo-ai/saas-models';\nimport type { Container } from 'inversify';\nimport {\n createResourceCustomServiceImplementation,\n ResourceCustomServiceImplementationHookTransactionPosture,\n type ResourceCustomServiceImplementationFactory,\n} from '~wildo-backend-lib/base-classes/services/services/resource-operation-custom-implementation.factory.backend';\nimport type { ResourcesRegistryBackendService } from '~wildo-backend-lib/resources/services/resources-registry.backend.service';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport {\n describeUnaddressableShape,\n resolveSingleSubjectId,\n} from '~wildo-backend-lib/base-classes/utils/single-subject-addressing.backend.utils';\nimport type { AppConfigurationService } from '~wildo-backend-lib/config/services/app-configuration.backend.service';\nimport type { AuthPasswordResetBackendService } from '~wildo-backend-lib/security/authentications/services/auth-methods/auth-password-reset.backend.service';\n\n/**\n * Identify the acting principal for the audit record.\n *\n * A human session yields the user id. A machine principal (a super-admin API key or OAuth\n * client) yields its credential provenance, which is the strongest attribution available for\n * that caller class. Returning `null` when NEITHER exists is deliberate: this operation is\n * gated on `APP_ADMIN_SUPER_ADMIN`, so an execution with no attributable principal at all is\n * not a caller shape this verb should serve, and proceeding would write an audit record that\n * cannot answer \"who did this\" — the single question the record exists for.\n */\nfunction resolveActorReference(executionContext: ExecutionContext<any>): string | null {\n const userId = executionContext.initiatorIds?.userId;\n if (userId) return userId;\n\n const machineCredential = executionContext.initiatorIds?.machineCredential;\n if (machineCredential) {\n return `${machineCredential.authMethod}:${machineCredential.credentialId}`;\n }\n\n return null;\n}\n\n/**\n * The request this operation will act on, once every precondition holds.\n *\n * Deriving it is PURE and idempotent so both handler phases can call it: the PREFIX phase to refuse\n * before anything is written, the REPLACE phase to obtain the same values without threading state\n * between them. There is no secret to carry across, so a per-execution bridge (the `WeakMap` the\n * api-key mint uses for its one-time plaintext) would be machinery without a reason.\n */\ninterface ValidatedForcedResetRequest {\n subjectUserId: string;\n actorUserId: string;\n reason: string;\n}\n\n/**\n * Resolve and validate the whole request, or throw.\n *\n * Every refusal this operation can raise lives here, so it can be run in the PREFIX phase where a\n * throw is genuinely free of side effects. See the phase note on the factory below for why that\n * matters more than it looks.\n */\nfunction resolveValidatedForcedResetRequest(\n _id: unknown,\n input: unknown,\n executionContext: ExecutionContext<any>,\n utils: { errorBuilder: { buildError: (type: ErrorType, ec?: unknown, opts?: { customMessageReference?: ErrorCustomMessageReference; context?: Record<string, unknown> }) => unknown } },\n): ValidatedForcedResetRequest {\n // Fail closed on anything but a single addressed user: a forced credential reset must name\n // exactly one account. A population-addressed request reaching this path would otherwise\n // reset a row the caller did not select.\n const subjectUserId = resolveSingleSubjectId(_id as never);\n if (subjectUserId === undefined) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: {\n reason: 'force_password_reset requires a single addressed user id',\n receivedAddressing: describeUnaddressableShape(_id as never),\n },\n });\n }\n\n const actorUserId = resolveActorReference(executionContext);\n if (!actorUserId) {\n throw utils.errorBuilder.buildError(ErrorType.AUTHORIZATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.AUTHORIZATION,\n context: { reason: 'force_password_reset requires an attributable principal' },\n });\n }\n\n // Self-service exists at `/auth/password/change`, which proves knowledge of the current\n // password. Letting an administrator route their OWN reset through the privileged verb\n // would bypass that proof using nothing but their admin role.\n //\n // Compared against the RESOLVED subject, never the raw addressing: a `{ _id: <self> }` filter\n // is the same self-reset, and testing the raw value would silently miss it now that the guard\n // above admits addressing shapes other than a bare string.\n if (subjectUserId === executionContext.initiatorIds?.userId) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: { reason: 'Use the self-service password change route to reset your own credential' },\n });\n }\n\n const { reason } = (input ?? {}) as { reason?: string };\n if (!reason) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: { reason: 'force_password_reset requires a documented reason' },\n });\n }\n\n return { subjectUserId, actorUserId, reason };\n}\n\n/**\n * FORCE_PASSWORD_RESET — mint a single-use reset token for the target, mail it to the TARGET,\n * evict their sessions, and record who forced it and why.\n *\n * # Why the guards run in PREFIX and the work in REPLACE\n *\n * `replaceCoreOperations` runs AFTER `executeCorePersistenceOperation` — the core write has\n * already happened by the time a REPLACE handler can throw. Every refusal therefore has to live in\n * the PREFIX phase, which runs before it.\n *\n * This is not theoretical. With all four guards in REPLACE, a SELF-addressed call was refused with\n * `400 service_resource_validation_failed` and STILL wrote the caller's own user row — bumping it\n * enough to invalidate their session, so the very next request from that administrator answered\n * `401 authentication_jwt_token_failed`. A refusal that silently logs you out is the same class of\n * defect this whole operation was written to remove: an action that does something it does not say\n * it does. Caught by `examples/wonder-todos/e2e/credential-administration.e2e.ts` on 2026-08-05 and\n * isolated by replaying the two self-addressed calls against `users.updatedAt`.\n *\n * REPLACE still owns the WORK (and the response shape): the verb's whole effect lives in the\n * credential and token stores, not in a field patch, and the operation must answer with\n * `{ expiresAt, sessionsRevoked }` rather than the user row. Mirrors `applicationApiKeys |\n * usage_stats` for the response half.\n *\n * The validation is re-derived in REPLACE rather than carried over. It is pure, so this costs\n * nothing, and it keeps REPLACE correct on its own terms instead of trusting that PREFIX ran.\n */\nexport function createUserForcePasswordResetFactories(): ResourceCustomServiceImplementationFactory[] {\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.USERS,\n operationIdentifier: Users_Operations.FORCE_PASSWORD_RESET,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n debugLabel: 'users.force_password_reset.credential-reset',\n /*\n * #2297: stays outside a transaction, for now. Its REPLACE revokes the user's sessions in Redis, mints a reset token\n * through a door it does not pass the transaction to, emails the raw token, and awaits its audit row so a failed\n * record fails the operation. To join, the token write must take utils.serviceOptions and the session revocation,\n * the email and the record move after the commit (which ends the record's fail-the-operation posture: a decision).\n */\n hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.STAYS_OUTSIDE_TRANSACTION,\n handlers: {\n // PREFIX owns every refusal — it is the only phase that runs BEFORE the core write, so it is\n // the only phase where throwing leaves the row untouched. Returns `input` unchanged: this\n // operation contributes no field patch.\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n resolveValidatedForcedResetRequest(_id, input, executionContext, utils);\n return input;\n },\n replaceCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n const { subjectUserId, actorUserId, reason } =\n resolveValidatedForcedResetRequest(_id, input, executionContext, utils);\n\n const appConfigService = container.get<AppConfigurationService>(SAAS_SERVICE_TYPES.AppConfigService);\n // The END-USER frontend, never the admin console the caller is sitting in: the link is\n // consumed by the account owner. Same resolution the notification dispatcher uses for\n // token-bearing emails.\n const frontendUrl = appConfigService.resolveAppPublicBaseUrl();\n if (!frontendUrl) {\n throw utils.errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n context: {\n message:\n 'Cannot resolve the default frontend public URL required to build the password-reset link. '\n + 'Configure `runtime.endPoints.<default frontend>.publicUrl` for this environment.',\n },\n });\n }\n\n const passwordResetService = container.get<AuthPasswordResetBackendService>(\n SAAS_SERVICE_TYPES.AuthPasswordResetService,\n );\n const { expiresAt, sessionsRevoked } = await passwordResetService.forcePasswordResetByAdministrator(\n executionContext,\n { targetUserId: subjectUserId, actorUserId, reason, frontendUrl },\n );\n\n // Deliberately NOT the token or the reset URL. Handing the caller a link that sets\n // someone else's password is an account-takeover primitive: they could complete it and\n // then authenticate as that user, unnoticed by the owner and — with no actor claim on a\n // session JWT — indistinguishable downstream from a genuine login. The link goes to the\n // account owner's inbox and nowhere else.\n return { expiresAt, sessionsRevoked };\n },\n },\n });\n\n return [factory as unknown as ResourceCustomServiceImplementationFactory];\n}\n"]}
1
+ {"version":3,"file":"user-force-password-reset.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/users/services/user-force-password-reset.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `users | force_password_reset` — administrator-compelled credential reset.\n *\n * # What this replaced\n *\n * The operation was ROUTED but never implemented. It declared\n * `{ reason, notifyUser, resetExpirationHours }` and promised\n * `{ resetToken, resetUrl, expiresAt }`, but nothing minted a token: `resourceOperationLike:\n * UPDATE` fell through to the generic core write, which persists only fields the resource schema\n * declares. None of the three is a `users` schema path, so the operation answered **HTTP 200**\n * having done nothing at all — while telling the operator a reset had been forced. Caught by the\n * 2026-08-01 routed-unimplemented sweep; see\n * the 2026-08-01 routed-but-unimplemented operation sweep (#1275).\n *\n * # Why a custom implementation rather than declarative `tokenGeneration`\n *\n * A declarative `tokenGeneration` block (the `organizationMembers | resend` shape) mints and\n * mails a token with no code, and it WOULD satisfy the startup coverage guard — `tokenGeneration`\n * alone flips the verdict to SERVICEABLE. It was rejected because it cannot do two things this\n * verb exists for:\n *\n * - **revoke the target's live sessions.** The operation's own `whenToUse` names suspected\n * compromise. Minting a reset link while the attacker's session keeps working makes the 200\n * protective in name only.\n * - **emit an actor-attributed audit event.** Declarative token generation emits none, and the\n * pre-existing `USER_PASSWORD_RESET_REQUESTED` event carries no actor field — routing an admin\n * action through it would record the account owner as the person who acted.\n *\n * Taking the declarative route would therefore have bought a green guard over a contract that\n * still lies in both directions. The real work lives in\n * `AuthPasswordResetBackendService.forcePasswordResetByAdministrator`, which owns credential\n * semantics; this file is the thin resource-operation entry point onto it.\n */\n\nimport {\n CoreResourceType,\n ErrorType,\n ErrorCustomMessageReference,\n ResourceOperationVariantType,\n Users_Operations,\n} from '@wildo-ai/saas-models';\nimport type { Container } from 'inversify';\nimport {\n createResourceCustomServiceImplementation,\n ResourceCustomServiceImplementationHookTransactionPosture,\n type ResourceCustomServiceImplementationFactory,\n} from '~wildo-backend-lib/base-classes/services/services/resource-operation-custom-implementation.factory.backend';\nimport { ResourceOperationBeforeTransactionalCoreDisposition } from '~wildo-backend-lib/base-classes/services/services/resource-operation-custom-implementation-base.backend.service';\nimport { BackendResourceType } from '~wildo-backend-lib/resources/models/resources-types.backend.schemas';\nimport type { ResourcesRegistryBackendService } from '~wildo-backend-lib/resources/services/resources-registry.backend.service';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport {\n describeUnaddressableShape,\n resolveSingleSubjectId,\n} from '~wildo-backend-lib/base-classes/utils/single-subject-addressing.backend.utils';\nimport type { AppConfigurationService } from '~wildo-backend-lib/config/services/app-configuration.backend.service';\nimport type { AuthPasswordResetBackendService } from '~wildo-backend-lib/security/authentications/services/auth-methods/auth-password-reset.backend.service';\n\n/**\n * Identify the acting principal for the audit record.\n *\n * A human session yields the user id. A machine principal (a super-admin API key or OAuth\n * client) yields its credential provenance, which is the strongest attribution available for\n * that caller class. Returning `null` when NEITHER exists is deliberate: this operation is\n * gated on `APP_ADMIN_SUPER_ADMIN`, so an execution with no attributable principal at all is\n * not a caller shape this verb should serve, and proceeding would write an audit record that\n * cannot answer \"who did this\" — the single question the record exists for.\n */\nfunction resolveActorReference(executionContext: ExecutionContext<any>): string | null {\n const userId = executionContext.initiatorIds?.userId;\n if (userId) return userId;\n\n const machineCredential = executionContext.initiatorIds?.machineCredential;\n if (machineCredential) {\n return `${machineCredential.authMethod}:${machineCredential.credentialId}`;\n }\n\n return null;\n}\n\n/**\n * The request this operation will act on, once every precondition holds.\n *\n * Deriving it is PURE and idempotent so both handler phases can call it: the PREFIX phase to refuse\n * before anything is written, the REPLACE phase to obtain the same values without threading state\n * between them. There is no secret to carry across, so a per-execution bridge (the `WeakMap` the\n * api-key mint uses for its one-time plaintext) would be machinery without a reason.\n */\ninterface ValidatedForcedResetRequest {\n subjectUserId: string;\n actorUserId: string;\n reason: string;\n}\n\n/**\n * Resolve and validate the whole request, or throw.\n *\n * Every refusal this operation can raise lives here, so it can be run in the PREFIX phase where a\n * throw is genuinely free of side effects. See the phase note on the factory below for why that\n * matters more than it looks.\n */\nfunction resolveValidatedForcedResetRequest(\n _id: unknown,\n input: unknown,\n executionContext: ExecutionContext<any>,\n utils: { errorBuilder: { buildError: (type: ErrorType, ec?: unknown, opts?: { customMessageReference?: ErrorCustomMessageReference; context?: Record<string, unknown> }) => unknown } },\n): ValidatedForcedResetRequest {\n // Fail closed on anything but a single addressed user: a forced credential reset must name\n // exactly one account. A population-addressed request reaching this path would otherwise\n // reset a row the caller did not select.\n const subjectUserId = resolveSingleSubjectId(_id as never);\n if (subjectUserId === undefined) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: {\n reason: 'force_password_reset requires a single addressed user id',\n receivedAddressing: describeUnaddressableShape(_id as never),\n },\n });\n }\n\n const actorUserId = resolveActorReference(executionContext);\n if (!actorUserId) {\n throw utils.errorBuilder.buildError(ErrorType.AUTHORIZATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.AUTHORIZATION,\n context: { reason: 'force_password_reset requires an attributable principal' },\n });\n }\n\n // Self-service exists at `/auth/password/change`, which proves knowledge of the current\n // password. Letting an administrator route their OWN reset through the privileged verb\n // would bypass that proof using nothing but their admin role.\n //\n // Compared against the RESOLVED subject, never the raw addressing: a `{ _id: <self> }` filter\n // is the same self-reset, and testing the raw value would silently miss it now that the guard\n // above admits addressing shapes other than a bare string.\n if (subjectUserId === executionContext.initiatorIds?.userId) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: { reason: 'Use the self-service password change route to reset your own credential' },\n });\n }\n\n const { reason } = (input ?? {}) as { reason?: string };\n if (!reason) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: { reason: 'force_password_reset requires a documented reason' },\n });\n }\n\n return { subjectUserId, actorUserId, reason };\n}\n\n/** What the `beforeTransactionalCore` phase hands REPLACE (#2297): the account whose sessions it revoked. */\ninterface ForcedResetContinuation {\n revokedUser: { _id: string; email: string };\n}\n\n/**\n * The END-USER frontend's public URL, or a CONFIGURATION refusal. Asked in `beforeTransactionalCore` too, so an\n * environment that could never build the link refuses BEFORE the sessions are revoked rather than after.\n *\n * The END-USER frontend, never the admin console the caller is sitting in: the link is consumed by the account owner.\n * Same resolution the notification dispatcher uses for token-bearing emails.\n */\nfunction resolveForcedResetFrontendUrl(\n container: Container,\n executionContext: ExecutionContext<any>,\n utils: { errorBuilder: { buildError: (type: ErrorType, ec?: unknown, opts?: { context?: Record<string, unknown> }) => unknown } },\n): string {\n const frontendUrl = container.get<AppConfigurationService>(SAAS_SERVICE_TYPES.AppConfigService).resolveAppPublicBaseUrl();\n if (!frontendUrl) {\n throw utils.errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n context: {\n message:\n 'Cannot resolve the default frontend public URL required to build the password-reset link. '\n + 'Configure `runtime.endPoints.<default frontend>.publicUrl` for this environment.',\n },\n });\n }\n return frontendUrl;\n}\n\n/**\n * FORCE_PASSWORD_RESET — mint a single-use reset token for the target, mail it to the TARGET,\n * evict their sessions, and record who forced it and why.\n *\n * # Why the guards run in PREFIX and the work in REPLACE\n *\n * `replaceCoreOperations` runs AFTER `executeCorePersistenceOperation` — the core write has\n * already happened by the time a REPLACE handler can throw. Every refusal therefore has to live in\n * the PREFIX phase, which runs before it.\n *\n * This is not theoretical. With all four guards in REPLACE, a SELF-addressed call was refused with\n * `400 service_resource_validation_failed` and STILL wrote the caller's own user row — bumping it\n * enough to invalidate their session, so the very next request from that administrator answered\n * `401 authentication_jwt_token_failed`. A refusal that silently logs you out is the same class of\n * defect this whole operation was written to remove: an action that does something it does not say\n * it does. Caught by `examples/wonder-todos/e2e/credential-administration.e2e.ts` on 2026-08-05 and\n * isolated by replaying the two self-addressed calls against `users.updatedAt`.\n *\n * REPLACE still owns the WORK (and the response shape): the verb's whole effect lives in the\n * credential and token stores, not in a field patch, and the operation must answer with\n * `{ expiresAt, sessionsRevoked }` rather than the user row. Mirrors `applicationApiKeys |\n * usage_stats` for the response half.\n *\n * The validation is re-derived in REPLACE rather than carried over. It is pure, so this costs\n * nothing, and it keeps REPLACE correct on its own terms instead of trusting that PREFIX ran.\n *\n * # And why the revocation runs in `beforeTransactionalCore` (#2297)\n *\n * The operation is transactional, so its audit row is kept with the commit. The session revocation is a Redis write no\n * rollback can undo, and it must still precede the token (a failed revocation mints nothing), so it runs in the phase\n * that runs once, OUTSIDE the transaction and before it: the same validation first, then the revocation. Only the token\n * write runs inside; the email and the forced-reset record run after the commit.\n */\nexport function createUserForcePasswordResetFactories(): ResourceCustomServiceImplementationFactory[] {\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.USERS,\n operationIdentifier: Users_Operations.FORCE_PASSWORD_RESET,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n debugLabel: 'users.force_password_reset.credential-reset',\n /*\n * #2297: split so its database part runs in the operation's transaction and its effects outside the database do not.\n * - `beforeTransactionalCore` (outside any transaction): every refusal, then the account lookup and the session\n * revocation, in that order, exactly as before. A refusal or a failed revocation still mints nothing, and the Redis\n * revocation is an effect no rollback could undo, so it never runs inside the transaction.\n * - REPLACE (inside): the reset token is written through `utils.serviceOptions`, so it commits with the attempt and a\n * rolled-back attempt leaves no live link. The email (it carries the token's raw value, so it is never kept in an\n * outbox) and the `USER_PASSWORD_RESET_FORCED` record (a capturable recording) run after the commit.\n * The framework's own `RESOURCE_OPERATION_PERFORMED` row is then kept with the commit like every transactional\n * write's. The consumable-token store is declared a participant, so startup proves it shares the users adapter.\n */\n transactionalCore: true,\n transactionParticipants: { consumableToken: BackendResourceType.CONSUMABLE_TOKENS },\n hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,\n handlers: {\n // Every refusal, then the session revocation, before the transaction opens (see above). The revoked account\n // rides to REPLACE as the phase's frozen continuation.\n beforeTransactionalCore: async (_id, input, executionContext, _operationPath, utils) => {\n const { subjectUserId } = resolveValidatedForcedResetRequest(_id, input, executionContext, utils);\n resolveForcedResetFrontendUrl(container, executionContext, utils);\n const passwordResetService = container.get<AuthPasswordResetBackendService>(\n SAAS_SERVICE_TYPES.AuthPasswordResetService,\n );\n const revokedUser = await passwordResetService.revokeAccessForForcedPasswordReset(executionContext, { targetUserId: subjectUserId });\n return {\n disposition: ResourceOperationBeforeTransactionalCoreDisposition.CONTINUE,\n coreInputPatch: {},\n continuation: { revokedUser } satisfies ForcedResetContinuation,\n };\n },\n // PREFIX owns every refusal inside the core phase — it is the only core phase that runs BEFORE the core write,\n // so it is the only one where throwing leaves the row untouched. Returns `input` unchanged: this operation\n // contributes no field patch.\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n resolveValidatedForcedResetRequest(_id, input, executionContext, utils);\n return input;\n },\n replaceCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n const { subjectUserId, actorUserId, reason } =\n resolveValidatedForcedResetRequest(_id, input, executionContext, utils);\n const continuation = utils.beforeTransactionalCore?.continuation as ForcedResetContinuation | undefined;\n // The account whose sessions were revoked before the transaction opened, and only that one: a REPLACE reached\n // without the revocation (or for another subject) must not mint a link for an account whose sessions still live.\n if (!continuation?.revokedUser || continuation.revokedUser._id !== subjectUserId) {\n throw utils.errorBuilder.buildError(ErrorType.INTERNAL, executionContext, {\n context: { reason: 'force_password_reset reached its token write without the session revocation for this subject' },\n });\n }\n const frontendUrl = resolveForcedResetFrontendUrl(container, executionContext, utils);\n\n const passwordResetService = container.get<AuthPasswordResetBackendService>(\n SAAS_SERVICE_TYPES.AuthPasswordResetService,\n );\n const { expiresAt } = await passwordResetService.issueForcedPasswordReset(\n executionContext,\n { user: continuation.revokedUser, actorUserId, reason, frontendUrl },\n utils.serviceOptions,\n );\n const sessionsRevoked = true;\n\n // Deliberately NOT the token or the reset URL. Handing the caller a link that sets\n // someone else's password is an account-takeover primitive: they could complete it and\n // then authenticate as that user, unnoticed by the owner and — with no actor claim on a\n // session JWT — indistinguishable downstream from a genuine login. The link goes to the\n // account owner's inbox and nowhere else.\n return { expiresAt, sessionsRevoked };\n },\n },\n });\n\n return [factory as unknown as ResourceCustomServiceImplementationFactory];\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"user-role-administration.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/users/services/user-role-administration.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAaH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAmZpH,uCAAuC;AACvC,wBAAgB,mCAAmC,IAAI,0CAA0C,EAAE,CAKlG;AAoJD,mDAAmD;AACnD,wBAAgB,kCAAkC,IAAI,0CAA0C,EAAE,CAKjG"}
1
+ {"version":3,"file":"user-role-administration.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/users/services/user-role-administration.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAaH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAwZpH,uCAAuC;AACvC,wBAAgB,mCAAmC,IAAI,0CAA0C,EAAE,CAKlG;AAsJD,mDAAmD;AACnD,wBAAgB,kCAAkC,IAAI,0CAA0C,EAAE,CAKjG"}
@@ -293,11 +293,14 @@ function createUserRoleSetOperationFactory(operationIdentifier) {
293
293
  },
294
294
  debugLabel: `users.${operationIdentifier}.app-roles`,
295
295
  /*
296
- * #2297: the role ceiling is in memory (its refusal audit is written just before the throw); the user row is read
297
- * through utils.serviceOptions; the super-administrator floor counts OTHER users' rows; the role-change audit row is
298
- * written once the write is durable.
296
+ * #3670: the transaction is OWNED here, not borrowed from the audited write's (#2297), because the super-administrator
297
+ * floor serializes concurrent revocations only inside it: it bumps the APPLICATION row and measures there. Borrowing
298
+ * would leave the race open on any deployment where the post-commit outbox cannot capture the attempt. The hooks were
299
+ * read for #2297: the role ceiling is in memory (its refusal audit is written just before the throw), the user row
300
+ * is read through utils.serviceOptions, and the role-change audit row is written once the write is durable.
299
301
  */
300
- hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,
302
+ transactionalCore: true,
303
+ transactionParticipants: { application: CoreResourceType.APPLICATION },
301
304
  handlers: {
302
305
  prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {
303
306
  // Fail closed on anything but a single addressed user: the set algebra reads and rewrites
@@ -345,6 +348,8 @@ function createUserRoleSetOperationFactory(operationIdentifier) {
345
348
  errorBuilder: utils.errorBuilder,
346
349
  conferringRoles: superAdminConferralResolver(),
347
350
  operationIdentifier,
351
+ // #3670: the floor serializes concurrent revocations only inside the revocation's own transaction.
352
+ transaction: utils.serviceOptions?.transaction,
348
353
  });
349
354
  stageAppRolesAudit(executionContext, {
350
355
  userId: subjectUserId,
@@ -473,6 +478,8 @@ function createUserCoreRoleCeilingFactory(operationIdentifier) {
473
478
  errorBuilder: utils.errorBuilder,
474
479
  conferringRoles: superAdminConferralResolver(),
475
480
  operationIdentifier,
481
+ // #3670: the UPDATE's transaction is owned by the floor's own UPDATE registration (`transactionalCore`).
482
+ transaction: utils.serviceOptions?.transaction,
476
483
  });
477
484
  // Only a ROLE change is an authority change to evidence. A status edit has its own
478
485
  // lifecycle semantics and is not a privilege grant, so it does not stage this event.
@@ -1 +1 @@
1
- {"version":3,"file":"user-role-administration.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/users/services/user-role-administration.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * USERS application-role administration.\n *\n * `User.appRoles` IS a principal's live application authority: `execution-context-creator` reads the\n * array straight back into `initiatorRoles`, so whatever is written here is in force on the subject's\n * very next request. Every path that can rewrite it therefore carries the same three obligations —\n * a role-ceiling gate, an audit record, and the last-super-administrator floor.\n *\n * ## The four registrations, and why each exists separately\n *\n * | Path | Obligation |\n * |---|---|\n * | `assign_roles` | additive union onto the persisted array + ceiling + audit (+ floor, always a no-op) |\n * | `revoke_roles` | subtractive difference from the persisted array + ceiling + audit + floor |\n * | core `CREATE` | ceiling + audit on the AUTHORED `appRoles` (no floor — a create only ADDS) |\n * | core `UPDATE` | ceiling + audit on the AUTHORED `appRoles` + floor on `appRoles` AND `status` |\n *\n * A custom implementation resolves by an EXACT operation-path key, so none of these inherits another's\n * gate — this is the same reason `ORGANIZATION_UNIT_MEMBERS` wires CREATE and UPDATE independently, and\n * the reason `organizationMembers | promote|demote` was deleted rather than implemented on 2026-08-01\n * (a custom verb over a ceiling-gated core operation is NOT itself ceiling-gated).\n *\n * ## Why the two verbs are not redundant with the core UPDATE\n *\n * The core UPDATE REPLACES the whole array, so an administrator adding one role must submit the full\n * resulting set — a read-modify-write that races with any concurrent edit and silently clobbers it.\n * `assign_roles` / `revoke_roles` express the set-algebra directly against the persisted row, which is\n * both race-free and idempotent (union and difference both are), matching the `idempotent: true` these\n * operations declare and their authored 200 meanings (\"the requested roles ARE / ARE NOT present\").\n *\n * It also fixes the defect these operations shipped with: they declared `requestDto: { roles, reason }`,\n * but the persisted field is `appRoles`. `roles` is not a schema path, so the strict parse at the\n * controller AND again at the repository dropped it and the route answered 200 having written nothing.\n *\n * assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE · WILDO.ACCESS.PRIVILEGE_EVIDENCE — least-privilege enforcement and evidence on\n * application-role grant and revocation.\n */\n\nimport { resolveAuditActorId } from '~wildo-backend-lib/compliance/audit-trails/utils/audit-actor.backend.utils';\nimport type { Container } from 'inversify';\nimport {\n CoreResourceOperation,\n CoreResourceType,\n ErrorCustomMessageReference,\n ErrorType,\n ResourceOperationVariantType,\n Users_Operations,\n type User,\n} from '@wildo-ai/saas-models';\nimport {\n createResourceCustomServiceImplementation,\n ResourceCustomServiceImplementationHookTransactionPosture,\n type ResourceCustomServiceImplementationFactory,\n} from '~wildo-backend-lib/base-classes/services/services/resource-operation-custom-implementation.factory.backend';\nimport { emitAuthorityChangeAuditWhenDurable } from '~wildo-backend-lib/organizations/services/authority-change-audit.backend.utils';\nimport type { LoggerService } from '~wildo-backend-lib/monitoring/logger/services/logger.service';\nimport type { ResourceOperation_CustomImplementation_Utils } from '~wildo-backend-lib/base-classes/services/services/resource-operation-custom-implementation-base.backend.service';\nimport type { ResourcesRegistryBackendService } from '~wildo-backend-lib/resources/services/resources-registry.backend.service';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport {\n describeUnaddressableShape,\n resolveSingleSubjectId,\n} from '~wildo-backend-lib/base-classes/utils/single-subject-addressing.backend.utils';\nimport type { AuditLogsBackendService } from '~wildo-backend-lib/compliance/audit-trails/services/audit-logs.backend.service';\nimport {\n assertRequestedRolesWithinCallerCeiling,\n createRoleCeilingGate,\n} from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\nimport type { RoleCeilingGate } from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\nimport {\n assertSuperAdminFloorPreservedForTransition,\n createSuperAdminConferralResolver,\n} from '~wildo-backend-lib/security/authorizations/super-admin-floor.backend.utils';\nimport { engineRepositoriesRegistry } from '~wildo-backend-lib/base-classes/services/services/engine-repositories-registry.backend.utils';\n\n/**\n * The persisted authority field.\n *\n * Typed against {@link User} so a rename breaks the build here rather than silently reintroducing the\n * original defect — a patch key that is not a schema path is dropped by the repository parse and the\n * operation answers 200 having changed nothing.\n */\nconst APP_ROLES_FIELD: Extract<keyof User, 'appRoles'> = 'appRoles';\n\n/**\n * The persisted account-lifecycle field.\n *\n * Read alongside {@link APP_ROLES_FIELD} because the last-super-administrator floor is breached by\n * EITHER axis: stripping the role, or moving the sole administrator out of the one status that can\n * authenticate. Typed against {@link User} for the same reason — a rename must break the build here.\n */\nconst STATUS_FIELD: Extract<keyof User, 'status'> = 'status';\n\n/** The subset of the user row these implementations read. Narrow on purpose — see {@link APP_ROLES_FIELD}. */\ntype UserRoleAdministrationRow = Pick<User, '_id' | 'appRoles' | 'status'>;\n\n/**\n * Pending audit payload bridged from the prefix (which alone can see the PREVIOUS roles) to the\n * postfix (which alone knows the write succeeded).\n *\n * Keyed by the per-operation execution-context instance and held weakly, exactly like the one-time\n * secret bridge in the OAuth-client mint pipeline: concurrent requests each have their own EC, and\n * nothing here is ever persisted or reachable from another operation.\n */\ntype PendingAppRolesAudit = {\n userId?: string;\n previousRoles: string[];\n newRoles: string[];\n requestedRoles: string[];\n operation: string;\n reason?: string;\n};\nconst PENDING_APP_ROLES_AUDIT_BY_EC = new WeakMap<ExecutionContext<any>, PendingAppRolesAudit>();\n\n/**\n * Read the caller-authored `appRoles`, distinguishing \"not authored\" from \"authored empty\".\n *\n * The distinction is load-bearing on both obligations. A core UPDATE that does not mention `appRoles`\n * must not be gated (there is no grant to measure) and must not be audited (no authority changed),\n * while an explicit `appRoles: []` IS an authored change — a full revocation — and must be both.\n *\n * Non-string members are dropped rather than trusted: this runs on UNPARSED caller input (prefixes see\n * the payload before the strict DTO parse), and under-reporting can only ever deny or under-audit,\n * never grant. Malformed input is refused by schema validation further down the pipeline.\n */\nfunction readAuthoredAppRoles(\n input: unknown,\n appRolesGrantedByServer?: boolean,\n): string[] | undefined {\n // A field the TRUSTED SERVER injected is not a caller request, so it is not measured against the\n // caller's ceiling. This is the doctrine of `assertRequestedRolesWithinCallerCeiling` itself:\n // \"Framework-injected defaults (e.g. an organization type's `defaultMemberRole`) are\n // policy-derived, not a caller request, and must not be measured against the caller's ceiling.\"\n //\n // Self-registration is exactly that case and is the reason this branch exists. `/auth/register`\n // creates the user with `appRoles: userTypeDefinition.defaultAppRoles ?? [APP_USER]` — resolved\n // from the active user-type CONFIGURATION, never from the signup payload. Without this, the gate\n // read `APP_USER` off the create payload, compared it to the anonymous registrant's `APP_PUBLIC`,\n // and denied every signup with `role_ceiling_exceeded` (403 on POST /auth/register).\n //\n // The exemption is `utils.appRolesGrantedByServer`, and nothing else (#1140). It is set only by explicit server code\n // on the one call it applies to, never sourced from a client payload, and `doOperation` consumes it so a forwarded\n // `serviceOptions` cannot carry it into another write. It was once read off `preserveServerInjectedFields`, which is\n // ALSO filled by inference (every key a trusted prefix introduced) — so a prefix that happened to add `appRoles` and\n // an implementation that forwarded its options would have skipped the ceiling with nobody deciding to.\n if (appRolesGrantedByServer === true) return undefined;\n if (input === null || typeof input !== 'object') return undefined;\n const authored = (input as Record<string, unknown>)[APP_ROLES_FIELD];\n if (!Array.isArray(authored)) return undefined;\n return authored.filter((role): role is string => typeof role === 'string' && role.length > 0);\n}\n\n/**\n * Read the caller-authored `status`, distinguishing \"not authored\" from an authored value.\n *\n * Same not-authored-vs-authored distinction as {@link readAuthoredAppRoles}, for the same reason:\n * an UPDATE that does not mention `status` leaves the account lifecycle untouched and must not be\n * measured against the super-administrator floor, while an authored `SUSPENDED` on the sole\n * administrator IS the lockout the floor exists to refuse.\n *\n * Runs on UNPARSED caller input, so a non-string is reported as not-authored rather than trusted —\n * under-reporting can only fail to CHECK a change that schema validation will then reject anyway,\n * never permit one that would breach the floor undetected.\n */\nfunction readAuthoredStatus(input: unknown): string | undefined {\n if (input === null || typeof input !== 'object') return undefined;\n const authored = (input as Record<string, unknown>)[STATUS_FIELD];\n return typeof authored === 'string' && authored.length > 0 ? authored : undefined;\n}\n\n/** The free-text justification the two verbs collect. Absent on the core CREATE/UPDATE paths. */\nfunction readAuthoredReason(input: unknown): string | undefined {\n if (input === null || typeof input !== 'object') return undefined;\n const reason = (input as { reason?: unknown }).reason;\n return typeof reason === 'string' && reason.length > 0 ? reason : undefined;\n}\n\n/**\n * The roles the two verbs operate on, read from their declared `requestDto` key.\n *\n * ⚠️ This is `roles` — the REQUEST contract — deliberately NOT {@link APP_ROLES_FIELD}, which is the\n * PERSISTENCE contract. Conflating the two is the original defect: the operation collects `roles` and\n * must translate it into an `appRoles` patch. The two names are kept visibly distinct here so the\n * translation is impossible to miss.\n */\nfunction readRequestedRoles(input: unknown): string[] {\n if (input === null || typeof input !== 'object') return [];\n const requested = (input as { roles?: unknown }).roles;\n if (!Array.isArray(requested)) return [];\n return requested.filter((role): role is string => typeof role === 'string' && role.length > 0);\n}\n\n/**\n * Load the user whose authority is about to change, or fail closed.\n *\n * Set algebra needs the CURRENT array, and on a HIGH-risk authority write \"could not read it\" must\n * never degrade into \"treat it as empty\" — that would turn `assign_roles` into a silent whole-array\n * replace and `revoke_roles` into a silent no-op. Both failure modes are refused explicitly.\n *\n * The read goes through the repository so it carries the contextual scope filter: a user outside the\n * caller's administrative scope is simply absent and reported as 404, never confirmed as existing.\n */\nasync function loadUserForRoleAdministration(\n userId: string,\n executionContext: ExecutionContext<any>,\n utils: ResourceOperation_CustomImplementation_Utils,\n operationIdentifier: string,\n): Promise<UserRoleAdministrationRow> {\n const userRepository = engineRepositoriesRegistry(utils).getRepository(CoreResourceType.USERS);\n\n // Fail closed: without the repository the current authority is unknowable, and every downstream\n // set operation would silently compute against an empty array.\n if (!userRepository) {\n throw utils.errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.CONFIGURATION,\n context: {\n message: 'Cannot administer application roles: the users repository is not registered',\n resourceType: CoreResourceType.USERS,\n operationIdentifier,\n },\n });\n }\n\n // Through the operation's options: inside its transaction this is the row the attempt holds (#2297).\n const user = await userRepository.read<UserRoleAdministrationRow>(executionContext, userId, utils.serviceOptions);\n\n if (!user) {\n throw utils.errorBuilder.buildError(ErrorType.NOT_FOUND, executionContext, {\n customMessageReference: ErrorCustomMessageReference.NOT_FOUND,\n context: { reason: 'user_not_found', userId, operationIdentifier },\n });\n }\n\n return user;\n}\n\n/**\n * The persisted authority as a clean string array.\n *\n * `appRoles` carries a `.default([APP_USER])`, which applies only to an ABSENT value at parse time —\n * a row can still legitimately hold an empty array (a fully revoked user). Anything non-array is\n * treated as empty rather than crashing the read, because the ceiling gate and the audit record are\n * both computed from it and both degrade safely toward \"no authority held\".\n */\nfunction currentAppRolesOf(user: UserRoleAdministrationRow): string[] {\n return Array.isArray(user.appRoles) ? user.appRoles.filter((role): role is string => typeof role === 'string') : [];\n}\n\n/**\n * Gate a role change against the caller's own authority.\n *\n * Always measured on the roles AS AUTHORED BY THE CALLER, never on the resolved post-operation set.\n * The distinction matters on the additive path: a user may already hold a role ABOVE the caller's\n * ceiling, and measuring the union would 403 an administrator merely adding an unrelated low role to\n * that user — refusing a legitimate grant because of authority someone else conferred earlier.\n *\n * REVOCATION IS GATED TOO, which the \"cannot grant what you do not hold\" framing does not immediately\n * suggest. Stripping a role is not escalation, but an administrator who may not GRANT\n * `APP_ADMIN_SUPER_ADMIN` must equally not be able to STRIP it — otherwise the cheapest attack on a\n * privileged account is to remove the authority of whoever could intervene. The users specification\n * already documents a 403 on `revoke_roles` for exactly this (\"the caller lacks the authority to\n * revoke the requested roles\"). Both bypasses still apply: an APPLICATION-scoped super-admin, and\n * internal/system writes.\n */\nfunction assertAppRoleChangeWithinCallerCeiling(\n authoredRoles: readonly string[],\n executionContext: ExecutionContext<any>,\n utils: ResourceOperation_CustomImplementation_Utils,\n roleCeilingGate: RoleCeilingGate,\n): void {\n assertRequestedRolesWithinCallerCeiling(executionContext, authoredRoles, utils.errorBuilder, roleCeilingGate);\n}\n\n/** Set equality over role arrays — order is not semantic, so a reorder is not a change. */\nfunction isSameRoleSet(left: readonly string[], right: readonly string[]): boolean {\n if (left.length !== right.length) return false;\n const rightSet = new Set(right);\n return left.every((role) => rightSet.has(role));\n}\n\n/**\n * Record that a role change is pending, unless it would change nothing.\n *\n * A grant of a role the subject already holds, or a revocation of one they do not, leaves authority\n * untouched — the operation is still a correct 200 (the requested end-state holds), but there is no\n * privilege change to evidence and recording it would bury real grants in no-op noise.\n */\nfunction stageAppRolesAudit(executionContext: ExecutionContext<any>, pending: PendingAppRolesAudit): void {\n if (isSameRoleSet(pending.previousRoles, pending.newRoles)) return;\n PENDING_APP_ROLES_AUDIT_BY_EC.set(executionContext, pending);\n}\n\n/**\n * Emit the staged audit event, after the write has actually been persisted.\n *\n * Best-effort by design, mirroring `AuditLogsBackendService._createAuditLog`, which already isolates\n * store failures so one unavailable sink cannot fail an otherwise valid operation. The write is\n * committed by the time this runs, so throwing here would report failure for a change that DID\n * happen — a worse outcome than a logged emission failure.\n */\nasync function emitStagedAppRolesAudit(\n container: Container,\n persistedUserId: string | undefined,\n executionContext: ExecutionContext<any>,\n utils: ResourceOperation_CustomImplementation_Utils,\n): Promise<void> {\n const pending = PENDING_APP_ROLES_AUDIT_BY_EC.get(executionContext);\n if (!pending) return;\n PENDING_APP_ROLES_AUDIT_BY_EC.delete(executionContext);\n\n const subjectUserId = pending.userId ?? persistedUserId;\n if (!subjectUserId) {\n utils.logger.warn?.('Application roles changed but the subject user id could not be resolved — audit event not emitted', {\n operation: pending.operation,\n });\n return;\n }\n\n /*\n * #2297: recorded once the write is durable. Inside a transaction (the one that keeps the operation's audit row) it is a\n * post-commit effect, so a role change that rolls back asserts no grant; without one the write has committed and it is\n * written now. Failures are contained and logged by the helper, as this function always did.\n */\n await emitAuthorityChangeAuditWhenDurable({\n serviceOptions: utils.serviceOptions,\n effectKey: ['user-app-roles-changed', pending.operation, subjectUserId].join(':'),\n logger: utils.logger as LoggerService,\n failureContext: { operation: pending.operation, userId: subjectUserId },\n emit: async () => {\n const auditLogsService = container.get<AuditLogsBackendService>(SAAS_SERVICE_TYPES.AuditLogsService);\n await auditLogsService.logUserAppRolesChanged({\n userId: subjectUserId,\n previousRoles: pending.previousRoles,\n newRoles: pending.newRoles,\n requestedRoles: pending.requestedRoles,\n operation: pending.operation,\n changedBy: resolveAuditActorId(executionContext),\n reason: pending.reason,\n }, executionContext);\n },\n });\n}\n\n/**\n * `assign_roles` / `revoke_roles` — set algebra against the persisted array.\n *\n * Both translate the request contract (`roles`) into the persistence contract (`appRoles`) and return\n * ONLY that patch: the declared `requestDto` names no schema field, so the core UPDATE has nothing to\n * persist unless the real mutation is supplied here (the same contract `ORGANIZATION_UNITS`\n * MOVE / ARCHIVE follow).\n *\n * Union preserves the existing order and appends, so a repeated grant is byte-identical rather than a\n * reshuffle. A full revocation is allowed to leave `appRoles: []` — legitimate offboarding, and\n * recoverable through `assign_roles`. Re-injecting the schema default there was rejected: it would\n * silently re-grant the very role the administrator just revoked.\n */\nfunction createUserRoleSetOperationFactory(\n operationIdentifier: Users_Operations.ASSIGN_ROLES | Users_Operations.REVOKE_ROLES,\n): ResourceCustomServiceImplementationFactory {\n const isAssign = operationIdentifier === Users_Operations.ASSIGN_ROLES;\n\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) => {\n const roleCeilingGate = createRoleCeilingGate(container);\n const superAdminConferralResolver = createSuperAdminConferralResolver(container);\n\n return createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.USERS,\n operationIdentifier,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n debugLabel: `users.${operationIdentifier}.app-roles`,\n /*\n * #2297: the role ceiling is in memory (its refusal audit is written just before the throw); the user row is read\n * through utils.serviceOptions; the super-administrator floor counts OTHER users' rows; the role-change audit row is\n * written once the write is durable.\n */\n hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,\n handlers: {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n // Fail closed on anything but a single addressed user: the set algebra reads and rewrites\n // ONE row, and a population-addressed write reaching this path would otherwise compute a\n // patch from a row it did not select. A conditional `{ _id, … }` filter is NOT such a case —\n // `_id` pins the row and the extra predicates only decide whether the write lands, so the\n // algebra still reads and rewrites the row it selected.\n const subjectUserId = resolveSingleSubjectId(_id);\n if (subjectUserId === undefined) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n context: {\n message: `Users ${operationIdentifier} requires a single addressed subject`,\n reason: 'user_role_administration_unaddressed',\n receivedAddressing: describeUnaddressableShape(_id),\n },\n });\n }\n\n const requestedRoles = readRequestedRoles(input);\n if (requestedRoles.length === 0) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: { reason: 'user_role_administration_no_roles', operationIdentifier, userId: subjectUserId },\n });\n }\n\n assertAppRoleChangeWithinCallerCeiling(requestedRoles, executionContext, utils, roleCeilingGate);\n\n const user = await loadUserForRoleAdministration(subjectUserId, executionContext, utils, operationIdentifier);\n const previousRoles = currentAppRolesOf(user);\n\n const requestedRoleSet = new Set(requestedRoles);\n const newRoles = isAssign\n ? [...previousRoles, ...requestedRoles.filter((role) => !previousRoles.includes(role))]\n : previousRoles.filter((role) => !requestedRoleSet.has(role));\n\n // The last-super-administrator floor. Invoked on BOTH verbs even though only `revoke_roles`\n // can breach it: the transition check short-circuits at zero cost whenever the subject still\n // confers super-admin afterwards, which a union always does — so wiring it to both costs\n // nothing and removes the standing invitation for a future change to the set algebra above\n // to bypass the floor unnoticed. Neither verb touches `status`, so it is passed unchanged.\n await assertSuperAdminFloorPreservedForTransition({\n subjectUserId,\n previousRoles,\n previousStatus: user.status,\n nextRoles: newRoles,\n nextStatus: user.status,\n executionContext,\n repositoriesRegistry: engineRepositoriesRegistry(utils),\n errorBuilder: utils.errorBuilder,\n conferringRoles: superAdminConferralResolver(),\n operationIdentifier,\n });\n\n stageAppRolesAudit(executionContext, {\n userId: subjectUserId,\n previousRoles,\n newRoles,\n requestedRoles,\n operation: operationIdentifier,\n reason: readAuthoredReason(input),\n });\n\n return { [APP_ROLES_FIELD]: newRoles };\n },\n postfixCoreOperations: async (_id, result, executionContext, _operationPath, utils) => {\n await emitStagedAppRolesAudit(container, resolveSingleSubjectId(_id), executionContext, utils);\n return result;\n },\n },\n });\n };\n\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n}\n\n/** `assign_roles` + `revoke_roles`. */\nexport function createUserRoleSetOperationFactories(): ResourceCustomServiceImplementationFactory[] {\n return [\n createUserRoleSetOperationFactory(Users_Operations.ASSIGN_ROLES),\n createUserRoleSetOperationFactory(Users_Operations.REVOKE_ROLES),\n ];\n}\n\n/**\n * Core `CREATE` / `UPDATE` — the paths that actually grant application roles today.\n *\n * `appRoles` is caller-settable on both: neither variant narrows its `requestDto`, and the field\n * carries no `.excludeFromCreate()` / `.excludeFromUpdate()`, so the generated API accepts it. Until\n * this gate landed, nothing measured it against the caller's own authority — the protection was\n * incidental, resting entirely on both operations being gated to `APP_ADMIN_SUPER_ADMIN`, which is\n * precisely the ceiling gate's own bypass. Any application that widens those `roles:` arrays (roles\n * are application-configurable by design) would have turned `appRoles` into an unguarded escalation\n * vector. The role-ceiling utility's own contract asks for this explicitly: wire it into the CREATE\n * and any roles-mutating UPDATE path of every resource whose roles are caller-settable.\n *\n * A request that does not mention `appRoles` is passed through untouched — there is no grant to\n * measure and no authority change to evidence, so neither obligation applies.\n */\nfunction createUserCoreRoleCeilingFactory(\n operationIdentifier: CoreResourceOperation.CREATE | CoreResourceOperation.UPDATE,\n): ResourceCustomServiceImplementationFactory {\n const isCreate = operationIdentifier === CoreResourceOperation.CREATE;\n\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) => {\n const roleCeilingGate = createRoleCeilingGate(container);\n const superAdminConferralResolver = createSuperAdminConferralResolver(container);\n\n return createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.USERS,\n operationIdentifier,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n debugLabel: `users.${isCreate ? 'create' : 'update'}.app-roles-ceiling`,\n // #2297: reviewed for CREATE only (its prefix checks the ceiling in memory; the role-change audit row is written once\n // the write is durable). UPDATE's prefix has not been read for it, so it declares nothing.\n ...(isCreate ? { hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION } : {}),\n handlers: {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n const authoredRoles = readAuthoredAppRoles(input, utils.appRolesGrantedByServer);\n const authoredStatus = readAuthoredStatus(input);\n\n // Neither authority axis is touched ⇒ nothing to gate, evidence or protect. At CREATE an\n // absent `appRoles` is the framework-injected `.default([APP_USER])`, which is\n // policy-derived rather than a caller request and must not face the caller's ceiling; at\n // UPDATE it is simply an edit of other fields.\n //\n // `status` joins the trigger because the super-administrator floor is breached by EITHER\n // axis: stripping the role, or moving the sole administrator into a status that cannot\n // authenticate. `status` is caller-settable on this contract (it carries no\n // `.excludeFromUpdate()` and the variant declares no narrowing `requestDto`), so a\n // roles-only trigger would leave the whole lockout-by-suspension path unguarded.\n if (authoredRoles === undefined && authoredStatus === undefined) return input;\n\n // The ceiling measures GRANTS, so it sees only the authored roles. A status edit confers\n // no authority and is deliberately outside it.\n if (authoredRoles !== undefined) {\n assertAppRoleChangeWithinCallerCeiling(authoredRoles, executionContext, utils, roleCeilingGate);\n }\n\n // CREATE has no prior row: the authored set IS the grant, staged against an empty baseline.\n // The floor is not consulted — a create can only ever ADD to the administrator population.\n if (isCreate) {\n if (authoredRoles !== undefined) {\n stageAppRolesAudit(executionContext, {\n previousRoles: [],\n newRoles: authoredRoles,\n requestedRoles: authoredRoles,\n operation: operationIdentifier,\n });\n }\n return input;\n }\n\n // Resolve the ONE subject this write addresses. A bare id, the `{ id, filter? }` envelope and\n // a conditional `{ _id, … }` filter all name a single row — invitation acceptance activates\n // the invitee through exactly that last form (`{ _id, status: PENDING_INVITATION }`), so the\n // former `typeof _id === 'string'` test refused a legitimate, framework-idiomatic write.\n //\n // Only a genuinely POPULATION-addressed write is refused: it can neither attribute the\n // authority change to one subject, nor name the subjects the floor would have to exclude\n // from its population count. `UPDATE_MANY` is INTERNAL_CALL and reaches a different path.\n const subjectUserId = resolveSingleSubjectId(_id);\n if (subjectUserId === undefined) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n context: {\n message: 'Users UPDATE carrying appRoles or status requires a single addressed subject',\n reason: 'user_role_administration_unaddressed',\n receivedAddressing: describeUnaddressableShape(_id),\n },\n });\n }\n\n // Read ONLY when the patch touches authority — an ordinary user edit pays nothing for this.\n const user = await loadUserForRoleAdministration(subjectUserId, executionContext, utils, operationIdentifier);\n const previousRoles = currentAppRolesOf(user);\n\n // An UNAUTHORED axis is unchanged by this write, so the resulting state carries the\n // persisted value forward. Reading `??` as \"absent means empty\" here would be the reverse\n // of the whole not-authored-vs-authored distinction: a status-only edit would look like a\n // full role revocation and a roles-only edit like a status wipe.\n await assertSuperAdminFloorPreservedForTransition({\n subjectUserId,\n previousRoles,\n previousStatus: user.status,\n nextRoles: authoredRoles ?? previousRoles,\n nextStatus: authoredStatus ?? user.status,\n executionContext,\n repositoriesRegistry: engineRepositoriesRegistry(utils),\n errorBuilder: utils.errorBuilder,\n conferringRoles: superAdminConferralResolver(),\n operationIdentifier,\n });\n\n // Only a ROLE change is an authority change to evidence. A status edit has its own\n // lifecycle semantics and is not a privilege grant, so it does not stage this event.\n if (authoredRoles !== undefined) {\n stageAppRolesAudit(executionContext, {\n userId: subjectUserId,\n previousRoles,\n newRoles: authoredRoles,\n requestedRoles: authoredRoles,\n operation: operationIdentifier,\n });\n }\n\n return input;\n },\n postfixCoreOperations: async (_id, result, executionContext, _operationPath, utils) => {\n // At CREATE the subject id exists only now, on the persisted row.\n const persistedUserId = resolveSingleSubjectId(_id)\n ?? (result && typeof result === 'object' ? (result as { _id?: unknown })._id : undefined);\n\n await emitStagedAppRolesAudit(\n container,\n typeof persistedUserId === 'string' ? persistedUserId : undefined,\n executionContext,\n utils,\n );\n return result;\n },\n },\n });\n };\n\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n}\n\n/** Core `CREATE` + `UPDATE` role-ceiling gates. */\nexport function createUserCoreRoleCeilingFactories(): ResourceCustomServiceImplementationFactory[] {\n return [\n createUserCoreRoleCeilingFactory(CoreResourceOperation.CREATE),\n createUserCoreRoleCeilingFactory(CoreResourceOperation.UPDATE),\n ];\n}\n"]}
1
+ {"version":3,"file":"user-role-administration.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/users/services/user-role-administration.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * USERS application-role administration.\n *\n * `User.appRoles` IS a principal's live application authority: `execution-context-creator` reads the\n * array straight back into `initiatorRoles`, so whatever is written here is in force on the subject's\n * very next request. Every path that can rewrite it therefore carries the same three obligations —\n * a role-ceiling gate, an audit record, and the last-super-administrator floor.\n *\n * ## The four registrations, and why each exists separately\n *\n * | Path | Obligation |\n * |---|---|\n * | `assign_roles` | additive union onto the persisted array + ceiling + audit (+ floor, always a no-op) |\n * | `revoke_roles` | subtractive difference from the persisted array + ceiling + audit + floor |\n * | core `CREATE` | ceiling + audit on the AUTHORED `appRoles` (no floor — a create only ADDS) |\n * | core `UPDATE` | ceiling + audit on the AUTHORED `appRoles` + floor on `appRoles` AND `status` |\n *\n * A custom implementation resolves by an EXACT operation-path key, so none of these inherits another's\n * gate — this is the same reason `ORGANIZATION_UNIT_MEMBERS` wires CREATE and UPDATE independently, and\n * the reason `organizationMembers | promote|demote` was deleted rather than implemented on 2026-08-01\n * (a custom verb over a ceiling-gated core operation is NOT itself ceiling-gated).\n *\n * ## Why the two verbs are not redundant with the core UPDATE\n *\n * The core UPDATE REPLACES the whole array, so an administrator adding one role must submit the full\n * resulting set — a read-modify-write that races with any concurrent edit and silently clobbers it.\n * `assign_roles` / `revoke_roles` express the set-algebra directly against the persisted row, which is\n * both race-free and idempotent (union and difference both are), matching the `idempotent: true` these\n * operations declare and their authored 200 meanings (\"the requested roles ARE / ARE NOT present\").\n *\n * It also fixes the defect these operations shipped with: they declared `requestDto: { roles, reason }`,\n * but the persisted field is `appRoles`. `roles` is not a schema path, so the strict parse at the\n * controller AND again at the repository dropped it and the route answered 200 having written nothing.\n *\n * assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE · WILDO.ACCESS.PRIVILEGE_EVIDENCE — least-privilege enforcement and evidence on\n * application-role grant and revocation.\n */\n\nimport { resolveAuditActorId } from '~wildo-backend-lib/compliance/audit-trails/utils/audit-actor.backend.utils';\nimport type { Container } from 'inversify';\nimport {\n CoreResourceOperation,\n CoreResourceType,\n ErrorCustomMessageReference,\n ErrorType,\n ResourceOperationVariantType,\n Users_Operations,\n type User,\n} from '@wildo-ai/saas-models';\nimport {\n createResourceCustomServiceImplementation,\n ResourceCustomServiceImplementationHookTransactionPosture,\n type ResourceCustomServiceImplementationFactory,\n} from '~wildo-backend-lib/base-classes/services/services/resource-operation-custom-implementation.factory.backend';\nimport { emitAuthorityChangeAuditWhenDurable } from '~wildo-backend-lib/organizations/services/authority-change-audit.backend.utils';\nimport type { LoggerService } from '~wildo-backend-lib/monitoring/logger/services/logger.service';\nimport type { ResourceOperation_CustomImplementation_Utils } from '~wildo-backend-lib/base-classes/services/services/resource-operation-custom-implementation-base.backend.service';\nimport type { ResourcesRegistryBackendService } from '~wildo-backend-lib/resources/services/resources-registry.backend.service';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport {\n describeUnaddressableShape,\n resolveSingleSubjectId,\n} from '~wildo-backend-lib/base-classes/utils/single-subject-addressing.backend.utils';\nimport type { AuditLogsBackendService } from '~wildo-backend-lib/compliance/audit-trails/services/audit-logs.backend.service';\nimport {\n assertRequestedRolesWithinCallerCeiling,\n createRoleCeilingGate,\n} from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\nimport type { RoleCeilingGate } from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\nimport {\n assertSuperAdminFloorPreservedForTransition,\n createSuperAdminConferralResolver,\n} from '~wildo-backend-lib/security/authorizations/super-admin-floor.backend.utils';\nimport { engineRepositoriesRegistry } from '~wildo-backend-lib/base-classes/services/services/engine-repositories-registry.backend.utils';\n\n/**\n * The persisted authority field.\n *\n * Typed against {@link User} so a rename breaks the build here rather than silently reintroducing the\n * original defect — a patch key that is not a schema path is dropped by the repository parse and the\n * operation answers 200 having changed nothing.\n */\nconst APP_ROLES_FIELD: Extract<keyof User, 'appRoles'> = 'appRoles';\n\n/**\n * The persisted account-lifecycle field.\n *\n * Read alongside {@link APP_ROLES_FIELD} because the last-super-administrator floor is breached by\n * EITHER axis: stripping the role, or moving the sole administrator out of the one status that can\n * authenticate. Typed against {@link User} for the same reason — a rename must break the build here.\n */\nconst STATUS_FIELD: Extract<keyof User, 'status'> = 'status';\n\n/** The subset of the user row these implementations read. Narrow on purpose — see {@link APP_ROLES_FIELD}. */\ntype UserRoleAdministrationRow = Pick<User, '_id' | 'appRoles' | 'status'>;\n\n/**\n * Pending audit payload bridged from the prefix (which alone can see the PREVIOUS roles) to the\n * postfix (which alone knows the write succeeded).\n *\n * Keyed by the per-operation execution-context instance and held weakly, exactly like the one-time\n * secret bridge in the OAuth-client mint pipeline: concurrent requests each have their own EC, and\n * nothing here is ever persisted or reachable from another operation.\n */\ntype PendingAppRolesAudit = {\n userId?: string;\n previousRoles: string[];\n newRoles: string[];\n requestedRoles: string[];\n operation: string;\n reason?: string;\n};\nconst PENDING_APP_ROLES_AUDIT_BY_EC = new WeakMap<ExecutionContext<any>, PendingAppRolesAudit>();\n\n/**\n * Read the caller-authored `appRoles`, distinguishing \"not authored\" from \"authored empty\".\n *\n * The distinction is load-bearing on both obligations. A core UPDATE that does not mention `appRoles`\n * must not be gated (there is no grant to measure) and must not be audited (no authority changed),\n * while an explicit `appRoles: []` IS an authored change — a full revocation — and must be both.\n *\n * Non-string members are dropped rather than trusted: this runs on UNPARSED caller input (prefixes see\n * the payload before the strict DTO parse), and under-reporting can only ever deny or under-audit,\n * never grant. Malformed input is refused by schema validation further down the pipeline.\n */\nfunction readAuthoredAppRoles(\n input: unknown,\n appRolesGrantedByServer?: boolean,\n): string[] | undefined {\n // A field the TRUSTED SERVER injected is not a caller request, so it is not measured against the\n // caller's ceiling. This is the doctrine of `assertRequestedRolesWithinCallerCeiling` itself:\n // \"Framework-injected defaults (e.g. an organization type's `defaultMemberRole`) are\n // policy-derived, not a caller request, and must not be measured against the caller's ceiling.\"\n //\n // Self-registration is exactly that case and is the reason this branch exists. `/auth/register`\n // creates the user with `appRoles: userTypeDefinition.defaultAppRoles ?? [APP_USER]` — resolved\n // from the active user-type CONFIGURATION, never from the signup payload. Without this, the gate\n // read `APP_USER` off the create payload, compared it to the anonymous registrant's `APP_PUBLIC`,\n // and denied every signup with `role_ceiling_exceeded` (403 on POST /auth/register).\n //\n // The exemption is `utils.appRolesGrantedByServer`, and nothing else (#1140). It is set only by explicit server code\n // on the one call it applies to, never sourced from a client payload, and `doOperation` consumes it so a forwarded\n // `serviceOptions` cannot carry it into another write. It was once read off `preserveServerInjectedFields`, which is\n // ALSO filled by inference (every key a trusted prefix introduced) — so a prefix that happened to add `appRoles` and\n // an implementation that forwarded its options would have skipped the ceiling with nobody deciding to.\n if (appRolesGrantedByServer === true) return undefined;\n if (input === null || typeof input !== 'object') return undefined;\n const authored = (input as Record<string, unknown>)[APP_ROLES_FIELD];\n if (!Array.isArray(authored)) return undefined;\n return authored.filter((role): role is string => typeof role === 'string' && role.length > 0);\n}\n\n/**\n * Read the caller-authored `status`, distinguishing \"not authored\" from an authored value.\n *\n * Same not-authored-vs-authored distinction as {@link readAuthoredAppRoles}, for the same reason:\n * an UPDATE that does not mention `status` leaves the account lifecycle untouched and must not be\n * measured against the super-administrator floor, while an authored `SUSPENDED` on the sole\n * administrator IS the lockout the floor exists to refuse.\n *\n * Runs on UNPARSED caller input, so a non-string is reported as not-authored rather than trusted —\n * under-reporting can only fail to CHECK a change that schema validation will then reject anyway,\n * never permit one that would breach the floor undetected.\n */\nfunction readAuthoredStatus(input: unknown): string | undefined {\n if (input === null || typeof input !== 'object') return undefined;\n const authored = (input as Record<string, unknown>)[STATUS_FIELD];\n return typeof authored === 'string' && authored.length > 0 ? authored : undefined;\n}\n\n/** The free-text justification the two verbs collect. Absent on the core CREATE/UPDATE paths. */\nfunction readAuthoredReason(input: unknown): string | undefined {\n if (input === null || typeof input !== 'object') return undefined;\n const reason = (input as { reason?: unknown }).reason;\n return typeof reason === 'string' && reason.length > 0 ? reason : undefined;\n}\n\n/**\n * The roles the two verbs operate on, read from their declared `requestDto` key.\n *\n * ⚠️ This is `roles` — the REQUEST contract — deliberately NOT {@link APP_ROLES_FIELD}, which is the\n * PERSISTENCE contract. Conflating the two is the original defect: the operation collects `roles` and\n * must translate it into an `appRoles` patch. The two names are kept visibly distinct here so the\n * translation is impossible to miss.\n */\nfunction readRequestedRoles(input: unknown): string[] {\n if (input === null || typeof input !== 'object') return [];\n const requested = (input as { roles?: unknown }).roles;\n if (!Array.isArray(requested)) return [];\n return requested.filter((role): role is string => typeof role === 'string' && role.length > 0);\n}\n\n/**\n * Load the user whose authority is about to change, or fail closed.\n *\n * Set algebra needs the CURRENT array, and on a HIGH-risk authority write \"could not read it\" must\n * never degrade into \"treat it as empty\" — that would turn `assign_roles` into a silent whole-array\n * replace and `revoke_roles` into a silent no-op. Both failure modes are refused explicitly.\n *\n * The read goes through the repository so it carries the contextual scope filter: a user outside the\n * caller's administrative scope is simply absent and reported as 404, never confirmed as existing.\n */\nasync function loadUserForRoleAdministration(\n userId: string,\n executionContext: ExecutionContext<any>,\n utils: ResourceOperation_CustomImplementation_Utils,\n operationIdentifier: string,\n): Promise<UserRoleAdministrationRow> {\n const userRepository = engineRepositoriesRegistry(utils).getRepository(CoreResourceType.USERS);\n\n // Fail closed: without the repository the current authority is unknowable, and every downstream\n // set operation would silently compute against an empty array.\n if (!userRepository) {\n throw utils.errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.CONFIGURATION,\n context: {\n message: 'Cannot administer application roles: the users repository is not registered',\n resourceType: CoreResourceType.USERS,\n operationIdentifier,\n },\n });\n }\n\n // Through the operation's options: inside its transaction this is the row the attempt holds (#2297).\n const user = await userRepository.read<UserRoleAdministrationRow>(executionContext, userId, utils.serviceOptions);\n\n if (!user) {\n throw utils.errorBuilder.buildError(ErrorType.NOT_FOUND, executionContext, {\n customMessageReference: ErrorCustomMessageReference.NOT_FOUND,\n context: { reason: 'user_not_found', userId, operationIdentifier },\n });\n }\n\n return user;\n}\n\n/**\n * The persisted authority as a clean string array.\n *\n * `appRoles` carries a `.default([APP_USER])`, which applies only to an ABSENT value at parse time —\n * a row can still legitimately hold an empty array (a fully revoked user). Anything non-array is\n * treated as empty rather than crashing the read, because the ceiling gate and the audit record are\n * both computed from it and both degrade safely toward \"no authority held\".\n */\nfunction currentAppRolesOf(user: UserRoleAdministrationRow): string[] {\n return Array.isArray(user.appRoles) ? user.appRoles.filter((role): role is string => typeof role === 'string') : [];\n}\n\n/**\n * Gate a role change against the caller's own authority.\n *\n * Always measured on the roles AS AUTHORED BY THE CALLER, never on the resolved post-operation set.\n * The distinction matters on the additive path: a user may already hold a role ABOVE the caller's\n * ceiling, and measuring the union would 403 an administrator merely adding an unrelated low role to\n * that user — refusing a legitimate grant because of authority someone else conferred earlier.\n *\n * REVOCATION IS GATED TOO, which the \"cannot grant what you do not hold\" framing does not immediately\n * suggest. Stripping a role is not escalation, but an administrator who may not GRANT\n * `APP_ADMIN_SUPER_ADMIN` must equally not be able to STRIP it — otherwise the cheapest attack on a\n * privileged account is to remove the authority of whoever could intervene. The users specification\n * already documents a 403 on `revoke_roles` for exactly this (\"the caller lacks the authority to\n * revoke the requested roles\"). Both bypasses still apply: an APPLICATION-scoped super-admin, and\n * internal/system writes.\n */\nfunction assertAppRoleChangeWithinCallerCeiling(\n authoredRoles: readonly string[],\n executionContext: ExecutionContext<any>,\n utils: ResourceOperation_CustomImplementation_Utils,\n roleCeilingGate: RoleCeilingGate,\n): void {\n assertRequestedRolesWithinCallerCeiling(executionContext, authoredRoles, utils.errorBuilder, roleCeilingGate);\n}\n\n/** Set equality over role arrays — order is not semantic, so a reorder is not a change. */\nfunction isSameRoleSet(left: readonly string[], right: readonly string[]): boolean {\n if (left.length !== right.length) return false;\n const rightSet = new Set(right);\n return left.every((role) => rightSet.has(role));\n}\n\n/**\n * Record that a role change is pending, unless it would change nothing.\n *\n * A grant of a role the subject already holds, or a revocation of one they do not, leaves authority\n * untouched — the operation is still a correct 200 (the requested end-state holds), but there is no\n * privilege change to evidence and recording it would bury real grants in no-op noise.\n */\nfunction stageAppRolesAudit(executionContext: ExecutionContext<any>, pending: PendingAppRolesAudit): void {\n if (isSameRoleSet(pending.previousRoles, pending.newRoles)) return;\n PENDING_APP_ROLES_AUDIT_BY_EC.set(executionContext, pending);\n}\n\n/**\n * Emit the staged audit event, after the write has actually been persisted.\n *\n * Best-effort by design, mirroring `AuditLogsBackendService._createAuditLog`, which already isolates\n * store failures so one unavailable sink cannot fail an otherwise valid operation. The write is\n * committed by the time this runs, so throwing here would report failure for a change that DID\n * happen — a worse outcome than a logged emission failure.\n */\nasync function emitStagedAppRolesAudit(\n container: Container,\n persistedUserId: string | undefined,\n executionContext: ExecutionContext<any>,\n utils: ResourceOperation_CustomImplementation_Utils,\n): Promise<void> {\n const pending = PENDING_APP_ROLES_AUDIT_BY_EC.get(executionContext);\n if (!pending) return;\n PENDING_APP_ROLES_AUDIT_BY_EC.delete(executionContext);\n\n const subjectUserId = pending.userId ?? persistedUserId;\n if (!subjectUserId) {\n utils.logger.warn?.('Application roles changed but the subject user id could not be resolved — audit event not emitted', {\n operation: pending.operation,\n });\n return;\n }\n\n /*\n * #2297: recorded once the write is durable. Inside a transaction (the one that keeps the operation's audit row) it is a\n * post-commit effect, so a role change that rolls back asserts no grant; without one the write has committed and it is\n * written now. Failures are contained and logged by the helper, as this function always did.\n */\n await emitAuthorityChangeAuditWhenDurable({\n serviceOptions: utils.serviceOptions,\n effectKey: ['user-app-roles-changed', pending.operation, subjectUserId].join(':'),\n logger: utils.logger as LoggerService,\n failureContext: { operation: pending.operation, userId: subjectUserId },\n emit: async () => {\n const auditLogsService = container.get<AuditLogsBackendService>(SAAS_SERVICE_TYPES.AuditLogsService);\n await auditLogsService.logUserAppRolesChanged({\n userId: subjectUserId,\n previousRoles: pending.previousRoles,\n newRoles: pending.newRoles,\n requestedRoles: pending.requestedRoles,\n operation: pending.operation,\n changedBy: resolveAuditActorId(executionContext),\n reason: pending.reason,\n }, executionContext);\n },\n });\n}\n\n/**\n * `assign_roles` / `revoke_roles` — set algebra against the persisted array.\n *\n * Both translate the request contract (`roles`) into the persistence contract (`appRoles`) and return\n * ONLY that patch: the declared `requestDto` names no schema field, so the core UPDATE has nothing to\n * persist unless the real mutation is supplied here (the same contract `ORGANIZATION_UNITS`\n * MOVE / ARCHIVE follow).\n *\n * Union preserves the existing order and appends, so a repeated grant is byte-identical rather than a\n * reshuffle. A full revocation is allowed to leave `appRoles: []` — legitimate offboarding, and\n * recoverable through `assign_roles`. Re-injecting the schema default there was rejected: it would\n * silently re-grant the very role the administrator just revoked.\n */\nfunction createUserRoleSetOperationFactory(\n operationIdentifier: Users_Operations.ASSIGN_ROLES | Users_Operations.REVOKE_ROLES,\n): ResourceCustomServiceImplementationFactory {\n const isAssign = operationIdentifier === Users_Operations.ASSIGN_ROLES;\n\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) => {\n const roleCeilingGate = createRoleCeilingGate(container);\n const superAdminConferralResolver = createSuperAdminConferralResolver(container);\n\n return createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.USERS,\n operationIdentifier,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n debugLabel: `users.${operationIdentifier}.app-roles`,\n /*\n * #3670: the transaction is OWNED here, not borrowed from the audited write's (#2297), because the super-administrator\n * floor serializes concurrent revocations only inside it: it bumps the APPLICATION row and measures there. Borrowing\n * would leave the race open on any deployment where the post-commit outbox cannot capture the attempt. The hooks were\n * read for #2297: the role ceiling is in memory (its refusal audit is written just before the throw), the user row\n * is read through utils.serviceOptions, and the role-change audit row is written once the write is durable.\n */\n transactionalCore: true,\n transactionParticipants: { application: CoreResourceType.APPLICATION },\n handlers: {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n // Fail closed on anything but a single addressed user: the set algebra reads and rewrites\n // ONE row, and a population-addressed write reaching this path would otherwise compute a\n // patch from a row it did not select. A conditional `{ _id, … }` filter is NOT such a case —\n // `_id` pins the row and the extra predicates only decide whether the write lands, so the\n // algebra still reads and rewrites the row it selected.\n const subjectUserId = resolveSingleSubjectId(_id);\n if (subjectUserId === undefined) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n context: {\n message: `Users ${operationIdentifier} requires a single addressed subject`,\n reason: 'user_role_administration_unaddressed',\n receivedAddressing: describeUnaddressableShape(_id),\n },\n });\n }\n\n const requestedRoles = readRequestedRoles(input);\n if (requestedRoles.length === 0) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.SERVICE_RESOURCE_VALIDATION_FAILED,\n context: { reason: 'user_role_administration_no_roles', operationIdentifier, userId: subjectUserId },\n });\n }\n\n assertAppRoleChangeWithinCallerCeiling(requestedRoles, executionContext, utils, roleCeilingGate);\n\n const user = await loadUserForRoleAdministration(subjectUserId, executionContext, utils, operationIdentifier);\n const previousRoles = currentAppRolesOf(user);\n\n const requestedRoleSet = new Set(requestedRoles);\n const newRoles = isAssign\n ? [...previousRoles, ...requestedRoles.filter((role) => !previousRoles.includes(role))]\n : previousRoles.filter((role) => !requestedRoleSet.has(role));\n\n // The last-super-administrator floor. Invoked on BOTH verbs even though only `revoke_roles`\n // can breach it: the transition check short-circuits at zero cost whenever the subject still\n // confers super-admin afterwards, which a union always does — so wiring it to both costs\n // nothing and removes the standing invitation for a future change to the set algebra above\n // to bypass the floor unnoticed. Neither verb touches `status`, so it is passed unchanged.\n await assertSuperAdminFloorPreservedForTransition({\n subjectUserId,\n previousRoles,\n previousStatus: user.status,\n nextRoles: newRoles,\n nextStatus: user.status,\n executionContext,\n repositoriesRegistry: engineRepositoriesRegistry(utils),\n errorBuilder: utils.errorBuilder,\n conferringRoles: superAdminConferralResolver(),\n operationIdentifier,\n // #3670: the floor serializes concurrent revocations only inside the revocation's own transaction.\n transaction: utils.serviceOptions?.transaction,\n });\n\n stageAppRolesAudit(executionContext, {\n userId: subjectUserId,\n previousRoles,\n newRoles,\n requestedRoles,\n operation: operationIdentifier,\n reason: readAuthoredReason(input),\n });\n\n return { [APP_ROLES_FIELD]: newRoles };\n },\n postfixCoreOperations: async (_id, result, executionContext, _operationPath, utils) => {\n await emitStagedAppRolesAudit(container, resolveSingleSubjectId(_id), executionContext, utils);\n return result;\n },\n },\n });\n };\n\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n}\n\n/** `assign_roles` + `revoke_roles`. */\nexport function createUserRoleSetOperationFactories(): ResourceCustomServiceImplementationFactory[] {\n return [\n createUserRoleSetOperationFactory(Users_Operations.ASSIGN_ROLES),\n createUserRoleSetOperationFactory(Users_Operations.REVOKE_ROLES),\n ];\n}\n\n/**\n * Core `CREATE` / `UPDATE` — the paths that actually grant application roles today.\n *\n * `appRoles` is caller-settable on both: neither variant narrows its `requestDto`, and the field\n * carries no `.excludeFromCreate()` / `.excludeFromUpdate()`, so the generated API accepts it. Until\n * this gate landed, nothing measured it against the caller's own authority — the protection was\n * incidental, resting entirely on both operations being gated to `APP_ADMIN_SUPER_ADMIN`, which is\n * precisely the ceiling gate's own bypass. Any application that widens those `roles:` arrays (roles\n * are application-configurable by design) would have turned `appRoles` into an unguarded escalation\n * vector. The role-ceiling utility's own contract asks for this explicitly: wire it into the CREATE\n * and any roles-mutating UPDATE path of every resource whose roles are caller-settable.\n *\n * A request that does not mention `appRoles` is passed through untouched — there is no grant to\n * measure and no authority change to evidence, so neither obligation applies.\n */\nfunction createUserCoreRoleCeilingFactory(\n operationIdentifier: CoreResourceOperation.CREATE | CoreResourceOperation.UPDATE,\n): ResourceCustomServiceImplementationFactory {\n const isCreate = operationIdentifier === CoreResourceOperation.CREATE;\n\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) => {\n const roleCeilingGate = createRoleCeilingGate(container);\n const superAdminConferralResolver = createSuperAdminConferralResolver(container);\n\n return createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.USERS,\n operationIdentifier,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n debugLabel: `users.${isCreate ? 'create' : 'update'}.app-roles-ceiling`,\n // #2297: reviewed for CREATE only (its prefix checks the ceiling in memory; the role-change audit row is written once\n // the write is durable). UPDATE's prefix has not been read for it, so it declares nothing.\n ...(isCreate ? { hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION } : {}),\n handlers: {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n const authoredRoles = readAuthoredAppRoles(input, utils.appRolesGrantedByServer);\n const authoredStatus = readAuthoredStatus(input);\n\n // Neither authority axis is touched ⇒ nothing to gate, evidence or protect. At CREATE an\n // absent `appRoles` is the framework-injected `.default([APP_USER])`, which is\n // policy-derived rather than a caller request and must not face the caller's ceiling; at\n // UPDATE it is simply an edit of other fields.\n //\n // `status` joins the trigger because the super-administrator floor is breached by EITHER\n // axis: stripping the role, or moving the sole administrator into a status that cannot\n // authenticate. `status` is caller-settable on this contract (it carries no\n // `.excludeFromUpdate()` and the variant declares no narrowing `requestDto`), so a\n // roles-only trigger would leave the whole lockout-by-suspension path unguarded.\n if (authoredRoles === undefined && authoredStatus === undefined) return input;\n\n // The ceiling measures GRANTS, so it sees only the authored roles. A status edit confers\n // no authority and is deliberately outside it.\n if (authoredRoles !== undefined) {\n assertAppRoleChangeWithinCallerCeiling(authoredRoles, executionContext, utils, roleCeilingGate);\n }\n\n // CREATE has no prior row: the authored set IS the grant, staged against an empty baseline.\n // The floor is not consulted — a create can only ever ADD to the administrator population.\n if (isCreate) {\n if (authoredRoles !== undefined) {\n stageAppRolesAudit(executionContext, {\n previousRoles: [],\n newRoles: authoredRoles,\n requestedRoles: authoredRoles,\n operation: operationIdentifier,\n });\n }\n return input;\n }\n\n // Resolve the ONE subject this write addresses. A bare id, the `{ id, filter? }` envelope and\n // a conditional `{ _id, … }` filter all name a single row — invitation acceptance activates\n // the invitee through exactly that last form (`{ _id, status: PENDING_INVITATION }`), so the\n // former `typeof _id === 'string'` test refused a legitimate, framework-idiomatic write.\n //\n // Only a genuinely POPULATION-addressed write is refused: it can neither attribute the\n // authority change to one subject, nor name the subjects the floor would have to exclude\n // from its population count. `UPDATE_MANY` is INTERNAL_CALL and reaches a different path.\n const subjectUserId = resolveSingleSubjectId(_id);\n if (subjectUserId === undefined) {\n throw utils.errorBuilder.buildError(ErrorType.VALIDATION, executionContext, {\n context: {\n message: 'Users UPDATE carrying appRoles or status requires a single addressed subject',\n reason: 'user_role_administration_unaddressed',\n receivedAddressing: describeUnaddressableShape(_id),\n },\n });\n }\n\n // Read ONLY when the patch touches authority — an ordinary user edit pays nothing for this.\n const user = await loadUserForRoleAdministration(subjectUserId, executionContext, utils, operationIdentifier);\n const previousRoles = currentAppRolesOf(user);\n\n // An UNAUTHORED axis is unchanged by this write, so the resulting state carries the\n // persisted value forward. Reading `??` as \"absent means empty\" here would be the reverse\n // of the whole not-authored-vs-authored distinction: a status-only edit would look like a\n // full role revocation and a roles-only edit like a status wipe.\n await assertSuperAdminFloorPreservedForTransition({\n subjectUserId,\n previousRoles,\n previousStatus: user.status,\n nextRoles: authoredRoles ?? previousRoles,\n nextStatus: authoredStatus ?? user.status,\n executionContext,\n repositoriesRegistry: engineRepositoriesRegistry(utils),\n errorBuilder: utils.errorBuilder,\n conferringRoles: superAdminConferralResolver(),\n operationIdentifier,\n // #3670: the UPDATE's transaction is owned by the floor's own UPDATE registration (`transactionalCore`).\n transaction: utils.serviceOptions?.transaction,\n });\n\n // Only a ROLE change is an authority change to evidence. A status edit has its own\n // lifecycle semantics and is not a privilege grant, so it does not stage this event.\n if (authoredRoles !== undefined) {\n stageAppRolesAudit(executionContext, {\n userId: subjectUserId,\n previousRoles,\n newRoles: authoredRoles,\n requestedRoles: authoredRoles,\n operation: operationIdentifier,\n });\n }\n\n return input;\n },\n postfixCoreOperations: async (_id, result, executionContext, _operationPath, utils) => {\n // At CREATE the subject id exists only now, on the persisted row.\n const persistedUserId = resolveSingleSubjectId(_id)\n ?? (result && typeof result === 'object' ? (result as { _id?: unknown })._id : undefined);\n\n await emitStagedAppRolesAudit(\n container,\n typeof persistedUserId === 'string' ? persistedUserId : undefined,\n executionContext,\n utils,\n );\n return result;\n },\n },\n });\n };\n\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n}\n\n/** Core `CREATE` + `UPDATE` role-ceiling gates. */\nexport function createUserCoreRoleCeilingFactories(): ResourceCustomServiceImplementationFactory[] {\n return [\n createUserCoreRoleCeilingFactory(CoreResourceOperation.CREATE),\n createUserCoreRoleCeilingFactory(CoreResourceOperation.UPDATE),\n ];\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"user-self-custom-implementations.backend.d.ts","sourceRoot":"","sources":["../../../../src/users/services/user-self-custom-implementations.backend.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAmTpH,eAAO,MAAM,6BAA6B,EAAE,0CAA0C,EAGrF,CAAC"}
1
+ {"version":3,"file":"user-self-custom-implementations.backend.d.ts","sourceRoot":"","sources":["../../../../src/users/services/user-self-custom-implementations.backend.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAqTpH,eAAO,MAAM,6BAA6B,EAAE,0CAA0C,EAGrF,CAAC"}
@@ -171,9 +171,11 @@ function createUsersUpdateFactory() {
171
171
  },
172
172
  });
173
173
  }
174
- // Read fresh, through the caller's own reach: a user outside it is absent and answered 404.
174
+ // Read fresh, through the caller's own reach: a user outside it is absent and answered 404. Inside the UPDATE's
175
+ // transaction (owned by the super-administrator floor's registration since #3670), so the row is read where the
176
+ // write will land rather than from another connection.
175
177
  const currentUser = await engineRepositoriesRegistry(utils).getRepositoryOrThrow(CoreResourceType.USERS, executionContext)
176
- .read(executionContext, subjectUserId);
178
+ .read(executionContext, subjectUserId, utils.serviceOptions);
177
179
  if (!currentUser) {
178
180
  throw utils.errorBuilder.buildError(ErrorType.NOT_FOUND, executionContext, {
179
181
  customMessageReference: ErrorCustomMessageReference.NOT_FOUND,