@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
@@ -1 +1 @@
1
- {"version":3,"file":"super-admin-floor.backend.utils.js","sourceRoot":"","sources":["../../../../src/security/authorizations/super-admin-floor.backend.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * THE last-super-administrator floor — one semantic authority, consumed by every reducing path.\n *\n * assurance-control: WILDO.ACCESS.ADMINISTRATIVE_CONTINUITY — an application must\n * retain an accountable administrative principal; losing the last one is simultaneously an access-\n * control failure and an availability failure, with no in-product remedy.\n *\n * ## The invariant, stated once\n *\n * An application always retains at least one **administratively usable super-administrator**. Every\n * write that could reduce that population below {@link SUPER_ADMIN_FLOOR_MINIMUM} is refused.\n *\n * ## Why this cannot be left to the operation gates\n *\n * Every path that can remove application authority is itself gated on `APP_ADMIN_SUPER_ADMIN`, so\n * the gates are satisfied precisely by the principal capable of causing the lockout. And every\n * recovery door — `assign_roles`, the core USERS `UPDATE`, the admin console — carries the same\n * gate, so once the population reaches zero there is no in-product way back. That combination is\n * what makes this a fail-closed invariant rather than a warning: the failure is unrecoverable, and\n * the only moment it can be prevented is before the write.\n *\n * ## \"Administratively usable\" — three independent conditions, all resolved toward the SMALLER count\n *\n * 1. **The roles must actually CONFER super-admin.** Testing for the literal\n * `APP_ADMIN_SUPER_ADMIN` string is wrong: roles are application-configurable\n * (`initializeRolesWithCustom`), and an app may register a role with\n * `inheritFrom: APP_ADMIN_SUPER_ADMIN` whose holder passes every super-admin gate —\n * `authorize` admits any role whose `inheritFrom` chain CONTAINS the required one. The\n * conferring set is therefore resolved from the live role table at call time; see\n * {@link resolveSuperAdminConferringRoles}.\n * 2. **The subject must be able to AUTHENTICATE.** `_enforceUserStatus` admits `UserStatus.ACTIVE`\n * and throws `AUTHENTICATION_USER_NOT_ACTIVE` for every other status. A `SUSPENDED` or\n * `PENDING_INVITATION` super-admin would satisfy a naive count while being unable to log in —\n * the exact lockout this module exists to prevent, arriving through the guard itself. See\n * {@link ADMINISTRATIVELY_USABLE_USER_STATUSES}.\n * 3. **The row must not be retention-hidden.** An erased-but-retained user KEEPS its `appRoles`:\n * the field carries no `.impersonalizeWith()` because it is `.isAuditEvidence()` — what decided\n * authority must survive the scrub. So a retained row still MATCHES a role filter while being\n * invisible and frozen to every ordinary path. This condition needs no code here: the repository\n * chokepoint applies the retention hide to LIST, so an ordinary request execution context cannot\n * see retained rows. It is stated because the absence of an explicit filter is otherwise\n * indistinguishable from having forgotten the case.\n *\n * The direction of every ambiguity is deliberate. **Under-counting refuses a legitimate removal**\n * — recoverable by appointing another administrator. **Over-counting permits the lockout** — not\n * recoverable at all. Anything uncertain therefore resolves toward not counting.\n *\n * ## The floor is a POPULATION rule, not a self-demotion check\n *\n * Nothing here compares the subject to the caller. Two usable administrators → either may be\n * demoted, including by themselves. One → nobody may remove it. The asymmetry that looks like it\n * should matter does not: to reach any of these operations a human caller already satisfies an\n * `APP_ADMIN_SUPER_ADMIN` gate, so demoting SOMEONE ELSE means the caller is themselves a second\n * usable administrator and the floor already holds. In practice the guard fires only on\n * self-demotion — but it is written as a population rule because the system paths it also protects\n * (subject erasure) have no human caller to compare against.\n *\n * ## No system / internal bypass\n *\n * `assertRequestedRolesWithinCallerCeiling` exempts `INTERNALLY_INITIATED_REQUEST` because a system\n * write carries no human ceiling to measure. This module deliberately does NOT copy that exemption:\n * a system write that strips the last administrator produces exactly the same unadministrable\n * application as a human one. The concrete case is subject erasure, and refusing it is the correct\n * compliance answer rather than an obstacle to one — an erasure that leaves the application\n * unadministrable is an availability incident, not a completed data-protection request. The\n * operator appoints a successor and then erases; {@link SUPER_ADMIN_FLOOR_REMEDY} is the text that\n * says so.\n */\n\nimport {\n AdministrativeContinuityErrorCode,\n CORE_APP_ROLES,\n CoreResourceOperation,\n CoreResourceType,\n ErrorCustomMessageReference,\n ErrorType,\n ResourcePrimaryScope,\n UserStatus,\n type User,\n} from '@wildo-ai/saas-models';\nimport type { Container } from 'inversify';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport type { ErrorBuilderBackendService } from '~wildo-backend-lib/errors/services/error-builder.backend.service';\nimport type { AuthorizationsBackendService } from '~wildo-backend-lib/security/authorizations/services/authorizations.backend.service';\nimport type { RepositoriesRegistryHandlerBackendService } from '~wildo-backend-lib/base-classes/services/repositories/repositories-registry-handler.backend.service';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport { resolveRolesConferringAnchor } from '~wildo-backend-lib/security/authorizations/role-conferral.backend.utils';\n\n/**\n * The minimum number of administratively usable super-administrators an application must retain.\n *\n * Named rather than inlined as `> 0` so the several call sites state the same rule, and so a\n * product that ever requires two (a four-eyes administrative policy) has one place to change.\n */\nexport const SUPER_ADMIN_FLOOR_MINIMUM = 1;\n\n/**\n * The statuses in which a user can actually authenticate, and therefore the only ones that count\n * toward the floor.\n *\n * Derived from `AuthenticationOrchestrator._enforceUserStatus`, which returns for\n * {@link UserStatus.ACTIVE} and throws `AUTHENTICATION_USER_NOT_ACTIVE` for everything else.\n *\n * `PENDING_VERIFICATION` is deliberately EXCLUDED even though the verification flow can flip it to\n * `ACTIVE`: a recoverable administrator is not a usable one, and admitting it would let the floor\n * be satisfied by an account nobody can currently log into. Same for `PENDING_INVITATION`, whose\n * subject has no credentials at all.\n *\n * Kept as an explicit list rather than `!== ACTIVE` so that adding a member to {@link UserStatus}\n * forces a decision here instead of silently inheriting \"usable\".\n */\nexport const ADMINISTRATIVELY_USABLE_USER_STATUSES: readonly UserStatus[] = [UserStatus.ACTIVE];\n\n/**\n * The remedy every refusal names.\n *\n * A guard that only says \"no\" on an operation an administrator is authorized to perform reads as a\n * defect. Stating the one action that unblocks it — appoint another administrator first — is what\n * makes the refusal actionable, and it is the same remedy for a human demotion and for a subject\n * erasure blocked by the floor.\n */\nexport const SUPER_ADMIN_FLOOR_REMEDY =\n 'Grant an application super-administrator role to another active user first, then retry.';\n\n/**\n * The reason code every floor refusal carries, so one filter finds them across the trail.\n *\n * INTERNAL. Not the wire contract — `sanitizeErrorForClient` strips `reason` from a CONFLICT and\n * allow-lists `code`, which carries {@link AdministrativeContinuityErrorCode}. Keep them distinct:\n * this one is free to stay richer than the client-facing code.\n */\nexport const SUPER_ADMIN_FLOOR_REFUSAL_REASON = 'last_super_admin_floor';\n\n/**\n * Why a subject would stop counting toward the floor. Rides the refusal so an operator reading the\n * error (or the audit trail) knows which axis of {@link ADMINISTRATIVELY_USABLE_USER_STATUSES} /\n * role conferral / row existence the blocked write was about to change.\n */\nexport enum SuperAdminFloorReductionCause {\n /** The subject's `appRoles` would no longer confer super-admin. */\n ROLE_REMOVAL = 'role_removal',\n /** The subject's `status` would leave the authenticable set. */\n STATUS_CHANGE = 'status_change',\n /** The subject's row would be destroyed. */\n DELETION = 'deletion',\n /** The subject's row would be scrubbed and retention-hidden. */\n ERASURE = 'erasure',\n}\n\n/**\n * Resolves the role keys that confer super-admin, from the LIVE application role table.\n *\n * Injected rather than imported for the same reason {@link RoleHierarchyResolver} is: the effective\n * hierarchy is application-configurable, so a statically-derived set would silently disagree with\n * the roles the authorization layer actually honours.\n */\nexport type SuperAdminConferralResolver = () => string[];\n\n/**\n * Every APPLICATION-scope role key whose `inheritFrom` chain reaches `APP_ADMIN_SUPER_ADMIN` — i.e.\n * every role whose holder passes a super-admin gate.\n *\n * This floor's BINDING of the shared conferral derivation. The derivation itself, and the reasoning\n * that used to be restated here and in the organization-owner floor in two different sets of words,\n * live in {@link resolveRolesConferringAnchor} — including why the anchor is seeded unconditionally\n * and why the SCOPE argument is what makes an org-scoped role string that happens to spell an app\n * role confer nothing. `appRoles` is the APPLICATION authority axis; ORGANIZATIONS-scope roles are\n * never consulted, and that is now one fact in one place rather than two paragraphs that can drift.\n */\nexport function resolveSuperAdminConferringRoles(\n authorizationsService: Pick<AuthorizationsBackendService, 'getAppRoles' | 'getRoleHierarchy'>,\n): string[] {\n return resolveRolesConferringAnchor({\n anchorRole: CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN,\n scope: ResourcePrimaryScope.APPLICATION,\n declaredRoleKeys: Object.keys(authorizationsService.getAppRoles() ?? {}),\n roleHierarchyResolver: (roleKey, scope) => authorizationsService.getRoleHierarchy(roleKey as any, scope),\n });\n}\n\n/**\n * Bind a {@link SuperAdminConferralResolver} to the container's live `AuthorizationsBackendService`.\n *\n * Resolution is LAZY then per-call, exactly like `createRoleHierarchyResolver`: custom-implementation\n * factories are constructed during module registration, which can run before the authorization\n * service is bound (an eager `container.get` would turn this into a startup crash), and the role\n * table is mutated in place by `initializeRolesWithCustom`, so the conferring set is recomputed per\n * call rather than memoized. The set is a handful of strings over a table of the same size — the\n * recomputation is far cheaper than the staleness it avoids.\n */\nexport function createSuperAdminConferralResolver(container: Container): SuperAdminConferralResolver {\n let authorizationsService: AuthorizationsBackendService | undefined;\n return () => {\n authorizationsService ??= container.get<AuthorizationsBackendService>(SAAS_SERVICE_TYPES.AuthorizationsService);\n return resolveSuperAdminConferringRoles(authorizationsService);\n };\n}\n\n/**\n * Whether a role array confers super-admin.\n *\n * Defensive on the array itself: this runs on values read from unparsed caller input as well as on\n * persisted rows, and a non-array is treated as conferring nothing — which can only ever make the\n * count smaller, the safe direction (see the module header).\n */\nexport function rolesConferSuperAdmin(roles: unknown, conferringRoles: readonly string[]): boolean {\n if (!Array.isArray(roles)) return false;\n const conferringRoleSet = new Set(conferringRoles);\n return roles.some((role) => typeof role === 'string' && conferringRoleSet.has(role));\n}\n\n/** Whether a status leaves the subject able to authenticate — see {@link ADMINISTRATIVELY_USABLE_USER_STATUSES}. */\nexport function isAdministrativelyUsableStatus(status: unknown): boolean {\n return ADMINISTRATIVELY_USABLE_USER_STATUSES.includes(status as UserStatus);\n}\n\n/** Whether a user row, as it stands, counts as one administratively usable super-administrator. */\nexport function isUsableSuperAdmin(\n user: Pick<User, 'appRoles' | 'status'> | undefined | null,\n conferringRoles: readonly string[],\n): boolean {\n if (!user) return false;\n return rolesConferSuperAdmin(user.appRoles, conferringRoles) && isAdministrativelyUsableStatus(user.status);\n}\n\n/** The narrow row shape the floor count reads. */\ntype SuperAdminFloorCandidateRow = Pick<User, '_id'>;\n\n/**\n * Count the administratively usable super-administrators OTHER than the named subjects.\n *\n * ## Why a LIST and not a `count`\n *\n * `RepositoryResource.count` requires the resource to declare `CoreResourceOperation.COUNT`, and\n * `USERS` does not — it declares READ / LIST / SEARCH / CREATE / UPDATE / UPDATE_MANY / DELETE. A\n * LIST over an indexed (`appRoles` is `.isDBIndexed()`) predicate that selects only application\n * super-administrators returns a population that is inherently tiny, so materialising it costs\n * nothing worth engineering around. Adding a COUNT operation to `USERS` purely to serve this guard\n * would open a new operation surface (and its DTO, its ACL, its route decision) for an internal\n * arithmetic need — a worse trade.\n *\n * ## Why the subjects are excluded in memory rather than in the query\n *\n * A `_id: {$nin: [...]}` fragment would have to agree with the adapter's id representation —\n * MongoDB stores `_id` as an `ObjectId` while every id crossing the service boundary is a string,\n * and a silent cast miss would leave the subject INSIDE the count, satisfying the floor with the\n * very row about to be removed. Comparing `String(row._id)` after the fact cannot fail that way and\n * is exact on both adapters.\n *\n * ## Retention\n *\n * No retention fragment is built here on purpose. `buildContextualFilter` applies the hide at the\n * repository chokepoint for LIST, so retained rows are already absent. Re-stating it here would be\n * a second copy of a rule the retention authority owns — the precise duplication that produced the\n * G-AGG defect it was created to end.\n */\nexport async function countOtherUsableSuperAdmins(params: {\n excludedSubjectIds: readonly string[];\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n}): Promise<number> {\n const { excludedSubjectIds, executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier } = params;\n\n const userRepository = repositoriesRegistry.getRepository(CoreResourceType.USERS);\n\n // Fail closed. Without the repository the population is unknowable, and answering \"0\" would\n // refuse every removal while answering \"many\" would permit the lockout. Neither is a guess worth\n // making on an invariant with no recovery path.\n if (!userRepository) {\n throw errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n context: {\n message: 'Cannot evaluate the super-administrator floor: the users repository is not registered',\n resourceType: CoreResourceType.USERS,\n operationIdentifier,\n },\n });\n }\n\n const candidates = await userRepository.list<SuperAdminFloorCandidateRow>(executionContext, {\n appRoles: { $in: [...conferringRoles] },\n status: { $in: [...ADMINISTRATIVELY_USABLE_USER_STATUSES] },\n });\n\n const excludedSubjectIdSet = new Set(excludedSubjectIds.map((subjectId) => String(subjectId)));\n const rows: SuperAdminFloorCandidateRow[] = Array.isArray(candidates) ? candidates : [];\n\n return rows.filter((row) => !excludedSubjectIdSet.has(String(row?._id))).length;\n}\n\n/**\n * Refuse a write that would leave the application without an administratively usable\n * super-administrator.\n *\n * Call this ONLY once the caller has established that the named subjects currently count toward the\n * floor and would stop counting — the population query is skipped entirely otherwise, so an\n * ordinary user edit, an additive grant, or the deletion of a non-administrator pays nothing.\n *\n * assurance-control: WILDO.ACCESS.ADMINISTRATIVE_CONTINUITY — refuse the removal of the last\n * accountable administrative principal; the denial is an access-control decision and is recorded as\n * one by the operation's own error path.\n */\nexport async function assertSuperAdminFloorPreserved(params: {\n /** The subjects about to stop counting. Excluded from the population before it is measured. */\n subjectUserIds: readonly string[];\n cause: SuperAdminFloorReductionCause;\n executionContext: ExecutionContext<any>;\n /**\n * The context the POPULATION is measured through, resolved only once a reduction is established.\n * Omitted, the caller's `executionContext` measures it, which is right for the administrator verbs:\n * their caller holds a super-administrator role at APPLICATION scope and lists every user.\n *\n * A self-service door is different. `user_self | deactivate_account` reaches this floor with the\n * person's own context, which confines a USERS list to their own row, so the count of OTHER\n * administrators is zero whoever else exists. That fails closed, but on the wrong answer: a second\n * administrator could never deactivate their own account. The lifecycle transition therefore passes\n * the system population context here. Refusals are still built on the caller's context, so the\n * error names the request that was refused.\n */\n resolvePopulationExecutionContext?: () => Promise<ExecutionContext<any>>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n}): Promise<void> {\n const { subjectUserIds, cause, executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier } = params;\n\n const remainingUsableSuperAdmins = await countOtherUsableSuperAdmins({\n excludedSubjectIds: subjectUserIds,\n executionContext: params.resolvePopulationExecutionContext\n ? await params.resolvePopulationExecutionContext()\n : executionContext,\n repositoriesRegistry,\n errorBuilder,\n conferringRoles,\n operationIdentifier,\n });\n\n if (remainingUsableSuperAdmins >= SUPER_ADMIN_FLOOR_MINIMUM) return;\n\n // CONFLICT, not AUTHORIZATION and not VALIDATION. The request is well-formed and the caller IS\n // authorized — every one of these operations is gated on the very role being protected. What\n // forbids the write is the current STATE of the application, which is what CONFLICT means. A 403\n // would tell an administrator to acquire authority they already hold (and none exists that lifts\n // this floor); a 400 would tell them to fix a payload that is correct.\n throw errorBuilder.buildError(ErrorType.CONFLICT, executionContext, {\n // Its own words (#3131): the generic conflict message tells the person to reload, and no reload lifts a floor.\n customMessageReference: ErrorCustomMessageReference.LAST_APPLICATION_SUPER_ADMIN,\n context: {\n // The WIRE contract — allow-listed by `sanitizeErrorForClient`, so this is what a client\n // switches on. Distinct from `reason` below, which is the internal audit/log filter handle and\n // is deliberately stripped at the boundary; the two must not be conflated.\n code: AdministrativeContinuityErrorCode.LAST_APPLICATION_SUPER_ADMIN,\n reason: SUPER_ADMIN_FLOOR_REFUSAL_REASON,\n message:\n 'Refused: this would leave the application with no usable super-administrator. '\n + SUPER_ADMIN_FLOOR_REMEDY,\n cause,\n operationIdentifier,\n subjectUserIds: [...subjectUserIds],\n remainingUsableSuperAdmins,\n requiredUsableSuperAdmins: SUPER_ADMIN_FLOOR_MINIMUM,\n conferringRoles: [...conferringRoles],\n usableStatuses: [...ADMINISTRATIVELY_USABLE_USER_STATUSES],\n },\n });\n}\n\n/**\n * The floor check for a role/status TRANSITION on a single subject.\n *\n * Encapsulates the short-circuit that keeps the population query off the hot path: the floor can\n * only be threatened when the subject counted BEFORE the write and would not count AFTER it. A\n * subject who was never a usable super-administrator, or who remains one, costs a set membership\n * test and nothing else.\n *\n * That shape is also why the additive `assign_roles` verb can call this unconditionally — a union\n * can never drop conferral, so it always short-circuits — which removes the standing invitation for\n * a future change to the set algebra to bypass the floor unnoticed.\n */\nexport async function assertSuperAdminFloorPreservedForTransition(params: {\n subjectUserId: string;\n previousRoles: unknown;\n previousStatus: unknown;\n nextRoles: unknown;\n nextStatus: unknown;\n executionContext: ExecutionContext<any>;\n /** See {@link assertSuperAdminFloorPreserved}; never resolved when the short-circuits below return. */\n resolvePopulationExecutionContext?: () => Promise<ExecutionContext<any>>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n}): Promise<void> {\n const {\n subjectUserId, previousRoles, previousStatus, nextRoles, nextStatus,\n executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier,\n } = params;\n\n const countedBefore = rolesConferSuperAdmin(previousRoles, conferringRoles) && isAdministrativelyUsableStatus(previousStatus);\n if (!countedBefore) return;\n\n const stillConfersRole = rolesConferSuperAdmin(nextRoles, conferringRoles);\n const stillUsableStatus = isAdministrativelyUsableStatus(nextStatus);\n if (stillConfersRole && stillUsableStatus) return;\n\n await assertSuperAdminFloorPreserved({\n subjectUserIds: [subjectUserId],\n // Which axis actually broke it. Role removal is named first when BOTH change in one write: it\n // is the axis the operator authored against the authority field, and the more specific finding.\n cause: !stillConfersRole ? SuperAdminFloorReductionCause.ROLE_REMOVAL : SuperAdminFloorReductionCause.STATUS_CHANGE,\n executionContext,\n resolvePopulationExecutionContext: params.resolvePopulationExecutionContext,\n repositoriesRegistry,\n errorBuilder,\n conferringRoles,\n operationIdentifier,\n });\n}\n\n/**\n * The floor check for a write that removes the subject's row wholesale — a hard DELETE or a subject\n * erasure (which scrubs the row and retention-hides it, so it stops counting just as decisively).\n *\n * Takes the subjects' CURRENT rows rather than a transition: there is no \"after\" state to compare,\n * the row simply stops counting. Subjects that did not count are filtered out first, so deleting a\n * hundred ordinary users never touches the population query.\n */\nexport async function assertSuperAdminFloorPreservedForRemoval(params: {\n subjects: readonly (Pick<User, '_id' | 'appRoles' | 'status'> | undefined | null)[];\n cause: SuperAdminFloorReductionCause;\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n}): Promise<void> {\n const { subjects, cause, executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier } = params;\n\n const removedUsableSuperAdminIds = subjects\n .filter((subject) => isUsableSuperAdmin(subject, conferringRoles))\n .map((subject) => String(subject!._id));\n\n if (removedUsableSuperAdminIds.length === 0) return;\n\n await assertSuperAdminFloorPreserved({\n subjectUserIds: removedUsableSuperAdminIds,\n cause,\n executionContext,\n repositoriesRegistry,\n errorBuilder,\n conferringRoles,\n operationIdentifier,\n });\n}\n\n/**\n * The resource types whose rows carry the application authority axis, and therefore the subject\n * types whose erasure can breach the floor.\n *\n * `USERS` and `USER_SELF` are two resource configurations over the SAME collection (see\n * `getCollectionNameForResource`), both declaring `RETAIN_AND_IMPERSONALIZE`, so an erasure\n * addressed through either door scrubs and hides the same row.\n */\nexport const SUPER_ADMIN_FLOOR_GOVERNED_SUBJECT_RESOURCE_TYPES: readonly string[] = [\n CoreResourceType.USERS,\n CoreResourceType.USER_SELF,\n];\n\n/** Whether an erasure of this subject resource type can reduce the super-administrator population. */\nexport function isSuperAdminFloorGovernedSubjectResource(resourceType: unknown): boolean {\n return SUPER_ADMIN_FLOOR_GOVERNED_SUBJECT_RESOURCE_TYPES.includes(String(resourceType));\n}\n\n/**\n * Read the subjects an erasure is about to scrub, for the floor check.\n *\n * Reads through the repository so the retention hide applies: a subject ALREADY erased is invisible\n * here, which is correct — it stopped counting at its own erasure, and a re-run must not be refused\n * on account of a row that no longer contributes. A subject that cannot be read is simply absent\n * and contributes nothing, which is the safe direction for a check whose job is to notice the\n * subjects that DO count.\n */\nexport async function readSubjectsForSuperAdminFloor(params: {\n subjectIds: readonly string[];\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n operationIdentifier: string;\n}): Promise<Pick<User, '_id' | 'appRoles' | 'status'>[]> {\n const { subjectIds, executionContext, repositoriesRegistry, errorBuilder, operationIdentifier } = params;\n\n const userRepository = repositoriesRegistry.getRepository(CoreResourceType.USERS);\n\n if (!userRepository) {\n throw errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n context: {\n message: 'Cannot evaluate the super-administrator floor: the users repository is not registered',\n resourceType: CoreResourceType.USERS,\n operationIdentifier,\n },\n });\n }\n\n const subjects = await userRepository.list<Pick<User, '_id' | 'appRoles' | 'status'>>(\n executionContext,\n [...subjectIds],\n );\n\n return Array.isArray(subjects) ? subjects : [];\n}\n\n/**\n * Measure the floor without refusing anything — the boot-time report.\n *\n * Separate from {@link assertSuperAdminFloorPreserved} because it answers a different question at a\n * moment when refusing is not an option: a fresh application legitimately boots with zero users\n * (the first administrator arrives through seeding or first-run registration), so a startup that\n * failed on this condition would brick every greenfield app. What it CAN do is observe the states\n * no write guard ever saw — a direct database edit, a restore from a backup taken mid-erasure, a\n * seed that authored no administrator, or a role-table reconfiguration that changed which roles\n * confer super-admin out from under an existing population.\n */\nexport async function measureUsableSuperAdminPopulation(params: {\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n}): Promise<{ usableSuperAdminCount: number; meetsFloor: boolean; conferringRoles: string[] }> {\n const usableSuperAdminCount = await countOtherUsableSuperAdmins({\n excludedSubjectIds: [],\n executionContext: params.executionContext,\n repositoriesRegistry: params.repositoriesRegistry,\n errorBuilder: params.errorBuilder,\n conferringRoles: params.conferringRoles,\n operationIdentifier: CoreResourceOperation.LIST,\n });\n\n return {\n usableSuperAdminCount,\n meetsFloor: usableSuperAdminCount >= SUPER_ADMIN_FLOOR_MINIMUM,\n conferringRoles: [...params.conferringRoles],\n };\n}\n\n/** One acknowledged breach of the application floor — reported, never thrown. */\nexport type SuperAdminFloorBreach = {\n erasedUsableSuperAdminIds: string[];\n conferringRoles: string[];\n};\n\n/**\n * The erasure door: refuse by default, defeasible by an explicit controller acknowledgement.\n *\n * The application twin of `assertOrganizationOwnerFloorForSubjectErasure`, and it exists for the same\n * reason that one does — so the POLICY lives in the authority rather than in whichever service\n * happens to call it. A caller that owns the decision must not also own the rule.\n *\n * ## Why this door alone is defeasible\n *\n * See `.claude/rules/administrative-continuity-floors.md`. In short: GDPR Art. 17(3) is a closed list\n * of grounds for refusing an erasure and \"the application would become unadministrable\" is not on it,\n * while Art. 12(3) requires an answer without undue delay. A framework that refuses indefinitely\n * takes that choice away from the application, which is the controller that owes the data subject an\n * answer. Every ADMINISTRATIVE door — `revoke_roles`, core UPDATE, core DELETE — stays unconditional,\n * because nobody holds a right to have their administrative role removed.\n *\n * ## Why the stakes argue FOR the escape, not against it\n *\n * A stranded application is worse than a stranded tenant: there is no scope above it, so there is no\n * higher-tier repair door to reach for, and recovery is necessarily a seed or a direct database\n * write. That is precisely why the escape must be explicit, per-request, non-default and LOUD — the\n * operator's only chance to act is the record this emits, since nobody will discover the state by\n * being denied something later.\n */\nexport async function assertSuperAdminFloorForSubjectErasure(params: {\n subjects: readonly (Pick<User, '_id' | 'appRoles' | 'status'> | undefined | null)[];\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n /** The controller's explicit decision to complete an erasure that strands the application. */\n acknowledgeFloorBreach?: boolean;\n /** Invoked when the breach is acknowledged AND real. Never on the refusal path. */\n onAcknowledgedBreach?: (breach: SuperAdminFloorBreach) => Promise<void> | void;\n}): Promise<void> {\n if (!params.acknowledgeFloorBreach) {\n await assertSuperAdminFloorPreservedForRemoval({\n ...params,\n cause: SuperAdminFloorReductionCause.ERASURE,\n });\n return;\n }\n\n // Acknowledged: nothing is refused, so the only remaining obligation is that the consequence is\n // never silent. The same usability test still runs — an acknowledged erasure that removes no\n // usable administrator must stay quiet, or the alert stops meaning anything.\n const erasedUsableSuperAdminIds = params.subjects\n .filter((subject) => isUsableSuperAdmin(subject, params.conferringRoles))\n .map((subject) => String(subject!._id));\n\n if (erasedUsableSuperAdminIds.length === 0) return;\n\n await params.onAcknowledgedBreach?.({\n erasedUsableSuperAdminIds,\n conferringRoles: [...params.conferringRoles],\n });\n}\n"]}
1
+ {"version":3,"file":"super-admin-floor.backend.utils.js","sourceRoot":"","sources":["../../../../src/security/authorizations/super-admin-floor.backend.utils.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * THE last-super-administrator floor — one semantic authority, consumed by every reducing path.\n *\n * assurance-control: WILDO.ACCESS.ADMINISTRATIVE_CONTINUITY — an application must\n * retain an accountable administrative principal; losing the last one is simultaneously an access-\n * control failure and an availability failure, with no in-product remedy.\n *\n * ## The invariant, stated once\n *\n * An application always retains at least one **administratively usable super-administrator**. Every\n * write that could reduce that population below {@link SUPER_ADMIN_FLOOR_MINIMUM} is refused.\n *\n * ## Why this cannot be left to the operation gates\n *\n * Every path that can remove application authority is itself gated on `APP_ADMIN_SUPER_ADMIN`, so\n * the gates are satisfied precisely by the principal capable of causing the lockout. And every\n * recovery door — `assign_roles`, the core USERS `UPDATE`, the admin console — carries the same\n * gate, so once the population reaches zero there is no in-product way back. That combination is\n * what makes this a fail-closed invariant rather than a warning: the failure is unrecoverable, and\n * the only moment it can be prevented is before the write.\n *\n * ## \"Administratively usable\" — three independent conditions, all resolved toward the SMALLER count\n *\n * 1. **The roles must actually CONFER super-admin.** Testing for the literal\n * `APP_ADMIN_SUPER_ADMIN` string is wrong: roles are application-configurable\n * (`initializeRolesWithCustom`), and an app may register a role with\n * `inheritFrom: APP_ADMIN_SUPER_ADMIN` whose holder passes every super-admin gate —\n * `authorize` admits any role whose `inheritFrom` chain CONTAINS the required one. The\n * conferring set is therefore resolved from the live role table at call time; see\n * {@link resolveSuperAdminConferringRoles}.\n * 2. **The subject must be able to AUTHENTICATE.** `_enforceUserStatus` admits `UserStatus.ACTIVE`\n * and throws `AUTHENTICATION_USER_NOT_ACTIVE` for every other status. A `SUSPENDED` or\n * `PENDING_INVITATION` super-admin would satisfy a naive count while being unable to log in —\n * the exact lockout this module exists to prevent, arriving through the guard itself. See\n * {@link ADMINISTRATIVELY_USABLE_USER_STATUSES}.\n * 3. **The row must not be retention-hidden.** An erased-but-retained user KEEPS its `appRoles`:\n * the field carries no `.impersonalizeWith()` because it is `.isAuditEvidence()` — what decided\n * authority must survive the scrub. So a retained row still MATCHES a role filter while being\n * invisible and frozen to every ordinary path. This condition needs no code here: the repository\n * chokepoint applies the retention hide to LIST, so an ordinary request execution context cannot\n * see retained rows. It is stated because the absence of an explicit filter is otherwise\n * indistinguishable from having forgotten the case.\n *\n * The direction of every ambiguity is deliberate. **Under-counting refuses a legitimate removal**\n * — recoverable by appointing another administrator. **Over-counting permits the lockout** — not\n * recoverable at all. Anything uncertain therefore resolves toward not counting.\n *\n * ## The floor is a POPULATION rule, not a self-demotion check\n *\n * Nothing here compares the subject to the caller. Two usable administrators → either may be\n * demoted, including by themselves. One → nobody may remove it. The asymmetry that looks like it\n * should matter does not: to reach any of these operations a human caller already satisfies an\n * `APP_ADMIN_SUPER_ADMIN` gate, so demoting SOMEONE ELSE means the caller is themselves a second\n * usable administrator and the floor already holds. In practice the guard fires only on\n * self-demotion — but it is written as a population rule because the system paths it also protects\n * (subject erasure) have no human caller to compare against.\n *\n * ## No system / internal bypass\n *\n * `assertRequestedRolesWithinCallerCeiling` exempts `INTERNALLY_INITIATED_REQUEST` because a system\n * write carries no human ceiling to measure. This module deliberately does NOT copy that exemption:\n * a system write that strips the last administrator produces exactly the same unadministrable\n * application as a human one. The concrete case is subject erasure, and refusing it is the correct\n * compliance answer rather than an obstacle to one — an erasure that leaves the application\n * unadministrable is an availability incident, not a completed data-protection request. The\n * operator appoints a successor and then erases; {@link SUPER_ADMIN_FLOOR_REMEDY} is the text that\n * says so.\n *\n * ## Concurrent reductions are serialized on the APPLICATION row (#3670)\n *\n * A count-then-write guard is open to write skew: administrator A removes B while B removes A, each\n * measures the other still present, both commit, and the application has none. No extra check closes\n * that, because each transaction's view is internally consistent. So, given the reducing operation's\n * `transaction`, the floor first bumps `Application.superAdminFloorSerializationToken` inside it and\n * only then measures: two concurrent reductions write the same row, and the store makes the second wait\n * (PostgreSQL) or retry (MongoDB) until the first has committed, after which its measurement sees the\n * removal. See {@link bumpSuperAdminFloorSerializationToken}.\n *\n * The collision exists only INSIDE the callers' transactions, so every reducing door forwards its own:\n *\n * | door | transaction |\n * |---|---|\n * | `users / assign_roles`, `revoke_roles` | `transactionalCore` on the role-administration registration |\n * | `users / update` (`appRoles`) | `transactionalCore` on the floor's UPDATE registration |\n * | `users / delete` | the audited write's own transaction (#2297), whenever the post-commit outbox can capture it |\n * | the account-lifecycle verbs (`suspend_user`, `deactivate_account`, …) | their `transactionalCore` |\n * | subject erasure | none: its guard runs before the erasure's writes, outside any transaction, so it stays unserialized, exactly as the organization owner floor's erasure guard does |\n *\n * Without a transaction the bump is skipped rather than autocommitted: outside one it serializes nothing\n * and would only add a write.\n */\n\nimport {\n AdministrativeContinuityErrorCode,\n CORE_APP_ROLES,\n CoreResourceOperation,\n CoreResourceType,\n ErrorCustomMessageReference,\n ErrorType,\n ResourcePrimaryScope,\n UserStatus,\n type User,\n} from '@wildo-ai/saas-models';\nimport type { Container } from 'inversify';\nimport type { ExecutionContext } from '~wildo-backend-lib/runtime/models/execution-context.backend.schemas';\nimport type { TransactionContext } from '~wildo-backend-lib/databases/models/transaction.shared.schemas';\nimport type { ErrorBuilderBackendService } from '~wildo-backend-lib/errors/services/error-builder.backend.service';\nimport type { AuthorizationsBackendService } from '~wildo-backend-lib/security/authorizations/services/authorizations.backend.service';\nimport type { RepositoriesRegistryHandlerBackendService } from '~wildo-backend-lib/base-classes/services/repositories/repositories-registry-handler.backend.service';\nimport { SAAS_SERVICE_TYPES } from '~wildo-backend-lib/inject/models/service-types';\nimport { resolveRolesConferringAnchor } from '~wildo-backend-lib/security/authorizations/role-conferral.backend.utils';\n\n/**\n * The minimum number of administratively usable super-administrators an application must retain.\n *\n * Named rather than inlined as `> 0` so the several call sites state the same rule, and so a\n * product that ever requires two (a four-eyes administrative policy) has one place to change.\n */\nexport const SUPER_ADMIN_FLOOR_MINIMUM = 1;\n\n/**\n * The statuses in which a user can actually authenticate, and therefore the only ones that count\n * toward the floor.\n *\n * Derived from `AuthenticationOrchestrator._enforceUserStatus`, which returns for\n * {@link UserStatus.ACTIVE} and throws `AUTHENTICATION_USER_NOT_ACTIVE` for everything else.\n *\n * `PENDING_VERIFICATION` is deliberately EXCLUDED even though the verification flow can flip it to\n * `ACTIVE`: a recoverable administrator is not a usable one, and admitting it would let the floor\n * be satisfied by an account nobody can currently log into. Same for `PENDING_INVITATION`, whose\n * subject has no credentials at all.\n *\n * Kept as an explicit list rather than `!== ACTIVE` so that adding a member to {@link UserStatus}\n * forces a decision here instead of silently inheriting \"usable\".\n */\nexport const ADMINISTRATIVELY_USABLE_USER_STATUSES: readonly UserStatus[] = [UserStatus.ACTIVE];\n\n/**\n * The remedy every refusal names.\n *\n * A guard that only says \"no\" on an operation an administrator is authorized to perform reads as a\n * defect. Stating the one action that unblocks it — appoint another administrator first — is what\n * makes the refusal actionable, and it is the same remedy for a human demotion and for a subject\n * erasure blocked by the floor.\n */\nexport const SUPER_ADMIN_FLOOR_REMEDY =\n 'Grant an application super-administrator role to another active user first, then retry.';\n\n/**\n * The reason code every floor refusal carries, so one filter finds them across the trail.\n *\n * INTERNAL. Not the wire contract — `sanitizeErrorForClient` strips `reason` from a CONFLICT and\n * allow-lists `code`, which carries {@link AdministrativeContinuityErrorCode}. Keep them distinct:\n * this one is free to stay richer than the client-facing code.\n */\nexport const SUPER_ADMIN_FLOOR_REFUSAL_REASON = 'last_super_admin_floor';\n\n/**\n * Why a subject would stop counting toward the floor. Rides the refusal so an operator reading the\n * error (or the audit trail) knows which axis of {@link ADMINISTRATIVELY_USABLE_USER_STATUSES} /\n * role conferral / row existence the blocked write was about to change.\n */\nexport enum SuperAdminFloorReductionCause {\n /** The subject's `appRoles` would no longer confer super-admin. */\n ROLE_REMOVAL = 'role_removal',\n /** The subject's `status` would leave the authenticable set. */\n STATUS_CHANGE = 'status_change',\n /** The subject's row would be destroyed. */\n DELETION = 'deletion',\n /** The subject's row would be scrubbed and retention-hidden. */\n ERASURE = 'erasure',\n}\n\n/**\n * Resolves the role keys that confer super-admin, from the LIVE application role table.\n *\n * Injected rather than imported for the same reason {@link RoleHierarchyResolver} is: the effective\n * hierarchy is application-configurable, so a statically-derived set would silently disagree with\n * the roles the authorization layer actually honours.\n */\nexport type SuperAdminConferralResolver = () => string[];\n\n/**\n * Every APPLICATION-scope role key whose `inheritFrom` chain reaches `APP_ADMIN_SUPER_ADMIN` — i.e.\n * every role whose holder passes a super-admin gate.\n *\n * This floor's BINDING of the shared conferral derivation. The derivation itself, and the reasoning\n * that used to be restated here and in the organization-owner floor in two different sets of words,\n * live in {@link resolveRolesConferringAnchor} — including why the anchor is seeded unconditionally\n * and why the SCOPE argument is what makes an org-scoped role string that happens to spell an app\n * role confer nothing. `appRoles` is the APPLICATION authority axis; ORGANIZATIONS-scope roles are\n * never consulted, and that is now one fact in one place rather than two paragraphs that can drift.\n */\nexport function resolveSuperAdminConferringRoles(\n authorizationsService: Pick<AuthorizationsBackendService, 'getAppRoles' | 'getRoleHierarchy'>,\n): string[] {\n return resolveRolesConferringAnchor({\n anchorRole: CORE_APP_ROLES.APP_ADMIN_SUPER_ADMIN,\n scope: ResourcePrimaryScope.APPLICATION,\n declaredRoleKeys: Object.keys(authorizationsService.getAppRoles() ?? {}),\n roleHierarchyResolver: (roleKey, scope) => authorizationsService.getRoleHierarchy(roleKey as any, scope),\n });\n}\n\n/**\n * Bind a {@link SuperAdminConferralResolver} to the container's live `AuthorizationsBackendService`.\n *\n * Resolution is LAZY then per-call, exactly like `createRoleHierarchyResolver`: custom-implementation\n * factories are constructed during module registration, which can run before the authorization\n * service is bound (an eager `container.get` would turn this into a startup crash), and the role\n * table is mutated in place by `initializeRolesWithCustom`, so the conferring set is recomputed per\n * call rather than memoized. The set is a handful of strings over a table of the same size — the\n * recomputation is far cheaper than the staleness it avoids.\n */\nexport function createSuperAdminConferralResolver(container: Container): SuperAdminConferralResolver {\n let authorizationsService: AuthorizationsBackendService | undefined;\n return () => {\n authorizationsService ??= container.get<AuthorizationsBackendService>(SAAS_SERVICE_TYPES.AuthorizationsService);\n return resolveSuperAdminConferringRoles(authorizationsService);\n };\n}\n\n/**\n * Whether a role array confers super-admin.\n *\n * Defensive on the array itself: this runs on values read from unparsed caller input as well as on\n * persisted rows, and a non-array is treated as conferring nothing — which can only ever make the\n * count smaller, the safe direction (see the module header).\n */\nexport function rolesConferSuperAdmin(roles: unknown, conferringRoles: readonly string[]): boolean {\n if (!Array.isArray(roles)) return false;\n const conferringRoleSet = new Set(conferringRoles);\n return roles.some((role) => typeof role === 'string' && conferringRoleSet.has(role));\n}\n\n/** Whether a status leaves the subject able to authenticate — see {@link ADMINISTRATIVELY_USABLE_USER_STATUSES}. */\nexport function isAdministrativelyUsableStatus(status: unknown): boolean {\n return ADMINISTRATIVELY_USABLE_USER_STATUSES.includes(status as UserStatus);\n}\n\n/** Whether a user row, as it stands, counts as one administratively usable super-administrator. */\nexport function isUsableSuperAdmin(\n user: Pick<User, 'appRoles' | 'status'> | undefined | null,\n conferringRoles: readonly string[],\n): boolean {\n if (!user) return false;\n return rolesConferSuperAdmin(user.appRoles, conferringRoles) && isAdministrativelyUsableStatus(user.status);\n}\n\n/** The narrow row shape the floor count reads. */\ntype SuperAdminFloorCandidateRow = Pick<User, '_id'>;\n\n/**\n * Count the administratively usable super-administrators OTHER than the named subjects.\n *\n * ## Why a LIST and not a `count`\n *\n * `RepositoryResource.count` requires the resource to declare `CoreResourceOperation.COUNT`, and\n * `USERS` does not — it declares READ / LIST / SEARCH / CREATE / UPDATE / UPDATE_MANY / DELETE. A\n * LIST over an indexed (`appRoles` is `.isDBIndexed()`) predicate that selects only application\n * super-administrators returns a population that is inherently tiny, so materialising it costs\n * nothing worth engineering around. Adding a COUNT operation to `USERS` purely to serve this guard\n * would open a new operation surface (and its DTO, its ACL, its route decision) for an internal\n * arithmetic need — a worse trade.\n *\n * ## Why the subjects are excluded in memory rather than in the query\n *\n * A `_id: {$nin: [...]}` fragment would have to agree with the adapter's id representation —\n * MongoDB stores `_id` as an `ObjectId` while every id crossing the service boundary is a string,\n * and a silent cast miss would leave the subject INSIDE the count, satisfying the floor with the\n * very row about to be removed. Comparing `String(row._id)` after the fact cannot fail that way and\n * is exact on both adapters.\n *\n * ## Retention\n *\n * No retention fragment is built here on purpose. `buildContextualFilter` applies the hide at the\n * repository chokepoint for LIST, so retained rows are already absent. Re-stating it here would be\n * a second copy of a rule the retention authority owns — the precise duplication that produced the\n * G-AGG defect it was created to end.\n */\nexport async function countOtherUsableSuperAdmins(params: {\n excludedSubjectIds: readonly string[];\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n /** The reducing operation's transaction. Measured inside it, a concurrent removal it has waited on is counted as gone. */\n transaction?: TransactionContext;\n}): Promise<number> {\n const { excludedSubjectIds, executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier } = params;\n\n const userRepository = repositoriesRegistry.getRepository(CoreResourceType.USERS);\n\n // Fail closed. Without the repository the population is unknowable, and answering \"0\" would\n // refuse every removal while answering \"many\" would permit the lockout. Neither is a guess worth\n // making on an invariant with no recovery path.\n if (!userRepository) {\n throw errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n context: {\n message: 'Cannot evaluate the super-administrator floor: the users repository is not registered',\n resourceType: CoreResourceType.USERS,\n operationIdentifier,\n },\n });\n }\n\n const candidates = await userRepository.list<SuperAdminFloorCandidateRow>(executionContext, {\n appRoles: { $in: [...conferringRoles] },\n status: { $in: [...ADMINISTRATIVELY_USABLE_USER_STATUSES] },\n }, params.transaction ? { transaction: params.transaction } : undefined);\n\n const excludedSubjectIdSet = new Set(excludedSubjectIds.map((subjectId) => String(subjectId)));\n const rows: SuperAdminFloorCandidateRow[] = Array.isArray(candidates) ? candidates : [];\n\n return rows.filter((row) => !excludedSubjectIdSet.has(String(row?._id))).length;\n}\n\n/**\n * The resources the floor writes or reads inside a caller's transaction, for a transactional-core registration's\n * `transactionParticipants`: the APPLICATION row it bumps and the USERS rows it measures. A registration whose own\n * resource is one of them may leave that one out (the framework includes the operation's resource).\n */\nexport const SUPER_ADMIN_FLOOR_TRANSACTION_PARTICIPANTS: Readonly<Record<string, CoreResourceType>> = Object.freeze({\n application: CoreResourceType.APPLICATION,\n users: CoreResourceType.USERS,\n});\n\n/** The reason a reduction is refused when the row it must serialize on cannot be written. */\nexport const SUPER_ADMIN_FLOOR_SERIALIZATION_ANCHOR_MISSING_REASON = 'super_admin_floor_serialization_anchor_missing';\n\n/**\n * Write the application's super-administrator serialization token inside the reducing transaction (#3670).\n *\n * An atomic increment through the infrastructure tier: no execution context has to be allowed to write APPLICATION (the\n * outer operation already decided who may reduce the population; this is a mechanism, not a second permission\n * question), and the strict request re-parse that would strip an `.excludeFromUpdate()` field never runs. The value is\n * irrelevant; what matters is that the row is WRITTEN in the transaction, which is what two reductions collide on.\n *\n * Fails CLOSED when no APPLICATION row was written. Every running application holds exactly one, so its absence is a\n * misconfigured store, and proceeding would put the check-then-act back with nothing serializing it, on a failure that\n * has no repair from above. The refusal costs a demotion that can be retried; the race costs the application.\n */\nexport async function bumpSuperAdminFloorSerializationToken(params: {\n transaction: TransactionContext;\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n operationIdentifier: string;\n}): Promise<void> {\n const { transaction, executionContext, repositoriesRegistry, errorBuilder, operationIdentifier } = params;\n // Addressed by id, read in the same transaction: PostgreSQL's guarded write refuses an unfiltered UPDATE, and the id\n // form is the one both adapters cast alike. A store holds one APPLICATION row; several would all be written, which still\n // gives every reduction the same row to collide on.\n const applicationIds = repositoriesRegistry.getRepository(CoreResourceType.APPLICATION)\n ? (await repositoriesRegistry.findManyForInfrastructure<{ _id?: unknown }>(CoreResourceType.APPLICATION, {}, executionContext, { transaction }))\n .map((row) => row?._id)\n .filter((id): id is NonNullable<typeof id> => id !== undefined && id !== null)\n .map((id) => String(id))\n : [];\n const written = applicationIds.length > 0\n ? await repositoriesRegistry.bulkUpdateForInfrastructure(\n CoreResourceType.APPLICATION,\n { _id: { $in: applicationIds } },\n { increment: { superAdminFloorSerializationToken: 1 } },\n executionContext,\n { transaction },\n )\n : 0;\n if (written > 0) return;\n throw errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n context: {\n reason: SUPER_ADMIN_FLOOR_SERIALIZATION_ANCHOR_MISSING_REASON,\n message:\n 'Cannot evaluate the super-administrator floor: the APPLICATION row that concurrent reductions serialize on '\n + 'could not be written, so two removals could not be made to collide. Refusing rather than removing an '\n + 'administrator without that guarantee.',\n resourceType: CoreResourceType.APPLICATION,\n operationIdentifier,\n },\n });\n}\n\n/**\n * Refuse a write that would leave the application without an administratively usable\n * super-administrator.\n *\n * Call this ONLY once the caller has established that the named subjects currently count toward the\n * floor and would stop counting — the population query is skipped entirely otherwise, so an\n * ordinary user edit, an additive grant, or the deletion of a non-administrator pays nothing.\n *\n * assurance-control: WILDO.ACCESS.ADMINISTRATIVE_CONTINUITY — refuse the removal of the last\n * accountable administrative principal; the denial is an access-control decision and is recorded as\n * one by the operation's own error path.\n */\nexport async function assertSuperAdminFloorPreserved(params: {\n /** The subjects about to stop counting. Excluded from the population before it is measured. */\n subjectUserIds: readonly string[];\n cause: SuperAdminFloorReductionCause;\n executionContext: ExecutionContext<any>;\n /**\n * The context the POPULATION is measured through, resolved only once a reduction is established.\n * Omitted, the caller's `executionContext` measures it, which is right for the administrator verbs:\n * their caller holds a super-administrator role at APPLICATION scope and lists every user.\n *\n * A self-service door is different. `user_self | deactivate_account` reaches this floor with the\n * person's own context, which confines a USERS list to their own row, so the count of OTHER\n * administrators is zero whoever else exists. That fails closed, but on the wrong answer: a second\n * administrator could never deactivate their own account. The lifecycle transition therefore passes\n * the system population context here. Refusals are still built on the caller's context, so the\n * error names the request that was refused.\n */\n resolvePopulationExecutionContext?: () => Promise<ExecutionContext<any>>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n /**\n * The transaction of the operation reducing the population. With it, the floor serializes against every other\n * reduction (see the module header) and measures inside it; without it, nothing serializes the check-then-act.\n * A prefix holds it as `utils.serviceOptions.transaction`.\n */\n transaction?: TransactionContext;\n}): Promise<void> {\n const { subjectUserIds, cause, executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier, transaction } = params;\n\n // BEFORE the measurement, so the conflict window covers the whole check-then-act rather than only the write at the end.\n // assurance-control: WILDO.ACCESS.ADMINISTRATIVE_CONTINUITY — serialize concurrent reductions of the application's\n // administrative population.\n if (transaction) {\n await bumpSuperAdminFloorSerializationToken({ transaction, executionContext, repositoriesRegistry, errorBuilder, operationIdentifier });\n }\n\n const remainingUsableSuperAdmins = await countOtherUsableSuperAdmins({\n excludedSubjectIds: subjectUserIds,\n executionContext: params.resolvePopulationExecutionContext\n ? await params.resolvePopulationExecutionContext()\n : executionContext,\n repositoriesRegistry,\n errorBuilder,\n conferringRoles,\n operationIdentifier,\n transaction,\n });\n\n if (remainingUsableSuperAdmins >= SUPER_ADMIN_FLOOR_MINIMUM) return;\n\n // CONFLICT, not AUTHORIZATION and not VALIDATION. The request is well-formed and the caller IS\n // authorized — every one of these operations is gated on the very role being protected. What\n // forbids the write is the current STATE of the application, which is what CONFLICT means. A 403\n // would tell an administrator to acquire authority they already hold (and none exists that lifts\n // this floor); a 400 would tell them to fix a payload that is correct.\n throw errorBuilder.buildError(ErrorType.CONFLICT, executionContext, {\n // Its own words (#3131): the generic conflict message tells the person to reload, and no reload lifts a floor.\n customMessageReference: ErrorCustomMessageReference.LAST_APPLICATION_SUPER_ADMIN,\n context: {\n // The WIRE contract — allow-listed by `sanitizeErrorForClient`, so this is what a client\n // switches on. Distinct from `reason` below, which is the internal audit/log filter handle and\n // is deliberately stripped at the boundary; the two must not be conflated.\n code: AdministrativeContinuityErrorCode.LAST_APPLICATION_SUPER_ADMIN,\n reason: SUPER_ADMIN_FLOOR_REFUSAL_REASON,\n message:\n 'Refused: this would leave the application with no usable super-administrator. '\n + SUPER_ADMIN_FLOOR_REMEDY,\n cause,\n operationIdentifier,\n subjectUserIds: [...subjectUserIds],\n remainingUsableSuperAdmins,\n requiredUsableSuperAdmins: SUPER_ADMIN_FLOOR_MINIMUM,\n conferringRoles: [...conferringRoles],\n usableStatuses: [...ADMINISTRATIVELY_USABLE_USER_STATUSES],\n },\n });\n}\n\n/**\n * The floor check for a role/status TRANSITION on a single subject.\n *\n * Encapsulates the short-circuit that keeps the population query off the hot path: the floor can\n * only be threatened when the subject counted BEFORE the write and would not count AFTER it. A\n * subject who was never a usable super-administrator, or who remains one, costs a set membership\n * test and nothing else.\n *\n * That shape is also why the additive `assign_roles` verb can call this unconditionally — a union\n * can never drop conferral, so it always short-circuits — which removes the standing invitation for\n * a future change to the set algebra to bypass the floor unnoticed.\n */\nexport async function assertSuperAdminFloorPreservedForTransition(params: {\n subjectUserId: string;\n previousRoles: unknown;\n previousStatus: unknown;\n nextRoles: unknown;\n nextStatus: unknown;\n executionContext: ExecutionContext<any>;\n /** See {@link assertSuperAdminFloorPreserved}; never resolved when the short-circuits below return. */\n resolvePopulationExecutionContext?: () => Promise<ExecutionContext<any>>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n /** See {@link assertSuperAdminFloorPreserved}. */\n transaction?: TransactionContext;\n}): Promise<void> {\n const {\n subjectUserId, previousRoles, previousStatus, nextRoles, nextStatus,\n executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier,\n } = params;\n\n const countedBefore = rolesConferSuperAdmin(previousRoles, conferringRoles) && isAdministrativelyUsableStatus(previousStatus);\n if (!countedBefore) return;\n\n const stillConfersRole = rolesConferSuperAdmin(nextRoles, conferringRoles);\n const stillUsableStatus = isAdministrativelyUsableStatus(nextStatus);\n if (stillConfersRole && stillUsableStatus) return;\n\n await assertSuperAdminFloorPreserved({\n subjectUserIds: [subjectUserId],\n // Which axis actually broke it. Role removal is named first when BOTH change in one write: it\n // is the axis the operator authored against the authority field, and the more specific finding.\n cause: !stillConfersRole ? SuperAdminFloorReductionCause.ROLE_REMOVAL : SuperAdminFloorReductionCause.STATUS_CHANGE,\n executionContext,\n resolvePopulationExecutionContext: params.resolvePopulationExecutionContext,\n repositoriesRegistry,\n errorBuilder,\n conferringRoles,\n operationIdentifier,\n transaction: params.transaction,\n });\n}\n\n/**\n * The floor check for a write that removes the subject's row wholesale — a hard DELETE or a subject\n * erasure (which scrubs the row and retention-hides it, so it stops counting just as decisively).\n *\n * Takes the subjects' CURRENT rows rather than a transition: there is no \"after\" state to compare,\n * the row simply stops counting. Subjects that did not count are filtered out first, so deleting a\n * hundred ordinary users never touches the population query.\n */\nexport async function assertSuperAdminFloorPreservedForRemoval(params: {\n subjects: readonly (Pick<User, '_id' | 'appRoles' | 'status'> | undefined | null)[];\n cause: SuperAdminFloorReductionCause;\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n /** See {@link assertSuperAdminFloorPreserved}. */\n transaction?: TransactionContext;\n}): Promise<void> {\n const { subjects, cause, executionContext, repositoriesRegistry, errorBuilder, conferringRoles, operationIdentifier } = params;\n\n const removedUsableSuperAdminIds = subjects\n .filter((subject) => isUsableSuperAdmin(subject, conferringRoles))\n .map((subject) => String(subject!._id));\n\n if (removedUsableSuperAdminIds.length === 0) return;\n\n await assertSuperAdminFloorPreserved({\n subjectUserIds: removedUsableSuperAdminIds,\n cause,\n executionContext,\n repositoriesRegistry,\n errorBuilder,\n conferringRoles,\n operationIdentifier,\n transaction: params.transaction,\n });\n}\n\n/**\n * The resource types whose rows carry the application authority axis, and therefore the subject\n * types whose erasure can breach the floor.\n *\n * `USERS` and `USER_SELF` are two resource configurations over the SAME collection (see\n * `getCollectionNameForResource`), both declaring `RETAIN_AND_IMPERSONALIZE`, so an erasure\n * addressed through either door scrubs and hides the same row.\n */\nexport const SUPER_ADMIN_FLOOR_GOVERNED_SUBJECT_RESOURCE_TYPES: readonly string[] = [\n CoreResourceType.USERS,\n CoreResourceType.USER_SELF,\n];\n\n/** Whether an erasure of this subject resource type can reduce the super-administrator population. */\nexport function isSuperAdminFloorGovernedSubjectResource(resourceType: unknown): boolean {\n return SUPER_ADMIN_FLOOR_GOVERNED_SUBJECT_RESOURCE_TYPES.includes(String(resourceType));\n}\n\n/**\n * Read the subjects an erasure is about to scrub, for the floor check.\n *\n * Reads through the repository so the retention hide applies: a subject ALREADY erased is invisible\n * here, which is correct — it stopped counting at its own erasure, and a re-run must not be refused\n * on account of a row that no longer contributes. A subject that cannot be read is simply absent\n * and contributes nothing, which is the safe direction for a check whose job is to notice the\n * subjects that DO count.\n */\nexport async function readSubjectsForSuperAdminFloor(params: {\n subjectIds: readonly string[];\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n operationIdentifier: string;\n}): Promise<Pick<User, '_id' | 'appRoles' | 'status'>[]> {\n const { subjectIds, executionContext, repositoriesRegistry, errorBuilder, operationIdentifier } = params;\n\n const userRepository = repositoriesRegistry.getRepository(CoreResourceType.USERS);\n\n if (!userRepository) {\n throw errorBuilder.buildError(ErrorType.CONFIGURATION, executionContext, {\n context: {\n message: 'Cannot evaluate the super-administrator floor: the users repository is not registered',\n resourceType: CoreResourceType.USERS,\n operationIdentifier,\n },\n });\n }\n\n const subjects = await userRepository.list<Pick<User, '_id' | 'appRoles' | 'status'>>(\n executionContext,\n [...subjectIds],\n );\n\n return Array.isArray(subjects) ? subjects : [];\n}\n\n/**\n * Measure the floor without refusing anything — the boot-time report.\n *\n * Separate from {@link assertSuperAdminFloorPreserved} because it answers a different question at a\n * moment when refusing is not an option: a fresh application legitimately boots with zero users\n * (the first administrator arrives through seeding or first-run registration), so a startup that\n * failed on this condition would brick every greenfield app. What it CAN do is observe the states\n * no write guard ever saw — a direct database edit, a restore from a backup taken mid-erasure, a\n * seed that authored no administrator, or a role-table reconfiguration that changed which roles\n * confer super-admin out from under an existing population.\n */\nexport async function measureUsableSuperAdminPopulation(params: {\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n}): Promise<{ usableSuperAdminCount: number; meetsFloor: boolean; conferringRoles: string[] }> {\n const usableSuperAdminCount = await countOtherUsableSuperAdmins({\n excludedSubjectIds: [],\n executionContext: params.executionContext,\n repositoriesRegistry: params.repositoriesRegistry,\n errorBuilder: params.errorBuilder,\n conferringRoles: params.conferringRoles,\n operationIdentifier: CoreResourceOperation.LIST,\n });\n\n return {\n usableSuperAdminCount,\n meetsFloor: usableSuperAdminCount >= SUPER_ADMIN_FLOOR_MINIMUM,\n conferringRoles: [...params.conferringRoles],\n };\n}\n\n/** One acknowledged breach of the application floor — reported, never thrown. */\nexport type SuperAdminFloorBreach = {\n erasedUsableSuperAdminIds: string[];\n conferringRoles: string[];\n};\n\n/**\n * The erasure door: refuse by default, defeasible by an explicit controller acknowledgement.\n *\n * The application twin of `assertOrganizationOwnerFloorForSubjectErasure`, and it exists for the same\n * reason that one does — so the POLICY lives in the authority rather than in whichever service\n * happens to call it. A caller that owns the decision must not also own the rule.\n *\n * ## Why this door alone is defeasible\n *\n * See `.claude/rules/administrative-continuity-floors.md`. In short: GDPR Art. 17(3) is a closed list\n * of grounds for refusing an erasure and \"the application would become unadministrable\" is not on it,\n * while Art. 12(3) requires an answer without undue delay. A framework that refuses indefinitely\n * takes that choice away from the application, which is the controller that owes the data subject an\n * answer. Every ADMINISTRATIVE door — `revoke_roles`, core UPDATE, core DELETE — stays unconditional,\n * because nobody holds a right to have their administrative role removed.\n *\n * ## Why the stakes argue FOR the escape, not against it\n *\n * A stranded application is worse than a stranded tenant: there is no scope above it, so there is no\n * higher-tier repair door to reach for, and recovery is necessarily a seed or a direct database\n * write. That is precisely why the escape must be explicit, per-request, non-default and LOUD — the\n * operator's only chance to act is the record this emits, since nobody will discover the state by\n * being denied something later.\n */\nexport async function assertSuperAdminFloorForSubjectErasure(params: {\n subjects: readonly (Pick<User, '_id' | 'appRoles' | 'status'> | undefined | null)[];\n executionContext: ExecutionContext<any>;\n repositoriesRegistry: RepositoriesRegistryHandlerBackendService;\n errorBuilder: ErrorBuilderBackendService;\n conferringRoles: readonly string[];\n operationIdentifier: string;\n /** The controller's explicit decision to complete an erasure that strands the application. */\n acknowledgeFloorBreach?: boolean;\n /** Invoked when the breach is acknowledged AND real. Never on the refusal path. */\n onAcknowledgedBreach?: (breach: SuperAdminFloorBreach) => Promise<void> | void;\n}): Promise<void> {\n if (!params.acknowledgeFloorBreach) {\n await assertSuperAdminFloorPreservedForRemoval({\n ...params,\n cause: SuperAdminFloorReductionCause.ERASURE,\n });\n return;\n }\n\n // Acknowledged: nothing is refused, so the only remaining obligation is that the consequence is\n // never silent. The same usability test still runs — an acknowledged erasure that removes no\n // usable administrator must stay quiet, or the alert stops meaning anything.\n const erasedUsableSuperAdminIds = params.subjects\n .filter((subject) => isUsableSuperAdmin(subject, params.conferringRoles))\n .map((subject) => String(subject!._id));\n\n if (erasedUsableSuperAdminIds.length === 0) return;\n\n await params.onAcknowledgedBreach?.({\n erasedUsableSuperAdminIds,\n conferringRoles: [...params.conferringRoles],\n });\n}\n"]}
@@ -50,10 +50,24 @@
50
50
  import { ApiKeyScope } from '@wildo-ai/saas-models';
51
51
  import { createResourceCustomServiceImplementation, type ResourceCustomServiceImplementationFactory } from '../../base-classes/services/services/resource-operation-custom-implementation.factory.backend';
52
52
  import type { RoleCeilingGate } from '../../security/authorizations/role-ceiling.backend.utils';
53
+ /** ROTATE keeps a bounded grace window; REGENERATE keeps an open-ended overlap. Exported for the role-ceiling pin. */
54
+ export declare enum OAuthClientReissueKind {
55
+ ROTATE = "rotate",
56
+ REGENERATE = "regenerate"
57
+ }
53
58
  /** The handler-object shape accepted by `createResourceCustomServiceImplementation`. */
54
59
  export type OAuthClientImplHandlers = Parameters<typeof createResourceCustomServiceImplementation>[0]['handlers'];
55
60
  /** CREATE — enforce the role-ceiling, then mint material; surface `plainSecret` once. Exported for focused create-pipeline unit tests. */
56
61
  export declare function buildOAuthClientMintHandlers(scope: ApiKeyScope, roleCeilingGate: RoleCeilingGate): OAuthClientImplHandlers;
62
+ /**
63
+ * ROTATE / REGENERATE — reissue the secret on the SAME record, moving the prior secret
64
+ * into the grace-window slot. The client-credentials exchange keeps honoring the prior
65
+ * secret until `previousSecretInvalidationDate` (ROTATE: the caller-chosen instant,
66
+ * defaulting to now = immediate cutover; REGENERATE: `null` = open-ended overlap).
67
+ *
68
+ * Exported for the role-ceiling pin (#3671).
69
+ */
70
+ export declare function buildOAuthClientReissueHandlers(scope: ApiKeyScope, kind: OAuthClientReissueKind, roleCeilingGate: RoleCeilingGate): OAuthClientImplHandlers;
57
71
  /** DEACTIVATE impls — ACTIVE → INACTIVE, forced; org default + org `admin` + app default (#1028). */
58
72
  export declare function createOAuthClientDeactivateFactories(): ResourceCustomServiceImplementationFactory[];
59
73
  /** REACTIVATE impls — INACTIVE → ACTIVE, forced. An EXPIRED client is NOT revivable (409). */
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-clients.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/security/oauth-clients/oauth-clients.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAGH,OAAO,EACL,WAAW,EAUZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,yCAAyC,EAEzC,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAOpH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uEAAuE,CAAC;AAqD7G,wFAAwF;AACxF,MAAM,MAAM,uBAAuB,GAAG,UAAU,CAAC,OAAO,yCAAyC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;AA0ElH,0IAA0I;AAC1I,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,eAAe,GAAG,uBAAuB,CAkC1H;AA0GD,qGAAqG;AACrG,wBAAgB,oCAAoC,IAAI,0CAA0C,EAAE,CAMnG;AAED,8FAA8F;AAC9F,wBAAgB,oCAAoC,IAAI,0CAA0C,EAAE,CAMnG;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAmB/F;AAED,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAK/F;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,eAAe,GAAG,uBAAuB,CAkBxG;AAED,8DAA8D;AAC9D,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAK/F;AAED,2CAA2C;AAC3C,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAK/F;AAED,6CAA6C;AAC7C,wBAAgB,oCAAoC,IAAI,0CAA0C,EAAE,CAKnG"}
1
+ {"version":3,"file":"oauth-clients.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/security/oauth-clients/oauth-clients.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AAGH,OAAO,EACL,WAAW,EAUZ,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,yCAAyC,EAEzC,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAOpH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uEAAuE,CAAC;AA2B7G,sHAAsH;AACtH,oBAAY,sBAAsB;IAChC,MAAM,WAAW;IACjB,UAAU,eAAe;CAC1B;AAsBD,wFAAwF;AACxF,MAAM,MAAM,uBAAuB,GAAG,UAAU,CAAC,OAAO,yCAAyC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC;AA0ElH,0IAA0I;AAC1I,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,WAAW,EAAE,eAAe,EAAE,eAAe,GAAG,uBAAuB,CAkC1H;AAED;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,sBAAsB,EAAE,eAAe,EAAE,eAAe,GAAG,uBAAuB,CA0D3J;AA+CD,qGAAqG;AACrG,wBAAgB,oCAAoC,IAAI,0CAA0C,EAAE,CAMnG;AAED,8FAA8F;AAC9F,wBAAgB,oCAAoC,IAAI,0CAA0C,EAAE,CAMnG;AAED;;;;;;GAMG;AACH,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAmB/F;AAED,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAK/F;AAED;;;;;;GAMG;AACH,wBAAgB,8BAA8B,CAAC,eAAe,EAAE,eAAe,GAAG,uBAAuB,CAkBxG;AAED,8DAA8D;AAC9D,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAK/F;AAED,2CAA2C;AAC3C,wBAAgB,gCAAgC,IAAI,0CAA0C,EAAE,CAK/F;AAED,6CAA6C;AAC7C,wBAAgB,oCAAoC,IAAI,0CAA0C,EAAE,CAKnG"}
@@ -75,8 +75,8 @@ const PLAINTEXT_SECRET_BY_EC = new WeakMap();
75
75
  * whether the update result round-trips that field.
76
76
  */
77
77
  const REISSUE_BRIDGE_BY_EC = new WeakMap();
78
- /** ROTATE keeps a bounded grace window; REGENERATE keeps an open-ended overlap. */
79
- var OAuthClientReissueKind;
78
+ /** ROTATE keeps a bounded grace window; REGENERATE keeps an open-ended overlap. Exported for the role-ceiling pin. */
79
+ export var OAuthClientReissueKind;
80
80
  (function (OAuthClientReissueKind) {
81
81
  OAuthClientReissueKind["ROTATE"] = "rotate";
82
82
  OAuthClientReissueKind["REGENERATE"] = "regenerate";
@@ -187,14 +187,22 @@ export function buildOAuthClientMintHandlers(scope, roleCeilingGate) {
187
187
  * into the grace-window slot. The client-credentials exchange keeps honoring the prior
188
188
  * secret until `previousSecretInvalidationDate` (ROTATE: the caller-chosen instant,
189
189
  * defaulting to now = immediate cutover; REGENERATE: `null` = open-ended overlap).
190
+ *
191
+ * Exported for the role-ceiling pin (#3671).
190
192
  */
191
- function buildOAuthClientReissueHandlers(scope, kind) {
193
+ export function buildOAuthClientReissueHandlers(scope, kind, roleCeilingGate) {
192
194
  return {
193
195
  prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {
194
196
  // `currentObject` is the pre-fetched DB record for this UPDATE-borrow op — it
195
197
  // includes the backend-only `hashedSecret`/`secretPrefix` we move into the
196
198
  // previous slot.
197
199
  const current = utils.currentObject;
200
+ // Reissuing hands the caller a working secret for THIS client's roles, so the client's roles face the caller's
201
+ // ceiling exactly as a create's requested roles do, and as the api-key reissue verbs' do (#1631 review F1). Without
202
+ // it, an administrator who could not have created an OWNER client could take a live secret for one by rotating it
203
+ // (#3671). Same gate, same refusal (`role_ceiling_exceeded`), same super-administrator and system bypass.
204
+ // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — a credential's secret is handed out only to a caller who holds its roles.
205
+ assertRequestedRolesWithinCallerCeiling(executionContext, current?.roles, utils.errorBuilder, roleCeilingGate);
198
206
  // Only an ACTIVE client can be reissued (409 for a revoked/expired client —
199
207
  // otherwise rotation would silently re-arm a deactivated credential).
200
208
  if (current && current.status !== OAuthClientStatus.ACTIVE) {
@@ -348,10 +356,10 @@ export function createOAuthClientUpdateFactories() {
348
356
  }
349
357
  /** ROTATE impls — bounded grace window. */
350
358
  export function createOAuthClientRotateFactories() {
351
- return oauthClientFactoriesForOperation((target) => target.ops.ROTATE, (target) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.ROTATE));
359
+ return oauthClientFactoriesForOperation((target) => target.ops.ROTATE, (target, container) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.ROTATE, createRoleCeilingGate(container)));
352
360
  }
353
361
  /** REGENERATE impls — open-ended overlap. */
354
362
  export function createOAuthClientRegenerateFactories() {
355
- return oauthClientFactoriesForOperation((target) => target.ops.REGENERATE, (target) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.REGENERATE));
363
+ return oauthClientFactoriesForOperation((target) => target.ops.REGENERATE, (target, container) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.REGENERATE, createRoleCeilingGate(container)));
356
364
  }
357
365
  //# sourceMappingURL=oauth-clients.custom-impl.backend.service.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-clients.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/security/oauth-clients/oauth-clients.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OAuth Clients Custom Implementations\n *\n * Implements the server-side behaviour for the organization + application OAuth-client\n * resources (`organizationOAuthClients`, `applicationOAuthClients`) that the schema\n * alone cannot express. FAITHFUL MIRROR of the api-keys mint pipeline\n * (`api-keys.custom-impl.backend.service.ts`), FOCUSED to the trunk operation set:\n *\n * CREATE → mint fresh secret material (the client must never supply it).\n * ROTATE → reissue the secret; retain the prior secret in a grace window that\n * stops authenticating at the caller-chosen `oldSecretInvalidationDate`.\n * REGENERATE → reissue the secret; keep the prior secret valid open-endedly (until\n * the next rotation) for a zero-downtime overlap.\n *\n * (api-keys' DEACTIVATE/REACTIVATE/EXTEND_EXPIRY/USAGE_STATS impls are deferred to the\n * OIDC branch alongside their resource-config ops.)\n *\n * WHY MINTING NEEDS AN IMPL: the schema requires `secretPrefix` + `hashedSecret`, but\n * both are `.excludeFromCreate().excludeFromUpdate()` (the client must never supply\n * them) and `plainSecret` is an ephemeral response-only field. Nothing generated them,\n * so a bare create/update persisted a row missing the required fields. These impls are\n * the single server-side choke point that mints secret material.\n *\n * PATTERN (CREATE / ROTATE / REGENERATE — all \"mint a secret\" ops):\n * prefixCoreOperations → mint material; inject the persisted `secretPrefix` +\n * `hashedSecret` (and, for rotate/regenerate, the previous-\n * secret grace-window fields) into the payload. Those fields\n * are `.excludeFrom{Create,Update}()`, so returning them from\n * the prefix is what carries them into the core write — the\n * orchestration diffs the prefix-added keys and forwards them\n * to the repository's `preserveServerInjectedFields` allowlist\n * so the final strict parse re-attaches (rather than strips)\n * them. Stash the one-time `plainSecret` for the postfix.\n * postfixCoreOperations → attach the one-time `plainSecret` (+ rotate's `oldClientId` /\n * `oldSecretInvalidationDate`) to the response so the\n * operation's ephemeral `plainSecret` field surfaces it.\n * The core write still runs, so notifications / websocket events fire normally — which\n * is why this is prefix+postfix rather than `replaceCoreOperations`.\n *\n * VARIANT COVERAGE: a custom impl is matched to a running op by its FULL operation-path\n * identity — `isOperationDefault` (the `isDefault` sentinel) vs `variantKey` produce\n * DISTINCT keys (`createOperationPathKey`). So a single default registration only\n * intercepts the default variant; the cross-tenant app-super-admin `admin` variant\n * (org clients only) needs its own registration or it runs the bare core op and fails.\n * `OAUTH_CLIENT_TARGETS` enumerates every (resource × variant) each op exposes. Minting\n * is org-agnostic — the target `organizationId` rides in the payload / pre-fetched\n * record, never from these handlers — so the admin variant reuses the identical handlers\n * and (per the user security decision) also receives the tenant's one-time `plainSecret`.\n */\n\nimport type { AdmissionWritePrecondition } from '~wildo-backend-lib/base-classes/utils/admission-write-precondition.backend.utils';\nimport {\n ApiKeyScope,\n AppOAuthClients_Operations,\n CoreResourceOperation,\n CoreResourceType,\n ErrorCustomMessageReference,\n ErrorType,\n OAuthClientStatus,\n OrgOAuthClients_Operations,\n ResourceOperationVariantType,\n effectiveRotationInvalidationDate,\n} from '@wildo-ai/saas-models';\nimport type { OAuthClientBase } 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 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 { generateOAuthClientSecretMaterial } from './oauth-client-generation.backend.utils';\nimport type { Container } from 'inversify';\nimport { assertRequestedRolesWithinCallerCeiling, createRoleCeilingGate } from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\nimport { assertMachineCredentialRolesOfferedByOrganizationType } from '~wildo-backend-lib/security/authorizations/machine-credential-organization-roles.backend.utils';\nimport type { RoleCeilingGate } from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\n\n/**\n * Bridge for the one-time plaintext secret between the CREATE prefix (which mints it)\n * and the postfix (which returns it), keyed by the per-operation `ExecutionContext`\n * INSTANCE. The orchestration threads the SAME `executionContext` through both handlers\n * (`services-registry-handler.backend.service.ts` prefix + postfix calls), so it is a\n * stable, per-invocation correlation key.\n *\n * A `WeakMap` keyed by the EC is deliberately chosen over a `Map` keyed by `hashedSecret`\n * / `_id`: (1) NO LEAK — the entry is reclaimed by GC when the request's EC becomes\n * unreachable, so a core-write failure between prefix and postfix cannot strand the\n * plaintext in memory; (2) NO RACE — concurrent operations run on DISTINCT EC instances,\n * so two simultaneous mints/reissues never collide on a shared key. `plainSecret` is\n * never persisted; the postfix also deletes the entry for promptness.\n */\nconst PLAINTEXT_SECRET_BY_EC = new WeakMap<ExecutionContext<any>, string>();\n\n/**\n * Bridge for ROTATE / REGENERATE, keyed by the same per-operation `ExecutionContext`\n * instance (see {@link PLAINTEXT_SECRET_BY_EC}). Carries the one-time `plainSecret` AND\n * the applied `previousSecretInvalidationDate`, so the postfix builds ROTATE's\n * `oldSecretInvalidationDate` from the value the prefix actually applied — independent of\n * whether the update result round-trips that field.\n */\nconst REISSUE_BRIDGE_BY_EC = new WeakMap<ExecutionContext<any>, { plainSecret: string; previousSecretInvalidationDate: Date | null }>();\n\n/** ROTATE keeps a bounded grace window; REGENERATE keeps an open-ended overlap. */\nenum OAuthClientReissueKind {\n ROTATE = 'rotate',\n REGENERATE = 'regenerate',\n}\n\n/**\n * The pre-fetched client fields the reissue prefix reads. `hashedSecret`/`secretPrefix`\n * are present on every persisted client of either scope.\n */\ntype ReissuableOAuthClient = Pick<OAuthClientBase, 'status'> & { hashedSecret: string; secretPrefix: string };\n\n/**\n * Structural view of a per-resource custom-operation enum — the members that have a\n * backend impl in this file. Both `OrgOAuthClients_Operations` and\n * `AppOAuthClients_Operations` satisfy it (identical string values).\n */\ntype OAuthClientCustomOps = { ROTATE: string; REGENERATE: string; DEACTIVATE: string; REACTIVATE: string; EXPIRE: string };\n\ntype OAuthClientTarget = {\n resourceType: CoreResourceType;\n scope: ApiKeyScope;\n ops: OAuthClientCustomOps;\n variants: ReadonlyArray<{ isOperationDefault: boolean; variantKey?: string }>;\n};\n\n/** The handler-object shape accepted by `createResourceCustomServiceImplementation`. */\nexport type OAuthClientImplHandlers = Parameters<typeof createResourceCustomServiceImplementation>[0]['handlers'];\n\n/**\n * The OAuth-client resources, their scope (drives the `oc_org_` / `oc_app_` prefix), the\n * per-resource custom-op enum, and every op VARIANT each resource exposes.\n *\n * - ORG clients expose an org-admin DEFAULT + a cross-tenant app-super-admin `admin`\n * variant → both registered.\n * - APP clients are single-variant (APP_ADMIN-scoped) → default only.\n */\nconst OAUTH_CLIENT_TARGETS: ReadonlyArray<OAuthClientTarget> = [\n {\n resourceType: CoreResourceType.OAUTH_CLIENTS_ORGANIZATION,\n scope: ApiKeyScope.ORGANIZATION,\n ops: OrgOAuthClients_Operations,\n variants: [\n { isOperationDefault: true },\n { isOperationDefault: false, variantKey: 'admin' },\n ],\n },\n {\n resourceType: CoreResourceType.OAUTH_CLIENTS_APPLICATION,\n scope: ApiKeyScope.APPLICATION,\n ops: AppOAuthClients_Operations,\n variants: [\n { isOperationDefault: true },\n ],\n },\n];\n\n/**\n * #2297 wave 3: every OAuth-client write's hooks may run inside the transaction that keeps its audit row (reviewed\n * 2026-11-10). They do nothing outside the database: the role ceiling is an in-memory check (its refusal audit is\n * written just before the throw, and nothing commits after it); secrets come from `node:crypto`; the plaintext crosses\n * from PREFIX to POSTFIX in memory, overwritten by a retried attempt; the organization-type read is of a row the write\n * does not own. Inside the transaction `utils.currentObject` is the row re-read (locked on PostgreSQL).\n */\nconst OAUTH_CLIENT_WRITE_HOOKS_RUN_INSIDE_TRANSACTION = ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION;\n\n/**\n * Register one custom-impl factory per (target × variant) for a single operation.\n * Every OAuth-client op is API_CALL and needs a distinct registration per variant\n * identity (see the module header) — this centralizes that fan-out.\n */\nfunction oauthClientFactoriesForOperation(\n operationIdentifierFor: (target: OAuthClientTarget) => string | CoreResourceOperation,\n handlersFor: (target: OAuthClientTarget, container: Container) => OAuthClientImplHandlers,\n /** #2543: what the row must still hold for the core write to land (a status transition's starting state). */\n coreWritePrecondition?: AdmissionWritePrecondition,\n): ResourceCustomServiceImplementationFactory[] {\n return OAUTH_CLIENT_TARGETS.flatMap((target) =>\n target.variants.map(({ isOperationDefault, variantKey }) => {\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: target.resourceType,\n // API_CALL: the lookup key is built from the RUNNING operation's variantType\n // (`extractOperationPath` passes it through unchanged), and OAuth-client ops\n // are dispatched as direct HTTP API_CALLs. Must match exactly.\n operationIdentifier: operationIdentifierFor(target),\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault,\n variantKey,\n },\n ...(coreWritePrecondition ? { coreWritePrecondition } : {}),\n // Every caller of this helper is a write (#2297).\n hookTransactionPosture: OAUTH_CLIENT_WRITE_HOOKS_RUN_INSIDE_TRANSACTION,\n handlers: handlersFor(target, container),\n });\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n }),\n );\n}\n\n/** CREATE — enforce the role-ceiling, then mint material; surface `plainSecret` once. Exported for focused create-pipeline unit tests. */\nexport function buildOAuthClientMintHandlers(scope: ApiKeyScope, roleCeilingGate: RoleCeilingGate): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n // Role-ceiling gate — a principal cannot mint a client carrying roles it does not itself hold\n // (self-service privilege-escalation prevention; APPLICATION-scoped super-admin + system writes\n // bypass). The ceiling is hierarchy-aware, so an ORG_ADMIN may mint a client carrying the roles\n // its own role confers (ORG_MANAGER/ORG_MEMBER/…) but never ORG_OWNER above it.\n // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — least-privilege on machine-credential minting.\n assertRequestedRolesWithinCallerCeiling(executionContext, (input as { roles?: string[] }).roles, utils.errorBuilder, roleCeilingGate);\n // And the organization's TYPE must offer them, whoever mints (#2648): the ceiling's platform bypass is about height.\n if (scope === ApiKeyScope.ORGANIZATION) {\n await assertMachineCredentialRolesOfferedByOrganizationType({\n roles: (input as { roles?: string[] }).roles,\n organizationId: executionContext.organizationId ?? (input as { organizationId?: string }).organizationId,\n executionContext,\n utils,\n });\n }\n const { plainSecret, secretPrefix, hashedSecret } = generateOAuthClientSecretMaterial(scope);\n // Bridge the one-time secret to the postfix, keyed by the per-op EC instance; never persisted.\n PLAINTEXT_SECRET_BY_EC.set(executionContext, plainSecret);\n // secretPrefix + hashedSecret are `.excludeFromCreate()`; returning them injects\n // them into the create payload (preserveServerInjectedFields re-attaches them past\n // the strict parse) so the row persists with the required fields.\n return { ...(input as Record<string, unknown>), secretPrefix, hashedSecret };\n },\n postfixCoreOperations: async (_id, createdClient, executionContext, _operationPath, _utils) => {\n if (!createdClient || typeof createdClient !== 'object') return createdClient;\n const plainSecret = PLAINTEXT_SECRET_BY_EC.get(executionContext);\n PLAINTEXT_SECRET_BY_EC.delete(executionContext);\n if (!plainSecret) return createdClient;\n return { ...(createdClient as Record<string, unknown>), plainSecret };\n },\n };\n}\n\n/**\n * ROTATE / REGENERATE — reissue the secret on the SAME record, moving the prior secret\n * into the grace-window slot. The client-credentials exchange keeps honoring the prior\n * secret until `previousSecretInvalidationDate` (ROTATE: the caller-chosen instant,\n * defaulting to now = immediate cutover; REGENERATE: `null` = open-ended overlap).\n */\nfunction buildOAuthClientReissueHandlers(scope: ApiKeyScope, kind: OAuthClientReissueKind): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n // `currentObject` is the pre-fetched DB record for this UPDATE-borrow op — it\n // includes the backend-only `hashedSecret`/`secretPrefix` we move into the\n // previous slot.\n const current = utils.currentObject as ReissuableOAuthClient | undefined;\n\n // Only an ACTIVE client can be reissued (409 for a revoked/expired client —\n // otherwise rotation would silently re-arm a deactivated credential).\n if (current && current.status !== OAuthClientStatus.ACTIVE) {\n throw utils.errorBuilder.buildError(ErrorType.CONFLICT, executionContext, {\n customMessageReference: ErrorCustomMessageReference.CONFLICT,\n context: { message: `OAuth client secret cannot be reissued in status '${current.status}'`, oauthClientStatus: current.status },\n });\n }\n\n const { plainSecret, secretPrefix, hashedSecret } = generateOAuthClientSecretMaterial(scope);\n const previousSecretInvalidationDate = kind === OAuthClientReissueKind.ROTATE\n // A past invalidation date is read as now (#3477): the effect is the default's, so refusing it adds nothing.\n ? effectiveRotationInvalidationDate((input as { oldSecretInvalidationDate?: Date }).oldSecretInvalidationDate)\n : null;\n REISSUE_BRIDGE_BY_EC.set(executionContext, { plainSecret, previousSecretInvalidationDate });\n\n return {\n ...(input as Record<string, unknown>),\n hashedSecret,\n secretPrefix,\n // Move the outgoing secret into the grace-window slot (server-managed,\n // excludeFrom{Create,Update} → carried by preserveServerInjectedFields).\n previousHashedSecret: current?.hashedSecret,\n previousSecretPrefix: current?.secretPrefix,\n previousSecretInvalidationDate,\n };\n },\n postfixCoreOperations: async (_id, updated, executionContext, _operationPath, _utils) => {\n if (!updated || typeof updated !== 'object') return updated;\n const record = updated as Record<string, unknown>;\n const bridged = REISSUE_BRIDGE_BY_EC.get(executionContext);\n REISSUE_BRIDGE_BY_EC.delete(executionContext);\n\n const withPlainSecret = bridged?.plainSecret ? { ...record, plainSecret: bridged.plainSecret } : { ...record };\n if (kind === OAuthClientReissueKind.ROTATE) {\n // ROTATE's response DTO additionally carries the rotated record's id + when the\n // prior secret is invalidated. Same record, so `oldClientId` is its own `_id`.\n return { ...withPlainSecret, oldClientId: _id, oldSecretInvalidationDate: bridged?.previousSecretInvalidationDate };\n }\n // REGENERATE's response DTO is resource + plainSecret only.\n return withPlainSecret;\n },\n };\n}\n\n/** CREATE impls — org default + org `admin` + app default. */\n/**\n * FORCE one OAuth-client status transition — the one handler behind DEACTIVATE (ACTIVE → INACTIVE),\n * REACTIVATE (INACTIVE → ACTIVE) and the sweep's EXPIRE (ACTIVE → EXPIRED, #413).\n *\n * Every status verb on this resource takes an EMPTY request (`requestDto: z.object({})` in the\n * paired configs) and this prefix supplies the transition the verb names: the return is MERGED onto\n * the caller payload by the dispatcher, so the forced `status` is what lands last, and nothing the\n * caller sent is echoed. `status` is a normal updatable field, so returning it lets the borrowed\n * core UPDATE persist it and keeps the response the updated resource.\n *\n * The precondition is what holds for every variant: the config's `enabledCondition` is enforced only\n * for the variant that declares it, and the cross-tenant `admin` variants omit it entirely. It is what keeps three facts true:\n * an INACTIVE client was retired by an administrator, and the sweep relabelling it EXPIRED would\n * erase who ended it; an EXPIRED client is not revived by a status flip, because its expiry was the\n * thing retiring it; and \"deactivate\" cannot be replayed onto a client that is not ACTIVE.\n *\n * What DEACTIVATE stops is new access — the direct-secret path and every mint re-read the client\n * and require ACTIVE. A client-credentials JWT already minted is not re-checked against the client\n * until it expires (#1028), which the configs' contracts state rather than imply otherwise.\n *\n * assurance-control: WILDO.IDENTITY.CREDENTIAL_LIFECYCLE — credential state transitions are\n * explicit and validated, never inferred from a caller-supplied field.\n */\nfunction buildOAuthClientStatusTransitionHandlers(requiredCurrentStatus: OAuthClientStatus, nextStatus: OAuthClientStatus): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, _input, executionContext, _operationPath, utils) => {\n // `currentObject` is the pre-fetched DB record for this UPDATE-borrow op.\n const current = utils.currentObject as { status?: OAuthClientStatus } | undefined;\n\n if (current && current.status !== requiredCurrentStatus) {\n throw utils.errorBuilder.buildError(ErrorType.CONFLICT, executionContext, {\n customMessageReference: ErrorCustomMessageReference.CONFLICT,\n context: {\n message: `OAuth client cannot move to '${nextStatus}' from status '${current.status}'`,\n oauthClientStatus: current.status,\n },\n });\n }\n\n return { status: nextStatus };\n },\n };\n}\n\n/** DEACTIVATE impls — ACTIVE → INACTIVE, forced; org default + org `admin` + app default (#1028). */\nexport function createOAuthClientDeactivateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.DEACTIVATE,\n () => buildOAuthClientStatusTransitionHandlers(OAuthClientStatus.ACTIVE, OAuthClientStatus.INACTIVE),\n { status: [OAuthClientStatus.ACTIVE] },\n );\n}\n\n/** REACTIVATE impls — INACTIVE → ACTIVE, forced. An EXPIRED client is NOT revivable (409). */\nexport function createOAuthClientReactivateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.REACTIVATE,\n () => buildOAuthClientStatusTransitionHandlers(OAuthClientStatus.INACTIVE, OAuthClientStatus.ACTIVE),\n { status: [OAuthClientStatus.INACTIVE] },\n );\n}\n\n/**\n * EXPIRE impls — registered separately from the other ops rather than through\n * `oauthClientFactoriesForOperation`, because that helper hardcodes `variantType: API_CALL` and\n * fans out over each resource's HTTP variants. EXPIRE is INTERNAL_CALL with a single default\n * variant, so it needs its own registration or the lookup key would never match and the guard above\n * would be silently unreachable.\n */\nexport function createOAuthClientExpireFactories(): ResourceCustomServiceImplementationFactory[] {\n return OAUTH_CLIENT_TARGETS.map((target) => {\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, _container: Container) =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: target.resourceType,\n operationIdentifier: target.ops.EXPIRE,\n variantType: ResourceOperationVariantType.INTERNAL_CALL,\n isOperationDefault: true,\n },\n // #2543: the prefix's ACTIVE-only precondition, carried by the core write too (where the write runs without a\n // transaction, nothing re-reads the row), so a sweep EXPIRE racing an administrator's DEACTIVATE cannot overwrite\n // it. A lost race answers CONFLICT, inside the audit-row transaction (#2297) as well.\n coreWritePrecondition: { status: [OAuthClientStatus.ACTIVE] },\n hookTransactionPosture: OAUTH_CLIENT_WRITE_HOOKS_RUN_INSIDE_TRANSACTION,\n handlers: buildOAuthClientStatusTransitionHandlers(OAuthClientStatus.ACTIVE, OAuthClientStatus.EXPIRED),\n });\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n });\n}\n\nexport function createOAuthClientCreateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n () => CoreResourceOperation.CREATE,\n (target, container) => buildOAuthClientMintHandlers(target.scope, createRoleCeilingGate(container)),\n );\n}\n\n/**\n * UPDATE — the role ceiling on an edited client (#2321). Exported for the role-ceiling pin.\n *\n * The request carries no secret, status or expiry, so the ceiling is all the prefix owes: a `roles`\n * value the caller could not register is refused before anything is written. Redirect URIs keep their\n * safety validation in the request schema itself.\n */\nexport function buildOAuthClientUpdateHandlers(roleCeilingGate: RoleCeilingGate): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n const requestedRoles = (input as { roles?: string[] } | undefined)?.roles;\n if (requestedRoles !== undefined) {\n // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — an edited machine credential holds no role its editor does not.\n assertRequestedRolesWithinCallerCeiling(executionContext, requestedRoles, utils.errorBuilder, roleCeilingGate);\n // An organization client's roles must be offered by its organization's type (#2648); an application client has none.\n await assertMachineCredentialRolesOfferedByOrganizationType({\n roles: requestedRoles,\n organizationId: (utils.currentObject as { organizationId?: string } | undefined)?.organizationId,\n executionContext,\n utils,\n });\n }\n return input as Record<string, unknown>;\n },\n };\n}\n\n/** UPDATE impls — org default + org `admin` + app default. */\nexport function createOAuthClientUpdateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n () => CoreResourceOperation.UPDATE,\n (_target, container) => buildOAuthClientUpdateHandlers(createRoleCeilingGate(container)),\n );\n}\n\n/** ROTATE impls — bounded grace window. */\nexport function createOAuthClientRotateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.ROTATE,\n (target) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.ROTATE),\n );\n}\n\n/** REGENERATE impls — open-ended overlap. */\nexport function createOAuthClientRegenerateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.REGENERATE,\n (target) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.REGENERATE),\n );\n}\n"]}
1
+ {"version":3,"file":"oauth-clients.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/security/oauth-clients/oauth-clients.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * OAuth Clients Custom Implementations\n *\n * Implements the server-side behaviour for the organization + application OAuth-client\n * resources (`organizationOAuthClients`, `applicationOAuthClients`) that the schema\n * alone cannot express. FAITHFUL MIRROR of the api-keys mint pipeline\n * (`api-keys.custom-impl.backend.service.ts`), FOCUSED to the trunk operation set:\n *\n * CREATE → mint fresh secret material (the client must never supply it).\n * ROTATE → reissue the secret; retain the prior secret in a grace window that\n * stops authenticating at the caller-chosen `oldSecretInvalidationDate`.\n * REGENERATE → reissue the secret; keep the prior secret valid open-endedly (until\n * the next rotation) for a zero-downtime overlap.\n *\n * (api-keys' DEACTIVATE/REACTIVATE/EXTEND_EXPIRY/USAGE_STATS impls are deferred to the\n * OIDC branch alongside their resource-config ops.)\n *\n * WHY MINTING NEEDS AN IMPL: the schema requires `secretPrefix` + `hashedSecret`, but\n * both are `.excludeFromCreate().excludeFromUpdate()` (the client must never supply\n * them) and `plainSecret` is an ephemeral response-only field. Nothing generated them,\n * so a bare create/update persisted a row missing the required fields. These impls are\n * the single server-side choke point that mints secret material.\n *\n * PATTERN (CREATE / ROTATE / REGENERATE — all \"mint a secret\" ops):\n * prefixCoreOperations → mint material; inject the persisted `secretPrefix` +\n * `hashedSecret` (and, for rotate/regenerate, the previous-\n * secret grace-window fields) into the payload. Those fields\n * are `.excludeFrom{Create,Update}()`, so returning them from\n * the prefix is what carries them into the core write — the\n * orchestration diffs the prefix-added keys and forwards them\n * to the repository's `preserveServerInjectedFields` allowlist\n * so the final strict parse re-attaches (rather than strips)\n * them. Stash the one-time `plainSecret` for the postfix.\n * postfixCoreOperations → attach the one-time `plainSecret` (+ rotate's `oldClientId` /\n * `oldSecretInvalidationDate`) to the response so the\n * operation's ephemeral `plainSecret` field surfaces it.\n * The core write still runs, so notifications / websocket events fire normally — which\n * is why this is prefix+postfix rather than `replaceCoreOperations`.\n *\n * VARIANT COVERAGE: a custom impl is matched to a running op by its FULL operation-path\n * identity — `isOperationDefault` (the `isDefault` sentinel) vs `variantKey` produce\n * DISTINCT keys (`createOperationPathKey`). So a single default registration only\n * intercepts the default variant; the cross-tenant app-super-admin `admin` variant\n * (org clients only) needs its own registration or it runs the bare core op and fails.\n * `OAUTH_CLIENT_TARGETS` enumerates every (resource × variant) each op exposes. Minting\n * is org-agnostic — the target `organizationId` rides in the payload / pre-fetched\n * record, never from these handlers — so the admin variant reuses the identical handlers\n * and (per the user security decision) also receives the tenant's one-time `plainSecret`.\n */\n\nimport type { AdmissionWritePrecondition } from '~wildo-backend-lib/base-classes/utils/admission-write-precondition.backend.utils';\nimport {\n ApiKeyScope,\n AppOAuthClients_Operations,\n CoreResourceOperation,\n CoreResourceType,\n ErrorCustomMessageReference,\n ErrorType,\n OAuthClientStatus,\n OrgOAuthClients_Operations,\n ResourceOperationVariantType,\n effectiveRotationInvalidationDate,\n} from '@wildo-ai/saas-models';\nimport type { OAuthClientBase } 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 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 { generateOAuthClientSecretMaterial } from './oauth-client-generation.backend.utils';\nimport type { Container } from 'inversify';\nimport { assertRequestedRolesWithinCallerCeiling, createRoleCeilingGate } from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\nimport { assertMachineCredentialRolesOfferedByOrganizationType } from '~wildo-backend-lib/security/authorizations/machine-credential-organization-roles.backend.utils';\nimport type { RoleCeilingGate } from '~wildo-backend-lib/security/authorizations/role-ceiling.backend.utils';\n\n/**\n * Bridge for the one-time plaintext secret between the CREATE prefix (which mints it)\n * and the postfix (which returns it), keyed by the per-operation `ExecutionContext`\n * INSTANCE. The orchestration threads the SAME `executionContext` through both handlers\n * (`services-registry-handler.backend.service.ts` prefix + postfix calls), so it is a\n * stable, per-invocation correlation key.\n *\n * A `WeakMap` keyed by the EC is deliberately chosen over a `Map` keyed by `hashedSecret`\n * / `_id`: (1) NO LEAK — the entry is reclaimed by GC when the request's EC becomes\n * unreachable, so a core-write failure between prefix and postfix cannot strand the\n * plaintext in memory; (2) NO RACE — concurrent operations run on DISTINCT EC instances,\n * so two simultaneous mints/reissues never collide on a shared key. `plainSecret` is\n * never persisted; the postfix also deletes the entry for promptness.\n */\nconst PLAINTEXT_SECRET_BY_EC = new WeakMap<ExecutionContext<any>, string>();\n\n/**\n * Bridge for ROTATE / REGENERATE, keyed by the same per-operation `ExecutionContext`\n * instance (see {@link PLAINTEXT_SECRET_BY_EC}). Carries the one-time `plainSecret` AND\n * the applied `previousSecretInvalidationDate`, so the postfix builds ROTATE's\n * `oldSecretInvalidationDate` from the value the prefix actually applied — independent of\n * whether the update result round-trips that field.\n */\nconst REISSUE_BRIDGE_BY_EC = new WeakMap<ExecutionContext<any>, { plainSecret: string; previousSecretInvalidationDate: Date | null }>();\n\n/** ROTATE keeps a bounded grace window; REGENERATE keeps an open-ended overlap. Exported for the role-ceiling pin. */\nexport enum OAuthClientReissueKind {\n ROTATE = 'rotate',\n REGENERATE = 'regenerate',\n}\n\n/**\n * The pre-fetched client fields the reissue prefix reads. `hashedSecret`/`secretPrefix`\n * are present on every persisted client of either scope.\n */\ntype ReissuableOAuthClient = Pick<OAuthClientBase, 'status' | 'roles'> & { hashedSecret: string; secretPrefix: string };\n\n/**\n * Structural view of a per-resource custom-operation enum — the members that have a\n * backend impl in this file. Both `OrgOAuthClients_Operations` and\n * `AppOAuthClients_Operations` satisfy it (identical string values).\n */\ntype OAuthClientCustomOps = { ROTATE: string; REGENERATE: string; DEACTIVATE: string; REACTIVATE: string; EXPIRE: string };\n\ntype OAuthClientTarget = {\n resourceType: CoreResourceType;\n scope: ApiKeyScope;\n ops: OAuthClientCustomOps;\n variants: ReadonlyArray<{ isOperationDefault: boolean; variantKey?: string }>;\n};\n\n/** The handler-object shape accepted by `createResourceCustomServiceImplementation`. */\nexport type OAuthClientImplHandlers = Parameters<typeof createResourceCustomServiceImplementation>[0]['handlers'];\n\n/**\n * The OAuth-client resources, their scope (drives the `oc_org_` / `oc_app_` prefix), the\n * per-resource custom-op enum, and every op VARIANT each resource exposes.\n *\n * - ORG clients expose an org-admin DEFAULT + a cross-tenant app-super-admin `admin`\n * variant → both registered.\n * - APP clients are single-variant (APP_ADMIN-scoped) → default only.\n */\nconst OAUTH_CLIENT_TARGETS: ReadonlyArray<OAuthClientTarget> = [\n {\n resourceType: CoreResourceType.OAUTH_CLIENTS_ORGANIZATION,\n scope: ApiKeyScope.ORGANIZATION,\n ops: OrgOAuthClients_Operations,\n variants: [\n { isOperationDefault: true },\n { isOperationDefault: false, variantKey: 'admin' },\n ],\n },\n {\n resourceType: CoreResourceType.OAUTH_CLIENTS_APPLICATION,\n scope: ApiKeyScope.APPLICATION,\n ops: AppOAuthClients_Operations,\n variants: [\n { isOperationDefault: true },\n ],\n },\n];\n\n/**\n * #2297 wave 3: every OAuth-client write's hooks may run inside the transaction that keeps its audit row (reviewed\n * 2026-11-10). They do nothing outside the database: the role ceiling is an in-memory check (its refusal audit is\n * written just before the throw, and nothing commits after it); secrets come from `node:crypto`; the plaintext crosses\n * from PREFIX to POSTFIX in memory, overwritten by a retried attempt; the organization-type read is of a row the write\n * does not own. Inside the transaction `utils.currentObject` is the row re-read (locked on PostgreSQL).\n */\nconst OAUTH_CLIENT_WRITE_HOOKS_RUN_INSIDE_TRANSACTION = ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION;\n\n/**\n * Register one custom-impl factory per (target × variant) for a single operation.\n * Every OAuth-client op is API_CALL and needs a distinct registration per variant\n * identity (see the module header) — this centralizes that fan-out.\n */\nfunction oauthClientFactoriesForOperation(\n operationIdentifierFor: (target: OAuthClientTarget) => string | CoreResourceOperation,\n handlersFor: (target: OAuthClientTarget, container: Container) => OAuthClientImplHandlers,\n /** #2543: what the row must still hold for the core write to land (a status transition's starting state). */\n coreWritePrecondition?: AdmissionWritePrecondition,\n): ResourceCustomServiceImplementationFactory[] {\n return OAUTH_CLIENT_TARGETS.flatMap((target) =>\n target.variants.map(({ isOperationDefault, variantKey }) => {\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, container: Container) =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: target.resourceType,\n // API_CALL: the lookup key is built from the RUNNING operation's variantType\n // (`extractOperationPath` passes it through unchanged), and OAuth-client ops\n // are dispatched as direct HTTP API_CALLs. Must match exactly.\n operationIdentifier: operationIdentifierFor(target),\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault,\n variantKey,\n },\n ...(coreWritePrecondition ? { coreWritePrecondition } : {}),\n // Every caller of this helper is a write (#2297).\n hookTransactionPosture: OAUTH_CLIENT_WRITE_HOOKS_RUN_INSIDE_TRANSACTION,\n handlers: handlersFor(target, container),\n });\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n }),\n );\n}\n\n/** CREATE — enforce the role-ceiling, then mint material; surface `plainSecret` once. Exported for focused create-pipeline unit tests. */\nexport function buildOAuthClientMintHandlers(scope: ApiKeyScope, roleCeilingGate: RoleCeilingGate): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n // Role-ceiling gate — a principal cannot mint a client carrying roles it does not itself hold\n // (self-service privilege-escalation prevention; APPLICATION-scoped super-admin + system writes\n // bypass). The ceiling is hierarchy-aware, so an ORG_ADMIN may mint a client carrying the roles\n // its own role confers (ORG_MANAGER/ORG_MEMBER/…) but never ORG_OWNER above it.\n // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — least-privilege on machine-credential minting.\n assertRequestedRolesWithinCallerCeiling(executionContext, (input as { roles?: string[] }).roles, utils.errorBuilder, roleCeilingGate);\n // And the organization's TYPE must offer them, whoever mints (#2648): the ceiling's platform bypass is about height.\n if (scope === ApiKeyScope.ORGANIZATION) {\n await assertMachineCredentialRolesOfferedByOrganizationType({\n roles: (input as { roles?: string[] }).roles,\n organizationId: executionContext.organizationId ?? (input as { organizationId?: string }).organizationId,\n executionContext,\n utils,\n });\n }\n const { plainSecret, secretPrefix, hashedSecret } = generateOAuthClientSecretMaterial(scope);\n // Bridge the one-time secret to the postfix, keyed by the per-op EC instance; never persisted.\n PLAINTEXT_SECRET_BY_EC.set(executionContext, plainSecret);\n // secretPrefix + hashedSecret are `.excludeFromCreate()`; returning them injects\n // them into the create payload (preserveServerInjectedFields re-attaches them past\n // the strict parse) so the row persists with the required fields.\n return { ...(input as Record<string, unknown>), secretPrefix, hashedSecret };\n },\n postfixCoreOperations: async (_id, createdClient, executionContext, _operationPath, _utils) => {\n if (!createdClient || typeof createdClient !== 'object') return createdClient;\n const plainSecret = PLAINTEXT_SECRET_BY_EC.get(executionContext);\n PLAINTEXT_SECRET_BY_EC.delete(executionContext);\n if (!plainSecret) return createdClient;\n return { ...(createdClient as Record<string, unknown>), plainSecret };\n },\n };\n}\n\n/**\n * ROTATE / REGENERATE — reissue the secret on the SAME record, moving the prior secret\n * into the grace-window slot. The client-credentials exchange keeps honoring the prior\n * secret until `previousSecretInvalidationDate` (ROTATE: the caller-chosen instant,\n * defaulting to now = immediate cutover; REGENERATE: `null` = open-ended overlap).\n *\n * Exported for the role-ceiling pin (#3671).\n */\nexport function buildOAuthClientReissueHandlers(scope: ApiKeyScope, kind: OAuthClientReissueKind, roleCeilingGate: RoleCeilingGate): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n // `currentObject` is the pre-fetched DB record for this UPDATE-borrow op — it\n // includes the backend-only `hashedSecret`/`secretPrefix` we move into the\n // previous slot.\n const current = utils.currentObject as ReissuableOAuthClient | undefined;\n\n // Reissuing hands the caller a working secret for THIS client's roles, so the client's roles face the caller's\n // ceiling exactly as a create's requested roles do, and as the api-key reissue verbs' do (#1631 review F1). Without\n // it, an administrator who could not have created an OWNER client could take a live secret for one by rotating it\n // (#3671). Same gate, same refusal (`role_ceiling_exceeded`), same super-administrator and system bypass.\n // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — a credential's secret is handed out only to a caller who holds its roles.\n assertRequestedRolesWithinCallerCeiling(executionContext, current?.roles, utils.errorBuilder, roleCeilingGate);\n\n // Only an ACTIVE client can be reissued (409 for a revoked/expired client —\n // otherwise rotation would silently re-arm a deactivated credential).\n if (current && current.status !== OAuthClientStatus.ACTIVE) {\n throw utils.errorBuilder.buildError(ErrorType.CONFLICT, executionContext, {\n customMessageReference: ErrorCustomMessageReference.CONFLICT,\n context: { message: `OAuth client secret cannot be reissued in status '${current.status}'`, oauthClientStatus: current.status },\n });\n }\n\n const { plainSecret, secretPrefix, hashedSecret } = generateOAuthClientSecretMaterial(scope);\n const previousSecretInvalidationDate = kind === OAuthClientReissueKind.ROTATE\n // A past invalidation date is read as now (#3477): the effect is the default's, so refusing it adds nothing.\n ? effectiveRotationInvalidationDate((input as { oldSecretInvalidationDate?: Date }).oldSecretInvalidationDate)\n : null;\n REISSUE_BRIDGE_BY_EC.set(executionContext, { plainSecret, previousSecretInvalidationDate });\n\n return {\n ...(input as Record<string, unknown>),\n hashedSecret,\n secretPrefix,\n // Move the outgoing secret into the grace-window slot (server-managed,\n // excludeFrom{Create,Update} → carried by preserveServerInjectedFields).\n previousHashedSecret: current?.hashedSecret,\n previousSecretPrefix: current?.secretPrefix,\n previousSecretInvalidationDate,\n };\n },\n postfixCoreOperations: async (_id, updated, executionContext, _operationPath, _utils) => {\n if (!updated || typeof updated !== 'object') return updated;\n const record = updated as Record<string, unknown>;\n const bridged = REISSUE_BRIDGE_BY_EC.get(executionContext);\n REISSUE_BRIDGE_BY_EC.delete(executionContext);\n\n const withPlainSecret = bridged?.plainSecret ? { ...record, plainSecret: bridged.plainSecret } : { ...record };\n if (kind === OAuthClientReissueKind.ROTATE) {\n // ROTATE's response DTO additionally carries the rotated record's id + when the\n // prior secret is invalidated. Same record, so `oldClientId` is its own `_id`.\n return { ...withPlainSecret, oldClientId: _id, oldSecretInvalidationDate: bridged?.previousSecretInvalidationDate };\n }\n // REGENERATE's response DTO is resource + plainSecret only.\n return withPlainSecret;\n },\n };\n}\n\n/** CREATE impls — org default + org `admin` + app default. */\n/**\n * FORCE one OAuth-client status transition — the one handler behind DEACTIVATE (ACTIVE → INACTIVE),\n * REACTIVATE (INACTIVE → ACTIVE) and the sweep's EXPIRE (ACTIVE → EXPIRED, #413).\n *\n * Every status verb on this resource takes an EMPTY request (`requestDto: z.object({})` in the\n * paired configs) and this prefix supplies the transition the verb names: the return is MERGED onto\n * the caller payload by the dispatcher, so the forced `status` is what lands last, and nothing the\n * caller sent is echoed. `status` is a normal updatable field, so returning it lets the borrowed\n * core UPDATE persist it and keeps the response the updated resource.\n *\n * The precondition is what holds for every variant: the config's `enabledCondition` is enforced only\n * for the variant that declares it, and the cross-tenant `admin` variants omit it entirely. It is what keeps three facts true:\n * an INACTIVE client was retired by an administrator, and the sweep relabelling it EXPIRED would\n * erase who ended it; an EXPIRED client is not revived by a status flip, because its expiry was the\n * thing retiring it; and \"deactivate\" cannot be replayed onto a client that is not ACTIVE.\n *\n * What DEACTIVATE stops is new access — the direct-secret path and every mint re-read the client\n * and require ACTIVE. A client-credentials JWT already minted is not re-checked against the client\n * until it expires (#1028), which the configs' contracts state rather than imply otherwise.\n *\n * assurance-control: WILDO.IDENTITY.CREDENTIAL_LIFECYCLE — credential state transitions are\n * explicit and validated, never inferred from a caller-supplied field.\n */\nfunction buildOAuthClientStatusTransitionHandlers(requiredCurrentStatus: OAuthClientStatus, nextStatus: OAuthClientStatus): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, _input, executionContext, _operationPath, utils) => {\n // `currentObject` is the pre-fetched DB record for this UPDATE-borrow op.\n const current = utils.currentObject as { status?: OAuthClientStatus } | undefined;\n\n if (current && current.status !== requiredCurrentStatus) {\n throw utils.errorBuilder.buildError(ErrorType.CONFLICT, executionContext, {\n customMessageReference: ErrorCustomMessageReference.CONFLICT,\n context: {\n message: `OAuth client cannot move to '${nextStatus}' from status '${current.status}'`,\n oauthClientStatus: current.status,\n },\n });\n }\n\n return { status: nextStatus };\n },\n };\n}\n\n/** DEACTIVATE impls — ACTIVE → INACTIVE, forced; org default + org `admin` + app default (#1028). */\nexport function createOAuthClientDeactivateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.DEACTIVATE,\n () => buildOAuthClientStatusTransitionHandlers(OAuthClientStatus.ACTIVE, OAuthClientStatus.INACTIVE),\n { status: [OAuthClientStatus.ACTIVE] },\n );\n}\n\n/** REACTIVATE impls — INACTIVE → ACTIVE, forced. An EXPIRED client is NOT revivable (409). */\nexport function createOAuthClientReactivateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.REACTIVATE,\n () => buildOAuthClientStatusTransitionHandlers(OAuthClientStatus.INACTIVE, OAuthClientStatus.ACTIVE),\n { status: [OAuthClientStatus.INACTIVE] },\n );\n}\n\n/**\n * EXPIRE impls — registered separately from the other ops rather than through\n * `oauthClientFactoriesForOperation`, because that helper hardcodes `variantType: API_CALL` and\n * fans out over each resource's HTTP variants. EXPIRE is INTERNAL_CALL with a single default\n * variant, so it needs its own registration or the lookup key would never match and the guard above\n * would be silently unreachable.\n */\nexport function createOAuthClientExpireFactories(): ResourceCustomServiceImplementationFactory[] {\n return OAUTH_CLIENT_TARGETS.map((target) => {\n const factory = (_resourcesRegistryService: ResourcesRegistryBackendService, _container: Container) =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: target.resourceType,\n operationIdentifier: target.ops.EXPIRE,\n variantType: ResourceOperationVariantType.INTERNAL_CALL,\n isOperationDefault: true,\n },\n // #2543: the prefix's ACTIVE-only precondition, carried by the core write too (where the write runs without a\n // transaction, nothing re-reads the row), so a sweep EXPIRE racing an administrator's DEACTIVATE cannot overwrite\n // it. A lost race answers CONFLICT, inside the audit-row transaction (#2297) as well.\n coreWritePrecondition: { status: [OAuthClientStatus.ACTIVE] },\n hookTransactionPosture: OAUTH_CLIENT_WRITE_HOOKS_RUN_INSIDE_TRANSACTION,\n handlers: buildOAuthClientStatusTransitionHandlers(OAuthClientStatus.ACTIVE, OAuthClientStatus.EXPIRED),\n });\n return factory as unknown as ResourceCustomServiceImplementationFactory;\n });\n}\n\nexport function createOAuthClientCreateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n () => CoreResourceOperation.CREATE,\n (target, container) => buildOAuthClientMintHandlers(target.scope, createRoleCeilingGate(container)),\n );\n}\n\n/**\n * UPDATE — the role ceiling on an edited client (#2321). Exported for the role-ceiling pin.\n *\n * The request carries no secret, status or expiry, so the ceiling is all the prefix owes: a `roles`\n * value the caller could not register is refused before anything is written. Redirect URIs keep their\n * safety validation in the request schema itself.\n */\nexport function buildOAuthClientUpdateHandlers(roleCeilingGate: RoleCeilingGate): OAuthClientImplHandlers {\n return {\n prefixCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n const requestedRoles = (input as { roles?: string[] } | undefined)?.roles;\n if (requestedRoles !== undefined) {\n // assurance-control: WILDO.ACCESS.LEAST_PRIVILEGE — an edited machine credential holds no role its editor does not.\n assertRequestedRolesWithinCallerCeiling(executionContext, requestedRoles, utils.errorBuilder, roleCeilingGate);\n // An organization client's roles must be offered by its organization's type (#2648); an application client has none.\n await assertMachineCredentialRolesOfferedByOrganizationType({\n roles: requestedRoles,\n organizationId: (utils.currentObject as { organizationId?: string } | undefined)?.organizationId,\n executionContext,\n utils,\n });\n }\n return input as Record<string, unknown>;\n },\n };\n}\n\n/** UPDATE impls — org default + org `admin` + app default. */\nexport function createOAuthClientUpdateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n () => CoreResourceOperation.UPDATE,\n (_target, container) => buildOAuthClientUpdateHandlers(createRoleCeilingGate(container)),\n );\n}\n\n/** ROTATE impls — bounded grace window. */\nexport function createOAuthClientRotateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.ROTATE,\n (target, container) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.ROTATE, createRoleCeilingGate(container)),\n );\n}\n\n/** REGENERATE impls — open-ended overlap. */\nexport function createOAuthClientRegenerateFactories(): ResourceCustomServiceImplementationFactory[] {\n return oauthClientFactoriesForOperation(\n (target) => target.ops.REGENERATE,\n (target, container) => buildOAuthClientReissueHandlers(target.scope, OAuthClientReissueKind.REGENERATE, createRoleCeilingGate(container)),\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"anonymous-consent.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/users/services/anonymous-consent.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAWH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAGpH,wBAAgB,+BAA+B,IAAI,0CAA0C,EAAE,CAwH9F"}
1
+ {"version":3,"file":"anonymous-consent.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/users/services/anonymous-consent.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAWH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AAGpH,wBAAgB,+BAA+B,IAAI,0CAA0C,EAAE,CA4H9F"}
@@ -56,9 +56,13 @@ export function createAnonymousConsentFactories() {
56
56
  * COMPOSES the stored block from two purpose lists, which is a stronger guarantee than
57
57
  * overwriting a field the caller could have sent.
58
58
  */
59
- // #2297: the decision is read and written through utils.serviceOptions; the patch is computed in memory. (The
60
- // operation borrows UPDATE_MANY, so it stays best effort as a multi-row write all the same.)
59
+ // #2297: the decision is read and written through utils.serviceOptions; the patch is computed in memory.
61
60
  hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,
61
+ // #2297 wave 4: the operation borrows UPDATE_MANY for its collection-shaped route, but REPLACE writes ONE row: a
62
+ // single-row `repository.update` addressed by `uuidRef`, which the schema declares globally unique
63
+ // (`anonymous-users.shared.schema.ts`, `.isUnique({ scope: 'global' })`). So one attempt holds one row, and the write is
64
+ // made transactional for its audit row like any single-row write.
65
+ maximumRowsWrittenPerAttempt: 1,
62
66
  handlers: {
63
67
  replaceCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {
64
68
  /*
@@ -1 +1 @@
1
- {"version":3,"file":"anonymous-consent.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/users/services/anonymous-consent.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ANONYMOUS_USERS `record_consent` — the door a visitor's own consent decision goes through (#1258).\n *\n * ## Why this operation needs an implementation at all\n *\n * Every other write on this resource is `INTERNAL_CALL`, so none of them resolves a row from a\n * request. This one does, and the way it resolves the row IS the security control:\n *\n * - **the selector is server-derived.** `executionContext.anonymousUserId` is the `uuidRef` the\n * anonymous-session JWT was verified to carry. Nothing in the request names a row, so describing\n * ANOTHER visitor's decision is not expressible rather than merely refused.\n * - **the row cannot be addressed by `_id`.** An anonymous session's identity is its client\n * `uuidRef`; its `_id` is a server ObjectId. The READ variant on this resource documents that trap\n * and is why anon self-read was never offered. Reading by `{ uuidRef }` is the correction, not a\n * workaround.\n * - **nothing else confines this write.** `ANONYMOUS_USERS` is its own scope ROOT, so both\n * repositories blacklist the anonymous axis and the contextual filter contributes no predicate\n * here — which is why LIST is super-admin. The filter built below is the whole of the confinement,\n * and it is what the tests pin.\n *\n * ## `replaceCoreOperations`, not a prefix\n *\n * The declared `resourceOperationLike` is `UPDATE_MANY`, which puts the route at the collection with\n * no entity id — the only shape in which the request cannot name a row. The core bulk write that\n * shape would otherwise perform is exactly what must NOT happen, so this replaces it with a single\n * filtered update. The surrounding pipeline (DTO parse, `authoritativeFields`, the\n * `auditBelowRiskFloor` emission) is untouched.\n *\n * ## The decision instant is the server's, through the shared rule\n *\n * `buildConsentDecisionPatch` decides whether this write IS a decision and stamps the instant. Using\n * it here rather than writing `new Date()` is what keeps one rule across the three holders — a\n * member's preferences, a lead's opt-in and a visitor's session — so two doors cannot come to\n * disagree about when a decision happened.\n *\n * assurance-control: WILDO.PRIVACY.CONSENT_RECORD — a visitor's own decision is recorded against the\n * session it was made in, before they have an account.\n */\n\nimport {\n AnonymousUsers_Operations,\n buildConsentDecisionPatch,\n CoreResourceType,\n ResourceOperationVariantType,\n normalizeConsentPurposes,\n} from '@wildo-ai/saas-models';\nimport type { AnonymousUser } from '@wildo-ai/saas-models';\nimport { ErrorCustomMessageReference, ErrorType } 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 { engineRepositoriesRegistry } from '~wildo-backend-lib/base-classes/services/services/engine-repositories-registry.backend.utils';\n\nexport function createAnonymousConsentFactories(): ResourceCustomServiceImplementationFactory[] {\n const recordConsentFactory: ResourceCustomServiceImplementationFactory = () =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.ANONYMOUS_USERS,\n operationIdentifier: AnonymousUsers_Operations.RECORD_CONSENT,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n /*\n * No `authoritativeFields`. The framework refuses the declaration on a replace-only\n * implementation — it takes effect in the PREFIX phase, which this has none of — and that\n * refusal is right twice over: the request DTO carries no `privacy` at all, so there is no\n * caller-supplied value for an authoritative-field overwrite to protect against. The handler\n * COMPOSES the stored block from two purpose lists, which is a stronger guarantee than\n * overwriting a field the caller could have sent.\n */\n // #2297: the decision is read and written through utils.serviceOptions; the patch is computed in memory. (The\n // operation borrows UPDATE_MANY, so it stays best effort as a multi-row write all the same.)\n hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,\n handlers: {\n replaceCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n /*\n * The verified session identity. Its ABSENCE is a configuration failure rather than a\n * caller error: the operation declares `APP_ANONYMOUS`, and an execution context holding\n * that role without an `anonymousUserId` would mean the anonymous authentication path\n * admitted a request it could not identify. Refuse rather than fall back to any other\n * selector — a write with no confinement on a scope-root resource would reach every row.\n */\n const anonymousUserId = executionContext.anonymousUserId;\n if (!anonymousUserId) {\n throw utils.errorBuilder.buildError(ErrorType.AUTHORIZATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.AUTHORIZATIONS_ACCESS_DENIED,\n context: { message: 'Recording a visitor consent decision requires a verified anonymous session.' },\n });\n }\n\n /*\n * The RESOURCE shape, not the request's. The variant's `basicPrefixOperation` maps the\n * request's two purpose lists onto `{ privacy: { … } }` before the pipeline validates it —\n * the `reset-to-defaults` idiom — because two purpose lists are not schema paths and the\n * service tier validates against the resource schema. Reading the request shape here made\n * the operation answer `service_resource_validation_failed` on every call (#1267, found by\n * the first live call and by nothing else).\n *\n * Re-normalized and re-intersected rather than trusted: this handler is reachable by any\n * dispatch of the operation, and the stored form must be de-duplicated and in vocabulary\n * order, or two payloads naming the same purposes differently read as two decisions to the\n * audit trail's before/after comparison. A grant for a purpose the same payload does not\n * list as offered is DROPPED — only the intersection is a statement worth storing.\n */\n const decided = (input as { privacy?: { consentedPurposes?: readonly string[]; offeredPurposes?: readonly string[] } }).privacy;\n const offeredPurposes = normalizeConsentPurposes(decided?.offeredPurposes);\n const grantedPurposes = normalizeConsentPurposes(decided?.consentedPurposes)\n .filter((purpose) => offeredPurposes.includes(purpose));\n\n const repository = engineRepositoriesRegistry(utils).getRepositoryOrThrow(CoreResourceType.ANONYMOUS_USERS, executionContext);\n /*\n * The filter is passed BARE, not wrapped in `{ filter: … }`. The repository's\n * `id_or_filter` parameter takes the predicate itself; wrapping it made the tier validate a\n * field literally named `filter.uuidRef` and refuse with `service_resource_validation_failed`\n * — a 400 on every call, found by the live probe and by nothing else (#1267).\n *\n * `AnonymousSessionBackendService.recordTranspositionOutcome` is the local precedent and\n * uses exactly this form, on this resource, for the same reason: the row's `_id` is a server\n * ObjectId while its identity is the client `uuidRef`.\n */\n const selector = { uuidRef: anonymousUserId };\n\n const current = await repository.read<AnonymousUser>(executionContext, selector, utils.serviceOptions);\n if (!current) {\n /*\n * The JWT verified, so the session existed when it was minted. A missing row means the\n * anonymous-session cleanup batch has since collected it (48h TTL) or the session was\n * converted. NOT_FOUND is the honest answer; the visitor's client re-initializes and\n * decides again.\n */\n throw utils.errorBuilder.buildError(ErrorType.NOT_FOUND, executionContext, {\n customMessageReference: ErrorCustomMessageReference.NOT_FOUND,\n context: { message: 'This anonymous session no longer exists.' },\n });\n }\n\n // One rule for when a write is a DECISION, shared with the member and lead doors. The\n // client instant is deliberately not accepted, so `consentDecidedAt` is only ever the\n // server's clock.\n const patch = buildConsentDecisionPatch<AnonymousUser>(\n { privacy: { consentedPurposes: grantedPurposes, consentDecidedAt: new Date() } } as Partial<AnonymousUser>,\n current,\n );\n\n const privacy = {\n ...(patch.privacy ?? current.privacy ?? {}),\n consentedPurposes: grantedPurposes,\n offeredPurposes,\n };\n\n await repository.update<AnonymousUser>(\n executionContext,\n selector,\n { privacy } as never,\n utils.serviceOptions,\n );\n\n /*\n * The DECISION, not the row. Returning the updated `AnonymousUser` failed the controller's\n * response validation AFTER the write had landed — a 400 on a call that had already done its\n * job (#1267) — and it would have handed an anonymous caller `userAgent`, `ipHash` and a\n * field marked `.isBackendOnly()` in exchange for a consent click.\n */\n return {\n consentedPurposes: privacy.consentedPurposes,\n offeredPurposes: privacy.offeredPurposes,\n ...(privacy.consentDecidedAt !== undefined ? { consentDecidedAt: privacy.consentDecidedAt } : {}),\n };\n },\n },\n });\n\n return [recordConsentFactory];\n}\n"]}
1
+ {"version":3,"file":"anonymous-consent.custom-impl.backend.service.js","sourceRoot":"","sources":["../../../../src/users/services/anonymous-consent.custom-impl.backend.service.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ANONYMOUS_USERS `record_consent` — the door a visitor's own consent decision goes through (#1258).\n *\n * ## Why this operation needs an implementation at all\n *\n * Every other write on this resource is `INTERNAL_CALL`, so none of them resolves a row from a\n * request. This one does, and the way it resolves the row IS the security control:\n *\n * - **the selector is server-derived.** `executionContext.anonymousUserId` is the `uuidRef` the\n * anonymous-session JWT was verified to carry. Nothing in the request names a row, so describing\n * ANOTHER visitor's decision is not expressible rather than merely refused.\n * - **the row cannot be addressed by `_id`.** An anonymous session's identity is its client\n * `uuidRef`; its `_id` is a server ObjectId. The READ variant on this resource documents that trap\n * and is why anon self-read was never offered. Reading by `{ uuidRef }` is the correction, not a\n * workaround.\n * - **nothing else confines this write.** `ANONYMOUS_USERS` is its own scope ROOT, so both\n * repositories blacklist the anonymous axis and the contextual filter contributes no predicate\n * here — which is why LIST is super-admin. The filter built below is the whole of the confinement,\n * and it is what the tests pin.\n *\n * ## `replaceCoreOperations`, not a prefix\n *\n * The declared `resourceOperationLike` is `UPDATE_MANY`, which puts the route at the collection with\n * no entity id — the only shape in which the request cannot name a row. The core bulk write that\n * shape would otherwise perform is exactly what must NOT happen, so this replaces it with a single\n * filtered update. The surrounding pipeline (DTO parse, `authoritativeFields`, the\n * `auditBelowRiskFloor` emission) is untouched.\n *\n * ## The decision instant is the server's, through the shared rule\n *\n * `buildConsentDecisionPatch` decides whether this write IS a decision and stamps the instant. Using\n * it here rather than writing `new Date()` is what keeps one rule across the three holders — a\n * member's preferences, a lead's opt-in and a visitor's session — so two doors cannot come to\n * disagree about when a decision happened.\n *\n * assurance-control: WILDO.PRIVACY.CONSENT_RECORD — a visitor's own decision is recorded against the\n * session it was made in, before they have an account.\n */\n\nimport {\n AnonymousUsers_Operations,\n buildConsentDecisionPatch,\n CoreResourceType,\n ResourceOperationVariantType,\n normalizeConsentPurposes,\n} from '@wildo-ai/saas-models';\nimport type { AnonymousUser } from '@wildo-ai/saas-models';\nimport { ErrorCustomMessageReference, ErrorType } 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 { engineRepositoriesRegistry } from '~wildo-backend-lib/base-classes/services/services/engine-repositories-registry.backend.utils';\n\nexport function createAnonymousConsentFactories(): ResourceCustomServiceImplementationFactory[] {\n const recordConsentFactory: ResourceCustomServiceImplementationFactory = () =>\n createResourceCustomServiceImplementation({\n operationPath: {\n resourceIdentifier: CoreResourceType.ANONYMOUS_USERS,\n operationIdentifier: AnonymousUsers_Operations.RECORD_CONSENT,\n variantType: ResourceOperationVariantType.API_CALL,\n isOperationDefault: true,\n },\n /*\n * No `authoritativeFields`. The framework refuses the declaration on a replace-only\n * implementation — it takes effect in the PREFIX phase, which this has none of — and that\n * refusal is right twice over: the request DTO carries no `privacy` at all, so there is no\n * caller-supplied value for an authoritative-field overwrite to protect against. The handler\n * COMPOSES the stored block from two purpose lists, which is a stronger guarantee than\n * overwriting a field the caller could have sent.\n */\n // #2297: the decision is read and written through utils.serviceOptions; the patch is computed in memory.\n hookTransactionPosture: ResourceCustomServiceImplementationHookTransactionPosture.RUNS_INSIDE_TRANSACTION,\n // #2297 wave 4: the operation borrows UPDATE_MANY for its collection-shaped route, but REPLACE writes ONE row: a\n // single-row `repository.update` addressed by `uuidRef`, which the schema declares globally unique\n // (`anonymous-users.shared.schema.ts`, `.isUnique({ scope: 'global' })`). So one attempt holds one row, and the write is\n // made transactional for its audit row like any single-row write.\n maximumRowsWrittenPerAttempt: 1,\n handlers: {\n replaceCoreOperations: async (_id, input, executionContext, _operationPath, utils) => {\n /*\n * The verified session identity. Its ABSENCE is a configuration failure rather than a\n * caller error: the operation declares `APP_ANONYMOUS`, and an execution context holding\n * that role without an `anonymousUserId` would mean the anonymous authentication path\n * admitted a request it could not identify. Refuse rather than fall back to any other\n * selector — a write with no confinement on a scope-root resource would reach every row.\n */\n const anonymousUserId = executionContext.anonymousUserId;\n if (!anonymousUserId) {\n throw utils.errorBuilder.buildError(ErrorType.AUTHORIZATION, executionContext, {\n customMessageReference: ErrorCustomMessageReference.AUTHORIZATIONS_ACCESS_DENIED,\n context: { message: 'Recording a visitor consent decision requires a verified anonymous session.' },\n });\n }\n\n /*\n * The RESOURCE shape, not the request's. The variant's `basicPrefixOperation` maps the\n * request's two purpose lists onto `{ privacy: { … } }` before the pipeline validates it —\n * the `reset-to-defaults` idiom — because two purpose lists are not schema paths and the\n * service tier validates against the resource schema. Reading the request shape here made\n * the operation answer `service_resource_validation_failed` on every call (#1267, found by\n * the first live call and by nothing else).\n *\n * Re-normalized and re-intersected rather than trusted: this handler is reachable by any\n * dispatch of the operation, and the stored form must be de-duplicated and in vocabulary\n * order, or two payloads naming the same purposes differently read as two decisions to the\n * audit trail's before/after comparison. A grant for a purpose the same payload does not\n * list as offered is DROPPED — only the intersection is a statement worth storing.\n */\n const decided = (input as { privacy?: { consentedPurposes?: readonly string[]; offeredPurposes?: readonly string[] } }).privacy;\n const offeredPurposes = normalizeConsentPurposes(decided?.offeredPurposes);\n const grantedPurposes = normalizeConsentPurposes(decided?.consentedPurposes)\n .filter((purpose) => offeredPurposes.includes(purpose));\n\n const repository = engineRepositoriesRegistry(utils).getRepositoryOrThrow(CoreResourceType.ANONYMOUS_USERS, executionContext);\n /*\n * The filter is passed BARE, not wrapped in `{ filter: … }`. The repository's\n * `id_or_filter` parameter takes the predicate itself; wrapping it made the tier validate a\n * field literally named `filter.uuidRef` and refuse with `service_resource_validation_failed`\n * — a 400 on every call, found by the live probe and by nothing else (#1267).\n *\n * `AnonymousSessionBackendService.recordTranspositionOutcome` is the local precedent and\n * uses exactly this form, on this resource, for the same reason: the row's `_id` is a server\n * ObjectId while its identity is the client `uuidRef`.\n */\n const selector = { uuidRef: anonymousUserId };\n\n const current = await repository.read<AnonymousUser>(executionContext, selector, utils.serviceOptions);\n if (!current) {\n /*\n * The JWT verified, so the session existed when it was minted. A missing row means the\n * anonymous-session cleanup batch has since collected it (48h TTL) or the session was\n * converted. NOT_FOUND is the honest answer; the visitor's client re-initializes and\n * decides again.\n */\n throw utils.errorBuilder.buildError(ErrorType.NOT_FOUND, executionContext, {\n customMessageReference: ErrorCustomMessageReference.NOT_FOUND,\n context: { message: 'This anonymous session no longer exists.' },\n });\n }\n\n // One rule for when a write is a DECISION, shared with the member and lead doors. The\n // client instant is deliberately not accepted, so `consentDecidedAt` is only ever the\n // server's clock.\n const patch = buildConsentDecisionPatch<AnonymousUser>(\n { privacy: { consentedPurposes: grantedPurposes, consentDecidedAt: new Date() } } as Partial<AnonymousUser>,\n current,\n );\n\n const privacy = {\n ...(patch.privacy ?? current.privacy ?? {}),\n consentedPurposes: grantedPurposes,\n offeredPurposes,\n };\n\n await repository.update<AnonymousUser>(\n executionContext,\n selector,\n { privacy } as never,\n utils.serviceOptions,\n );\n\n /*\n * The DECISION, not the row. Returning the updated `AnonymousUser` failed the controller's\n * response validation AFTER the write had landed — a 400 on a call that had already done its\n * job (#1267) — and it would have handed an anonymous caller `userAgent`, `ipHash` and a\n * field marked `.isBackendOnly()` in exchange for a consent click.\n */\n return {\n consentedPurposes: privacy.consentedPurposes,\n offeredPurposes: privacy.offeredPurposes,\n ...(privacy.consentDecidedAt !== undefined ? { consentDecidedAt: privacy.consentDecidedAt } : {}),\n };\n },\n },\n });\n\n return [recordConsentFactory];\n}\n"]}
@@ -57,6 +57,13 @@ import { type ResourceCustomServiceImplementationFactory } from '../../base-clas
57
57
  *
58
58
  * The validation is re-derived in REPLACE rather than carried over. It is pure, so this costs
59
59
  * nothing, and it keeps REPLACE correct on its own terms instead of trusting that PREFIX ran.
60
+ *
61
+ * # And why the revocation runs in `beforeTransactionalCore` (#2297)
62
+ *
63
+ * The operation is transactional, so its audit row is kept with the commit. The session revocation is a Redis write no
64
+ * rollback can undo, and it must still precede the token (a failed revocation mints nothing), so it runs in the phase
65
+ * that runs once, OUTSIDE the transaction and before it: the same validation first, then the revocation. Only the token
66
+ * write runs inside; the email and the forced-reset record run after the commit.
60
67
  */
61
68
  export declare function createUserForcePasswordResetFactories(): ResourceCustomServiceImplementationFactory[];
62
69
  //# sourceMappingURL=user-force-password-reset.custom-impl.backend.service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"user-force-password-reset.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/users/services/user-force-password-reset.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAUH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AA2GpH;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,qCAAqC,IAAI,0CAA0C,EAAE,CA+DpG"}
1
+ {"version":3,"file":"user-force-password-reset.custom-impl.backend.service.d.ts","sourceRoot":"","sources":["../../../../src/users/services/user-force-password-reset.custom-impl.backend.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAUH,OAAO,EAGL,KAAK,0CAA0C,EAChD,MAAM,4GAA4G,CAAC;AA2IpH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,qCAAqC,IAAI,0CAA0C,EAAE,CAiFpG"}