@flowdular/sdk 0.2.3 → 0.3.0

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 (912) hide show
  1. package/.ai/agents/sandbox/business-manager.md +5 -1
  2. package/.ai/blueprints/author-spec/README.md +1 -1
  3. package/.ai/blueprints/author-spec/spec-requirements.yaml +44 -0
  4. package/.ai/blueprints/author-spec/steps.yaml +5 -5
  5. package/.ai/blueprints/author-spec/templates/module.yaml +99 -3
  6. package/.ai/blueprints/edit-module/gates.yaml +4 -0
  7. package/.ai/blueprints/edit-module/required-files.yaml +9 -0
  8. package/.ai/blueprints/new-module/gates.yaml +4 -0
  9. package/.ai/blueprints/new-module/spec-requirements.yaml +2 -2
  10. package/.ai/blueprints/release/gates.yaml +4 -0
  11. package/.ai/platform-capabilities.md +128 -0
  12. package/.ai/policies/capabilities.yaml +100 -0
  13. package/.ai/policies/path-ownership.yaml +5 -2
  14. package/.ai/policies/task-budgets.yaml +5 -3
  15. package/.ai/references/catalog/module.json +4 -4
  16. package/.ai/references/catalog/package.json +2 -2
  17. package/.ai/references/catalog/spec/module.yaml +5 -3
  18. package/.ai/references/catalog/src/platform.ts +2 -0
  19. package/.ai/references/catalog/src/services/catalog-service.ts +89 -1
  20. package/.ai/references/catalog/src/services/data-classes.ts +47 -0
  21. package/.ai/references/catalog/src/services/database-repository.ts +98 -1
  22. package/.ai/references/catalog/src/services/repository.ts +22 -1
  23. package/.ai/references/catalog/tests/data-classes.test.ts +157 -0
  24. package/.ai/references/catalog.provenance.json +12 -10
  25. package/.ai/skills/README.md +10 -0
  26. package/.ai/skills/agent-tool-design/SKILL.md +1 -2
  27. package/.ai/skills/auth-security-review/SKILL.md +1 -1
  28. package/.ai/skills/business-agent-design/SKILL.md +0 -1
  29. package/.ai/skills/deploy-operate/SKILL.md +114 -0
  30. package/.ai/skills/module-new/SKILL.md +29 -3
  31. package/.ai/skills/module-update/SKILL.md +9 -3
  32. package/.ai/skills/perf-audit/SKILL.md +0 -1
  33. package/.ai/skills/release-eject-pr/SKILL.md +0 -1
  34. package/.ai/skills/spec-interview/SKILL.md +120 -0
  35. package/.ai/skills/test-hardening/SKILL.md +1 -0
  36. package/.ai/skills/ux-design/SKILL.md +34 -3
  37. package/.ai/skills/variables/SKILL.md +0 -2
  38. package/.ai/skills/workflow-development/SKILL.md +0 -1
  39. package/AGENTS.md +4 -0
  40. package/docs/agent-contract.md +2 -2
  41. package/docs/design-system.md +185 -31
  42. package/modules/access/migrations/0001_access_core.down.sql +1 -0
  43. package/modules/access/migrations/0001_access_core.up.sql +28 -0
  44. package/modules/access/migrations/README.md +3 -0
  45. package/modules/access/module.json +33 -0
  46. package/modules/access/package.json +43 -0
  47. package/modules/access/spec/module.yaml +225 -0
  48. package/modules/access/src/acl/permissions.ts +6 -0
  49. package/modules/access/src/api/endpoints.ts +244 -0
  50. package/modules/access/src/client/ActivityView.tsrx +245 -0
  51. package/modules/access/src/client/AttestationsView.tsrx +176 -0
  52. package/modules/access/src/client/ReviewView.tsrx +586 -0
  53. package/modules/access/src/client/api.ts +132 -0
  54. package/modules/access/src/client/contribution.tsrx +41 -0
  55. package/modules/access/src/client/index.ts +41 -0
  56. package/modules/access/src/client/navigation.ts +57 -0
  57. package/modules/access/src/client/presentation.ts +69 -0
  58. package/modules/access/src/client/state.ts +72 -0
  59. package/modules/access/src/domain/types.ts +189 -0
  60. package/modules/access/src/index.ts +38 -0
  61. package/modules/access/src/platform.ts +60 -0
  62. package/modules/access/src/server/index.ts +42 -0
  63. package/modules/access/src/server/runtime.ts +105 -0
  64. package/modules/access/src/services/access-service.ts +298 -0
  65. package/modules/access/src/services/audit-window.ts +78 -0
  66. package/modules/access/src/services/auth-directory.ts +72 -0
  67. package/modules/access/src/services/changes.ts +91 -0
  68. package/modules/access/src/services/data-classes.ts +28 -0
  69. package/modules/access/src/services/database-repository.ts +185 -0
  70. package/modules/access/src/services/directory.ts +102 -0
  71. package/modules/access/src/services/index.ts +33 -0
  72. package/modules/access/src/services/list-exports.ts +150 -0
  73. package/modules/access/src/services/migration.ts +54 -0
  74. package/modules/access/src/services/repository.ts +30 -0
  75. package/modules/access/src/services/review.ts +178 -0
  76. package/modules/access/translations/en.json +155 -0
  77. package/modules/access/translations/pl.json +155 -0
  78. package/modules/agents/migrations/0022_credential_rotation_inventory.down.sql +2 -0
  79. package/modules/agents/migrations/0022_credential_rotation_inventory.up.sql +7 -0
  80. package/modules/agents/migrations/0023_agents_retention_indexes.down.sql +2 -0
  81. package/modules/agents/migrations/0023_agents_retention_indexes.up.sql +9 -0
  82. package/modules/agents/migrations/0024_agent_meter_refusals.down.sql +3 -0
  83. package/modules/agents/migrations/0024_agent_meter_refusals.up.sql +22 -0
  84. package/modules/agents/migrations/0025_agent_list_indexes.down.sql +3 -0
  85. package/modules/agents/migrations/0025_agent_list_indexes.up.sql +11 -0
  86. package/modules/agents/module.json +30 -4
  87. package/modules/agents/package.json +3 -2
  88. package/modules/agents/spec/module.yaml +63 -5
  89. package/modules/agents/src/api/endpoints.ts +291 -33
  90. package/modules/agents/src/cli/commands.json +11 -0
  91. package/modules/agents/src/cli/index.ts +45 -0
  92. package/modules/agents/src/client/AgentDashboardWidget.tsrx +19 -11
  93. package/modules/agents/src/client/AgentDefinitionList.tsrx +53 -10
  94. package/modules/agents/src/client/AgentPlaygroundView.tsrx +4 -4
  95. package/modules/agents/src/client/AgentProceduresView.tsrx +2 -2
  96. package/modules/agents/src/client/AgentRunsView.tsrx +143 -40
  97. package/modules/agents/src/client/AgentsView.tsrx +109 -33
  98. package/modules/agents/src/client/api.ts +99 -21
  99. package/modules/agents/src/client/state.ts +97 -0
  100. package/modules/agents/src/domain/types.ts +44 -4
  101. package/modules/agents/src/platform.ts +42 -0
  102. package/modules/agents/src/server/action-execution.ts +201 -106
  103. package/modules/agents/src/server/runtime.ts +32 -0
  104. package/modules/agents/src/services/agent-service.ts +158 -30
  105. package/modules/agents/src/services/credential-rotation.ts +185 -0
  106. package/modules/agents/src/services/credential-vault.ts +71 -46
  107. package/modules/agents/src/services/data-classes.ts +176 -0
  108. package/modules/agents/src/services/database-repository.ts +393 -101
  109. package/modules/agents/src/services/metering.ts +111 -0
  110. package/modules/agents/src/services/migration.ts +109 -0
  111. package/modules/agents/src/services/module-agent-preflight.ts +17 -9
  112. package/modules/agents/src/services/notifications.ts +98 -0
  113. package/modules/agents/src/services/provider-repository.ts +9 -18
  114. package/modules/agents/src/services/provider-service.ts +1 -9
  115. package/modules/agents/src/services/reports.ts +107 -0
  116. package/modules/agents/src/services/repository.ts +86 -6
  117. package/modules/agents/src/services/usage-service.ts +2 -0
  118. package/modules/agents/src/services/worker.ts +114 -4
  119. package/modules/agents/src/settings.ts +14 -3
  120. package/modules/agents/translations/en.json +14 -4
  121. package/modules/agents/translations/pl.json +14 -4
  122. package/modules/approvals/migrations/0001_approvals_core.down.sql +3 -0
  123. package/modules/approvals/migrations/0001_approvals_core.up.sql +78 -0
  124. package/modules/approvals/migrations/0002_approvals_expiry_routing_role.down.sql +2 -0
  125. package/modules/approvals/migrations/0002_approvals_expiry_routing_role.up.sql +19 -0
  126. package/modules/approvals/migrations/0003_approvals_retention_indexes.down.sql +6 -0
  127. package/modules/approvals/migrations/0003_approvals_retention_indexes.up.sql +14 -0
  128. package/modules/approvals/migrations/0004_approvals_inbox_keyset_indexes.down.sql +5 -0
  129. package/modules/approvals/migrations/0004_approvals_inbox_keyset_indexes.up.sql +12 -0
  130. package/modules/approvals/migrations/README.md +3 -0
  131. package/modules/approvals/module.json +34 -0
  132. package/modules/approvals/package.json +43 -0
  133. package/modules/approvals/spec/module.yaml +318 -0
  134. package/modules/approvals/src/acl/permissions.ts +7 -0
  135. package/modules/approvals/src/api/endpoints.ts +457 -0
  136. package/modules/approvals/src/client/ApprovalRequestDrawer.tsrx +138 -0
  137. package/modules/approvals/src/client/ApprovalsView.tsrx +562 -0
  138. package/modules/approvals/src/client/PendingCountWidget.tsrx +52 -0
  139. package/modules/approvals/src/client/api.ts +203 -0
  140. package/modules/approvals/src/client/approvals.css +51 -0
  141. package/modules/approvals/src/client/contribution.tsrx +44 -0
  142. package/modules/approvals/src/client/index.ts +26 -0
  143. package/modules/approvals/src/client/navigation.ts +39 -0
  144. package/modules/approvals/src/client/pending-count.ts +26 -0
  145. package/modules/approvals/src/client/presentation.ts +52 -0
  146. package/modules/approvals/src/client/state.ts +141 -0
  147. package/modules/approvals/src/domain/capability.ts +95 -0
  148. package/modules/approvals/src/domain/types.ts +154 -0
  149. package/modules/approvals/src/index.ts +52 -0
  150. package/modules/approvals/src/platform.ts +128 -0
  151. package/modules/approvals/src/server/index.ts +42 -0
  152. package/modules/approvals/src/server/runtime.ts +181 -0
  153. package/modules/approvals/src/services/approvals-service.ts +557 -0
  154. package/modules/approvals/src/services/callbacks.ts +75 -0
  155. package/modules/approvals/src/services/data-classes.ts +167 -0
  156. package/modules/approvals/src/services/database-repository.ts +724 -0
  157. package/modules/approvals/src/services/eligibility.ts +121 -0
  158. package/modules/approvals/src/services/expiry-runner.ts +81 -0
  159. package/modules/approvals/src/services/index.ts +31 -0
  160. package/modules/approvals/src/services/migration.ts +222 -0
  161. package/modules/approvals/src/services/notifications.ts +95 -0
  162. package/modules/approvals/src/services/repository.ts +161 -0
  163. package/modules/approvals/src/services/service-error.ts +70 -0
  164. package/modules/approvals/src/settings.ts +102 -0
  165. package/modules/approvals/translations/en.json +104 -0
  166. package/modules/approvals/translations/pl.json +104 -0
  167. package/modules/audit/migrations/0001_audit_core.down.sql +8 -0
  168. package/modules/audit/migrations/0001_audit_core.up.sql +107 -0
  169. package/modules/audit/migrations/0002_audit_sweep_routing_role.down.sql +2 -0
  170. package/modules/audit/migrations/0002_audit_sweep_routing_role.up.sql +21 -0
  171. package/modules/audit/migrations/0003_audit_export_request.down.sql +12 -0
  172. package/modules/audit/migrations/0003_audit_export_request.up.sql +40 -0
  173. package/modules/audit/migrations/0004_audit_hold_seal_erasure.down.sql +17 -0
  174. package/modules/audit/migrations/0004_audit_hold_seal_erasure.up.sql +192 -0
  175. package/modules/audit/migrations/0005_audit_event_format.down.sql +17 -0
  176. package/modules/audit/migrations/0005_audit_event_format.up.sql +60 -0
  177. package/modules/audit/migrations/0006_audit_own_class_walks.down.sql +3 -0
  178. package/modules/audit/migrations/0006_audit_own_class_walks.up.sql +14 -0
  179. package/modules/audit/migrations/README.md +3 -0
  180. package/modules/audit/module.json +32 -0
  181. package/modules/audit/package.json +49 -0
  182. package/modules/audit/spec/module.yaml +809 -0
  183. package/modules/audit/src/acl/permissions.ts +7 -0
  184. package/modules/audit/src/api/endpoints.ts +315 -0
  185. package/modules/audit/src/cli/commands.json +95 -0
  186. package/modules/audit/src/cli/index.ts +658 -0
  187. package/modules/audit/src/client/DataClassesView.tsrx +420 -0
  188. package/modules/audit/src/client/ExportsView.tsrx +256 -0
  189. package/modules/audit/src/client/HoldsView.tsrx +604 -0
  190. package/modules/audit/src/client/SweepsView.tsrx +251 -0
  191. package/modules/audit/src/client/api.ts +185 -0
  192. package/modules/audit/src/client/contribution.tsrx +52 -0
  193. package/modules/audit/src/client/index.ts +23 -0
  194. package/modules/audit/src/client/navigation.ts +74 -0
  195. package/modules/audit/src/client/presentation.ts +105 -0
  196. package/modules/audit/src/client/state.ts +86 -0
  197. package/modules/audit/src/domain/data-classes.ts +16 -0
  198. package/modules/audit/src/domain/types.ts +339 -0
  199. package/modules/audit/src/index.ts +76 -0
  200. package/modules/audit/src/platform.ts +44 -0
  201. package/modules/audit/src/server/index.ts +177 -0
  202. package/modules/audit/src/server/runtime.ts +303 -0
  203. package/modules/audit/src/services/anchor-key.ts +143 -0
  204. package/modules/audit/src/services/anchor-rotation.ts +147 -0
  205. package/modules/audit/src/services/audit-runners.ts +278 -0
  206. package/modules/audit/src/services/backup-guard.ts +95 -0
  207. package/modules/audit/src/services/database-repository.ts +1946 -0
  208. package/modules/audit/src/services/declared-classes.ts +64 -0
  209. package/modules/audit/src/services/erasure-port.ts +126 -0
  210. package/modules/audit/src/services/erasure-service.ts +770 -0
  211. package/modules/audit/src/services/export-directory.ts +106 -0
  212. package/modules/audit/src/services/export-service.ts +714 -0
  213. package/modules/audit/src/services/hold-service.ts +295 -0
  214. package/modules/audit/src/services/index.ts +17 -0
  215. package/modules/audit/src/services/migration.ts +658 -0
  216. package/modules/audit/src/services/own-classes.ts +318 -0
  217. package/modules/audit/src/services/platform-version.ts +23 -0
  218. package/modules/audit/src/services/repository.ts +495 -0
  219. package/modules/audit/src/services/retention-service.ts +191 -0
  220. package/modules/audit/src/services/seal-service.ts +769 -0
  221. package/modules/audit/src/services/service-error.ts +61 -0
  222. package/modules/audit/src/services/subject-keys.ts +90 -0
  223. package/modules/audit/src/services/sweep-service.ts +298 -0
  224. package/modules/audit/src/services/zip.ts +217 -0
  225. package/modules/audit/src/settings.ts +69 -0
  226. package/modules/audit/translations/en.json +210 -0
  227. package/modules/audit/translations/pl.json +210 -0
  228. package/modules/auth/README.md +77 -29
  229. package/modules/auth/migrations/0016_external_identities.up.sql +16 -0
  230. package/modules/auth/migrations/0017_mfa_key_id.up.sql +8 -0
  231. package/modules/auth/migrations/0018_retire_bundled_module_scopes.up.sql +29 -0
  232. package/modules/auth/migrations/0019_notifications_member_scopes.up.sql +44 -0
  233. package/modules/auth/migrations/0020_identity_providers.up.sql +61 -0
  234. package/modules/auth/migrations/0021_membership_status.up.sql +19 -0
  235. package/modules/auth/migrations/0022_auth_provider_scopes.up.sql +45 -0
  236. package/modules/auth/migrations/0023_enterprise_module_scopes.down.sql +19 -0
  237. package/modules/auth/migrations/0023_enterprise_module_scopes.up.sql +71 -0
  238. package/modules/auth/migrations/0024_approvals_member_scopes.down.sql +22 -0
  239. package/modules/auth/migrations/0024_approvals_member_scopes.up.sql +48 -0
  240. package/modules/auth/migrations/0025_audit_holds_owner_scope.down.sql +21 -0
  241. package/modules/auth/migrations/0025_audit_holds_owner_scope.up.sql +47 -0
  242. package/modules/auth/migrations/0026_auth_export_keyset_indexes.down.sql +6 -0
  243. package/modules/auth/migrations/0026_auth_export_keyset_indexes.up.sql +15 -0
  244. package/modules/auth/migrations/0027_workflow_automation_profile_scopes.down.sql +34 -0
  245. package/modules/auth/migrations/0027_workflow_automation_profile_scopes.up.sql +72 -0
  246. package/modules/auth/migrations/0028_auth_member_search_prefix_indexes.down.sql +6 -0
  247. package/modules/auth/migrations/0028_auth_member_search_prefix_indexes.up.sql +21 -0
  248. package/modules/auth/migrations/0029_reports_exports_access_scopes.down.sql +22 -0
  249. package/modules/auth/migrations/0029_reports_exports_access_scopes.up.sql +56 -0
  250. package/modules/auth/migrations/0030_auth_membership_keyset_index.down.sql +4 -0
  251. package/modules/auth/migrations/0030_auth_membership_keyset_index.up.sql +10 -0
  252. package/modules/auth/migrations/0031_membership_scope_backfills_under_rls.down.sql +4 -0
  253. package/modules/auth/migrations/0031_membership_scope_backfills_under_rls.up.sql +31 -0
  254. package/modules/auth/migrations/0032_audit_actor_service.down.sql +9 -0
  255. package/modules/auth/migrations/0032_audit_actor_service.up.sql +10 -0
  256. package/modules/auth/migrations/0033_auth_account_keyset_indexes.down.sql +5 -0
  257. package/modules/auth/migrations/0033_auth_account_keyset_indexes.up.sql +23 -0
  258. package/modules/auth/module.json +3 -3
  259. package/modules/auth/package.json +3 -2
  260. package/modules/auth/spec/module.yaml +372 -3
  261. package/modules/auth/src/acl/scopes.ts +50 -6
  262. package/modules/auth/src/cli/commands.json +11 -0
  263. package/modules/auth/src/cli/greenfield.ts +2 -2
  264. package/modules/auth/src/cli/index.ts +14 -0
  265. package/modules/auth/src/cli/secrets.ts +114 -0
  266. package/modules/auth/src/client/AuthenticationCore.tsrx +61 -8
  267. package/modules/auth/src/client/api.ts +72 -6
  268. package/modules/auth/src/client/contribution.tsrx +23 -0
  269. package/modules/auth/src/client/index.ts +6 -0
  270. package/modules/auth/src/client/providers/IdentityProviderForm.tsrx +191 -0
  271. package/modules/auth/src/client/providers/IdentityProvidersView.tsrx +358 -0
  272. package/modules/auth/src/client/providers/api.ts +128 -0
  273. package/modules/auth/src/client/providers/provider-columns.tsrx +60 -0
  274. package/modules/auth/src/client/providers/state.ts +35 -0
  275. package/modules/auth/src/client/security/SecurityView.tsrx +9 -1
  276. package/modules/auth/src/client/state.ts +15 -1
  277. package/modules/auth/src/domain/types.ts +55 -0
  278. package/modules/auth/src/index.ts +11 -1
  279. package/modules/auth/src/middleware/authentication.ts +4 -0
  280. package/modules/auth/src/server/endpoints.ts +467 -171
  281. package/modules/auth/src/server/http.ts +3 -2
  282. package/modules/auth/src/server/index.ts +22 -1
  283. package/modules/auth/src/server/membership-endpoints.ts +62 -0
  284. package/modules/auth/src/server/mfa-enforcement.ts +156 -0
  285. package/modules/auth/src/server/oidc.ts +389 -0
  286. package/modules/auth/src/server/provider-endpoints.ts +246 -0
  287. package/modules/auth/src/server/provider-host.ts +77 -0
  288. package/modules/auth/src/server/runtime.ts +231 -55
  289. package/modules/auth/src/services/auth-service-error.ts +7 -2
  290. package/modules/auth/src/services/auth-service.ts +948 -51
  291. package/modules/auth/src/services/data-classes.ts +236 -0
  292. package/modules/auth/src/services/database-repository.ts +853 -71
  293. package/modules/auth/src/services/identity-provider-service.ts +571 -0
  294. package/modules/auth/src/services/mail-delivery.ts +16 -16
  295. package/modules/auth/src/services/mail-message.ts +54 -0
  296. package/modules/auth/src/services/mail-port.ts +24 -0
  297. package/modules/auth/src/services/mail-smtp.ts +85 -0
  298. package/modules/auth/src/services/mfa-rotation.ts +144 -0
  299. package/modules/auth/src/services/migration.ts +810 -0
  300. package/modules/auth/src/services/password.ts +14 -0
  301. package/modules/auth/src/services/provider-secret-rotation.ts +169 -0
  302. package/modules/auth/src/services/provider-secrets.ts +136 -0
  303. package/modules/auth/src/services/repository.ts +321 -5
  304. package/modules/auth/src/services/session-sweep-runner.ts +73 -0
  305. package/modules/auth/src/services/settings-store.ts +10 -83
  306. package/modules/auth/src/services/totp.ts +88 -62
  307. package/modules/auth/src/services/validation.ts +194 -7
  308. package/modules/auth/src/settings.ts +18 -0
  309. package/modules/auth/translations/en.json +84 -1
  310. package/modules/auth/translations/pl.json +84 -1
  311. package/modules/automations/migrations/0005_secret_rotation_inventory.down.sql +1 -0
  312. package/modules/automations/migrations/0005_secret_rotation_inventory.up.sql +6 -0
  313. package/modules/automations/migrations/0006_automations_list_sort_indexes.down.sql +4 -0
  314. package/modules/automations/migrations/0006_automations_list_sort_indexes.up.sql +11 -0
  315. package/modules/automations/module.json +24 -6
  316. package/modules/automations/package.json +5 -2
  317. package/modules/automations/spec/module.yaml +81 -7
  318. package/modules/automations/src/api/endpoints.ts +202 -28
  319. package/modules/automations/src/cli/commands.json +17 -0
  320. package/modules/automations/src/cli/index.ts +114 -0
  321. package/modules/automations/src/client/AutomationScheduleForm.tsrx +77 -19
  322. package/modules/automations/src/client/AutomationSchedulesView.tsrx +144 -40
  323. package/modules/automations/src/client/AutomationTriggersView.tsrx +131 -32
  324. package/modules/automations/src/client/api.ts +62 -19
  325. package/modules/automations/src/client/presentation.ts +20 -2
  326. package/modules/automations/src/client/state.ts +84 -0
  327. package/modules/automations/src/domain/cadence.ts +99 -3
  328. package/modules/automations/src/domain/cron.ts +366 -0
  329. package/modules/automations/src/domain/time-zone.ts +61 -0
  330. package/modules/automations/src/platform.ts +38 -4
  331. package/modules/automations/src/server/index.ts +11 -0
  332. package/modules/automations/src/server/runtime.ts +46 -22
  333. package/modules/{automations-workflows-integration/src/server/adapter.ts → automations/src/server/workflow-target.ts} +13 -14
  334. package/modules/{automations-workflows-integration/src/server/tools.ts → automations/src/server/workflow-tools.ts} +0 -0
  335. package/modules/automations/src/services/data-classes.ts +62 -0
  336. package/modules/automations/src/services/database-repository.ts +152 -38
  337. package/modules/automations/src/services/migration.ts +119 -9
  338. package/modules/automations/src/services/repository.ts +52 -1
  339. package/modules/automations/src/services/schedule-runner.ts +84 -0
  340. package/modules/automations/src/services/schedule-service.ts +152 -26
  341. package/modules/automations/src/services/secret-rotation.ts +176 -0
  342. package/modules/automations/src/services/secret-vault.ts +68 -41
  343. package/modules/automations/src/services/trigger-service.ts +13 -10
  344. package/modules/automations/translations/en.json +15 -2
  345. package/modules/automations/translations/pl.json +15 -2
  346. package/modules/connectors/migrations/0001_connectors_core.down.sql +3 -0
  347. package/modules/connectors/migrations/0001_connectors_core.up.sql +72 -0
  348. package/modules/connectors/migrations/0002_connectors_call_keys.down.sql +3 -0
  349. package/modules/connectors/migrations/0002_connectors_call_keys.up.sql +18 -0
  350. package/modules/connectors/migrations/README.md +3 -0
  351. package/modules/connectors/module.json +28 -0
  352. package/modules/{automations-workflows-integration → connectors}/package.json +27 -26
  353. package/modules/connectors/spec/module.yaml +447 -0
  354. package/modules/connectors/src/acl/permissions.ts +6 -0
  355. package/modules/connectors/src/agent/tools.ts +180 -0
  356. package/modules/connectors/src/api/endpoints.ts +572 -0
  357. package/modules/connectors/src/client/ConnectorCallsView.tsrx +358 -0
  358. package/modules/connectors/src/client/ConnectorForm.tsrx +424 -0
  359. package/modules/connectors/src/client/ConnectorsView.tsrx +711 -0
  360. package/modules/connectors/src/client/api.ts +281 -0
  361. package/modules/connectors/src/client/consent.ts +48 -0
  362. package/modules/connectors/src/client/contribution.tsrx +37 -0
  363. package/modules/connectors/src/client/index.ts +22 -0
  364. package/modules/connectors/src/client/navigation.ts +42 -0
  365. package/modules/connectors/src/client/presentation.ts +38 -0
  366. package/modules/connectors/src/client/state.ts +175 -0
  367. package/modules/connectors/src/domain/calls.ts +70 -0
  368. package/modules/connectors/src/domain/definitions.ts +190 -0
  369. package/modules/connectors/src/domain/http-json.ts +68 -0
  370. package/modules/connectors/src/domain/types.ts +185 -0
  371. package/modules/connectors/src/index.ts +64 -0
  372. package/modules/connectors/src/platform.ts +56 -0
  373. package/modules/connectors/src/server/index.ts +66 -0
  374. package/modules/connectors/src/server/runtime.ts +163 -0
  375. package/modules/connectors/src/services/call-service.ts +903 -0
  376. package/modules/connectors/src/services/connectors-service.ts +653 -0
  377. package/modules/connectors/src/services/credential-vault.ts +188 -0
  378. package/modules/connectors/src/services/data-classes.ts +158 -0
  379. package/modules/connectors/src/services/database-repository.ts +897 -0
  380. package/modules/connectors/src/services/egress.ts +261 -0
  381. package/modules/connectors/src/services/index.ts +19 -0
  382. package/modules/connectors/src/services/migration.ts +166 -0
  383. package/modules/connectors/src/services/repository.ts +175 -0
  384. package/modules/connectors/src/services/service-error.ts +46 -0
  385. package/modules/connectors/src/settings.ts +69 -0
  386. package/modules/connectors/translations/en.json +191 -0
  387. package/modules/connectors/translations/pl.json +191 -0
  388. package/modules/directory/migrations/0001_directory_core.down.sql +5 -0
  389. package/modules/directory/migrations/0001_directory_core.up.sql +111 -0
  390. package/modules/directory/migrations/README.md +3 -0
  391. package/modules/directory/module.json +27 -0
  392. package/modules/directory/package.json +46 -0
  393. package/modules/directory/spec/module.yaml +351 -0
  394. package/modules/directory/src/acl/permissions.ts +7 -0
  395. package/modules/directory/src/api/endpoints.ts +478 -0
  396. package/modules/directory/src/api/scim-endpoints.ts +516 -0
  397. package/modules/directory/src/client/GroupMappingsView.tsrx +430 -0
  398. package/modules/directory/src/client/ProvisioningLogView.tsrx +248 -0
  399. package/modules/directory/src/client/ScimTokensView.tsrx +587 -0
  400. package/modules/directory/src/client/api.ts +222 -0
  401. package/modules/directory/src/client/contribution.tsrx +46 -0
  402. package/modules/directory/src/client/index.ts +22 -0
  403. package/modules/directory/src/client/navigation.ts +57 -0
  404. package/modules/directory/src/client/presentation.ts +143 -0
  405. package/modules/directory/src/client/state.ts +173 -0
  406. package/modules/directory/src/domain/scim.ts +346 -0
  407. package/modules/directory/src/domain/types.ts +166 -0
  408. package/modules/directory/src/index.ts +44 -0
  409. package/modules/directory/src/platform.ts +43 -0
  410. package/modules/directory/src/server/index.ts +17 -0
  411. package/modules/directory/src/server/runtime.ts +124 -0
  412. package/modules/directory/src/services/auth-port.ts +127 -0
  413. package/modules/directory/src/services/data-classes.ts +41 -0
  414. package/modules/directory/src/services/database-repository.ts +1067 -0
  415. package/modules/directory/src/services/directory-service.ts +311 -0
  416. package/modules/directory/src/services/index.ts +43 -0
  417. package/modules/directory/src/services/migration.ts +146 -0
  418. package/modules/directory/src/services/provisioning-service.ts +1420 -0
  419. package/modules/directory/src/services/rate-limiter.ts +103 -0
  420. package/modules/directory/src/services/repository.ts +208 -0
  421. package/modules/directory/src/services/service-error.ts +42 -0
  422. package/modules/directory/src/services/token-service.ts +215 -0
  423. package/modules/directory/src/settings.ts +86 -0
  424. package/modules/directory/translations/en.json +185 -0
  425. package/modules/directory/translations/pl.json +185 -0
  426. package/modules/documents/migrations/0001_documents_core.down.sql +1 -0
  427. package/modules/documents/migrations/0001_documents_core.up.sql +35 -0
  428. package/modules/documents/migrations/0002_documents_files_page.down.sql +1 -0
  429. package/modules/documents/migrations/0002_documents_files_page.up.sql +6 -0
  430. package/modules/documents/migrations/README.md +3 -0
  431. package/modules/documents/module.json +28 -0
  432. package/modules/documents/package.json +49 -0
  433. package/modules/documents/spec/module.yaml +247 -0
  434. package/modules/documents/src/acl/permissions.ts +6 -0
  435. package/modules/documents/src/api/endpoints.ts +401 -0
  436. package/modules/documents/src/client/DocumentUploader.tsrx +148 -0
  437. package/modules/documents/src/client/DocumentsView.tsrx +540 -0
  438. package/modules/documents/src/client/api.ts +209 -0
  439. package/modules/documents/src/client/contribution.tsrx +29 -0
  440. package/modules/documents/src/client/download.ts +70 -0
  441. package/modules/documents/src/client/index.ts +22 -0
  442. package/modules/documents/src/client/navigation.ts +25 -0
  443. package/modules/documents/src/client/presentation.ts +110 -0
  444. package/modules/documents/src/client/state.ts +117 -0
  445. package/modules/documents/src/domain/attachments.ts +84 -0
  446. package/modules/documents/src/domain/types.ts +80 -0
  447. package/modules/documents/src/index.ts +46 -0
  448. package/modules/documents/src/platform.ts +61 -0
  449. package/modules/documents/src/server/index.ts +30 -0
  450. package/modules/documents/src/server/runtime.ts +107 -0
  451. package/modules/documents/src/services/attachments.ts +39 -0
  452. package/modules/documents/src/services/data-classes.ts +25 -0
  453. package/modules/documents/src/services/database-repository.ts +259 -0
  454. package/modules/documents/src/services/documents-service.ts +586 -0
  455. package/modules/documents/src/services/index.ts +18 -0
  456. package/modules/documents/src/services/migration.ts +82 -0
  457. package/modules/documents/src/services/repository.ts +48 -0
  458. package/modules/documents/src/settings.ts +107 -0
  459. package/modules/documents/translations/en.json +111 -0
  460. package/modules/documents/translations/pl.json +111 -0
  461. package/modules/exports/migrations/0001_exports_core.down.sql +2 -0
  462. package/modules/exports/migrations/0001_exports_core.up.sql +28 -0
  463. package/modules/exports/migrations/0002_exports_job_routing_role.down.sql +4 -0
  464. package/modules/exports/migrations/0002_exports_job_routing_role.up.sql +21 -0
  465. package/modules/exports/migrations/README.md +3 -0
  466. package/modules/exports/module.json +28 -0
  467. package/modules/exports/package.json +44 -0
  468. package/modules/exports/spec/module.yaml +229 -0
  469. package/modules/exports/src/acl/permissions.ts +6 -0
  470. package/modules/exports/src/api/endpoints.ts +211 -0
  471. package/modules/exports/src/client/ExportsView.tsrx +327 -0
  472. package/modules/exports/src/client/api.ts +133 -0
  473. package/modules/exports/src/client/contribution.tsrx +31 -0
  474. package/modules/exports/src/client/index.ts +20 -0
  475. package/modules/exports/src/client/navigation.ts +25 -0
  476. package/modules/exports/src/client/presentation.ts +53 -0
  477. package/modules/exports/src/client/state.ts +56 -0
  478. package/modules/exports/src/domain/lists.ts +29 -0
  479. package/modules/exports/src/domain/types.ts +120 -0
  480. package/modules/exports/src/index.ts +41 -0
  481. package/modules/exports/src/platform.ts +80 -0
  482. package/modules/exports/src/server/index.ts +51 -0
  483. package/modules/exports/src/server/runtime.ts +160 -0
  484. package/modules/exports/src/services/data-classes.ts +56 -0
  485. package/modules/exports/src/services/database-repository.ts +405 -0
  486. package/modules/exports/src/services/export-runner.ts +123 -0
  487. package/modules/exports/src/services/export-service.ts +356 -0
  488. package/modules/exports/src/services/index.ts +6 -0
  489. package/modules/exports/src/services/list-registry.ts +105 -0
  490. package/modules/exports/src/services/migration.ts +89 -0
  491. package/modules/exports/src/services/repository.ts +97 -0
  492. package/modules/exports/src/settings.ts +70 -0
  493. package/modules/exports/translations/en.json +69 -0
  494. package/modules/exports/translations/pl.json +69 -0
  495. package/modules/import/migrations/0001_import_core.down.sql +6 -0
  496. package/modules/import/migrations/0001_import_core.up.sql +61 -0
  497. package/modules/import/migrations/0002_import_job_routing_role.down.sql +4 -0
  498. package/modules/import/migrations/0002_import_job_routing_role.up.sql +20 -0
  499. package/modules/import/migrations/0003_import_jobs_export_index.down.sql +1 -0
  500. package/modules/import/migrations/0003_import_jobs_export_index.up.sql +7 -0
  501. package/modules/import/migrations/0004_import_jobs_traceparent.down.sql +1 -0
  502. package/modules/import/migrations/0004_import_jobs_traceparent.up.sql +7 -0
  503. package/modules/import/migrations/README.md +3 -0
  504. package/modules/import/module.json +37 -0
  505. package/modules/import/package.json +50 -0
  506. package/modules/import/spec/module.yaml +335 -0
  507. package/modules/import/src/acl/permissions.ts +6 -0
  508. package/modules/import/src/api/endpoints.ts +400 -0
  509. package/modules/import/src/client/ImportJobDrawer.tsrx +229 -0
  510. package/modules/import/src/client/ImportsView.tsrx +455 -0
  511. package/modules/import/src/client/NewImportForm.tsrx +274 -0
  512. package/modules/import/src/client/api.ts +243 -0
  513. package/modules/import/src/client/contribution.tsrx +29 -0
  514. package/modules/import/src/client/index.ts +23 -0
  515. package/modules/import/src/client/navigation.ts +25 -0
  516. package/modules/import/src/client/presentation.ts +59 -0
  517. package/modules/import/src/client/state.ts +43 -0
  518. package/modules/import/src/domain/csv.ts +258 -0
  519. package/modules/import/src/domain/ports.ts +157 -0
  520. package/modules/import/src/domain/types.ts +179 -0
  521. package/modules/import/src/index.ts +58 -0
  522. package/modules/import/src/platform.ts +60 -0
  523. package/modules/import/src/server/index.ts +49 -0
  524. package/modules/import/src/server/runtime.ts +158 -0
  525. package/modules/import/src/services/csv-source.ts +121 -0
  526. package/modules/import/src/services/data-classes.ts +63 -0
  527. package/modules/import/src/services/database-repository.ts +780 -0
  528. package/modules/import/src/services/import-runner.ts +127 -0
  529. package/modules/import/src/services/import-service.ts +872 -0
  530. package/modules/import/src/services/index.ts +6 -0
  531. package/modules/import/src/services/migration.ts +156 -0
  532. package/modules/import/src/services/port-registry.ts +158 -0
  533. package/modules/import/src/services/repository.ts +147 -0
  534. package/modules/import/src/settings.ts +70 -0
  535. package/modules/import/translations/en.json +175 -0
  536. package/modules/import/translations/pl.json +175 -0
  537. package/modules/metering/migrations/0001_metering_core.down.sql +6 -0
  538. package/modules/metering/migrations/0001_metering_core.up.sql +119 -0
  539. package/modules/metering/migrations/0002_metering_bucket_export.down.sql +3 -0
  540. package/modules/metering/migrations/0002_metering_bucket_export.up.sql +5 -0
  541. package/modules/metering/migrations/README.md +3 -0
  542. package/modules/metering/module.json +46 -0
  543. package/modules/metering/package.json +50 -0
  544. package/modules/metering/spec/module.yaml +269 -0
  545. package/modules/metering/src/acl/permissions.ts +5 -0
  546. package/modules/metering/src/api/endpoints.ts +104 -0
  547. package/modules/metering/src/cli/commands.json +28 -0
  548. package/modules/metering/src/cli/index.ts +246 -0
  549. package/modules/metering/src/client/LimitsView.tsrx +170 -0
  550. package/modules/metering/src/client/UsageSummaryWidget.tsrx +63 -0
  551. package/modules/metering/src/client/UsageView.tsrx +205 -0
  552. package/modules/metering/src/client/api.ts +91 -0
  553. package/modules/metering/src/client/contribution.tsrx +42 -0
  554. package/modules/metering/src/client/index.ts +26 -0
  555. package/modules/metering/src/client/navigation.ts +54 -0
  556. package/modules/metering/src/client/presentation.ts +63 -0
  557. package/modules/metering/src/client/state.ts +31 -0
  558. package/modules/metering/src/domain/meters.ts +100 -0
  559. package/modules/metering/src/domain/types.ts +71 -0
  560. package/modules/metering/src/index.ts +51 -0
  561. package/modules/metering/src/platform.ts +84 -0
  562. package/modules/metering/src/server/index.ts +40 -0
  563. package/modules/metering/src/server/runtime.ts +165 -0
  564. package/modules/metering/src/services/data-classes.ts +34 -0
  565. package/modules/metering/src/services/database-repository.ts +574 -0
  566. package/modules/metering/src/services/index.ts +9 -0
  567. package/modules/metering/src/services/meter-registry.ts +145 -0
  568. package/modules/metering/src/services/metering-service.ts +441 -0
  569. package/modules/metering/src/services/migration.ts +183 -0
  570. package/modules/metering/src/services/notifications.ts +75 -0
  571. package/modules/metering/src/services/reports.ts +105 -0
  572. package/modules/metering/src/services/repository.ts +132 -0
  573. package/modules/metering/src/services/service-error.ts +46 -0
  574. package/modules/metering/src/settings.ts +47 -0
  575. package/modules/metering/translations/en.json +68 -0
  576. package/modules/metering/translations/pl.json +68 -0
  577. package/modules/notifications/migrations/0001_notifications_core.down.sql +1 -0
  578. package/modules/notifications/migrations/0001_notifications_core.up.sql +26 -0
  579. package/modules/notifications/migrations/0002_notifications_preferences.down.sql +1 -0
  580. package/modules/notifications/migrations/0002_notifications_preferences.up.sql +17 -0
  581. package/modules/notifications/migrations/0003_notifications_webhook_subscriptions.down.sql +1 -0
  582. package/modules/notifications/migrations/0003_notifications_webhook_subscriptions.up.sql +30 -0
  583. package/modules/notifications/migrations/0004_notifications_deliveries.down.sql +1 -0
  584. package/modules/notifications/migrations/0004_notifications_deliveries.up.sql +35 -0
  585. package/modules/notifications/migrations/0005_notifications_delivery_routing_role.down.sql +2 -0
  586. package/modules/notifications/migrations/0005_notifications_delivery_routing_role.up.sql +17 -0
  587. package/modules/notifications/migrations/0006_notifications_delivery_title.down.sql +1 -0
  588. package/modules/notifications/migrations/0006_notifications_delivery_title.up.sql +11 -0
  589. package/modules/notifications/migrations/0007_notifications_secret_rotation_inventory.down.sql +2 -0
  590. package/modules/notifications/migrations/0007_notifications_secret_rotation_inventory.up.sql +19 -0
  591. package/modules/notifications/migrations/0008_notifications_delivery_claim.down.sql +8 -0
  592. package/modules/notifications/migrations/0008_notifications_delivery_claim.up.sql +19 -0
  593. package/modules/notifications/migrations/0009_notifications_kind_approvals.down.sql +16 -0
  594. package/modules/notifications/migrations/0009_notifications_kind_approvals.up.sql +18 -0
  595. package/modules/notifications/migrations/0010_notifications_kind_meter_threshold.down.sql +16 -0
  596. package/modules/notifications/migrations/0010_notifications_kind_meter_threshold.up.sql +18 -0
  597. package/modules/notifications/migrations/0011_notifications_retention_keyset.down.sql +5 -0
  598. package/modules/notifications/migrations/0011_notifications_retention_keyset.up.sql +15 -0
  599. package/modules/notifications/migrations/0012_notifications_email_channel.down.sql +12 -0
  600. package/modules/notifications/migrations/0012_notifications_email_channel.up.sql +38 -0
  601. package/modules/notifications/migrations/0013_notifications_list_pages.down.sql +5 -0
  602. package/modules/notifications/migrations/0013_notifications_list_pages.up.sql +13 -0
  603. package/modules/notifications/migrations/README.md +3 -0
  604. package/modules/notifications/module.json +32 -0
  605. package/modules/notifications/package.json +49 -0
  606. package/modules/notifications/spec/module.yaml +718 -0
  607. package/modules/notifications/src/acl/permissions.ts +11 -0
  608. package/modules/notifications/src/api/endpoints.ts +814 -0
  609. package/modules/notifications/src/cli/commands.json +17 -0
  610. package/modules/notifications/src/cli/index.ts +126 -0
  611. package/modules/notifications/src/client/DeliveriesView.tsrx +483 -0
  612. package/modules/notifications/src/client/FactList.tsrx +24 -0
  613. package/modules/notifications/src/client/NotificationPreferencesView.tsrx +240 -0
  614. package/modules/notifications/src/client/NotificationsInboxView.tsrx +592 -0
  615. package/modules/notifications/src/client/UnreadCountWidget.tsrx +62 -0
  616. package/modules/notifications/src/client/WebhookForm.tsrx +297 -0
  617. package/modules/notifications/src/client/WebhooksView.tsrx +637 -0
  618. package/modules/notifications/src/client/api.ts +382 -0
  619. package/modules/notifications/src/client/contribution.tsrx +83 -0
  620. package/modules/notifications/src/client/inbox.ts +41 -0
  621. package/modules/notifications/src/client/index.ts +29 -0
  622. package/modules/notifications/src/client/navigation.ts +92 -0
  623. package/modules/notifications/src/client/notifications.css +70 -0
  624. package/modules/notifications/src/client/presentation.ts +81 -0
  625. package/modules/notifications/src/client/state.ts +246 -0
  626. package/modules/notifications/src/client/unread-count.ts +26 -0
  627. package/modules/notifications/src/domain/locale.ts +47 -0
  628. package/modules/notifications/src/domain/publish.ts +46 -0
  629. package/modules/notifications/src/domain/types.ts +186 -0
  630. package/modules/notifications/src/index.ts +62 -0
  631. package/modules/notifications/src/platform.ts +85 -0
  632. package/modules/notifications/src/server/index.ts +82 -0
  633. package/modules/notifications/src/server/runtime.ts +251 -0
  634. package/modules/notifications/src/services/data-classes.ts +139 -0
  635. package/modules/notifications/src/services/database-repository.ts +1334 -0
  636. package/modules/notifications/src/services/delivery-payload.ts +93 -0
  637. package/modules/notifications/src/services/delivery-runner.ts +170 -0
  638. package/modules/notifications/src/services/delivery-service.ts +811 -0
  639. package/modules/notifications/src/services/egress.ts +239 -0
  640. package/modules/notifications/src/services/email-channel.ts +93 -0
  641. package/modules/notifications/src/services/index.ts +23 -0
  642. package/modules/notifications/src/services/migration.ts +572 -0
  643. package/modules/notifications/src/services/notifications-service.ts +297 -0
  644. package/modules/notifications/src/services/paging.ts +60 -0
  645. package/modules/notifications/src/services/publish-service.ts +133 -0
  646. package/modules/notifications/src/services/repository.ts +342 -0
  647. package/modules/notifications/src/services/secret-rotation.ts +177 -0
  648. package/modules/notifications/src/services/secret-vault.ts +168 -0
  649. package/modules/notifications/src/services/service-error.ts +67 -0
  650. package/modules/notifications/src/services/signature.ts +81 -0
  651. package/modules/notifications/src/services/webhook-service.ts +307 -0
  652. package/modules/notifications/src/settings.ts +130 -0
  653. package/modules/notifications/translations/en.json +255 -0
  654. package/modules/notifications/translations/pl.json +255 -0
  655. package/modules/profile/module.json +3 -3
  656. package/modules/profile/package.json +2 -2
  657. package/modules/profile/spec/module.yaml +3 -3
  658. package/modules/profile/src/services/database-repository.ts +3 -13
  659. package/modules/reports/module.json +28 -0
  660. package/modules/reports/package.json +42 -0
  661. package/modules/reports/spec/module.yaml +231 -0
  662. package/modules/reports/src/acl/permissions.ts +5 -0
  663. package/modules/reports/src/api/endpoints.ts +72 -0
  664. package/modules/reports/src/client/ReportsSummaryWidget.tsrx +52 -0
  665. package/modules/reports/src/client/ReportsView.tsrx +246 -0
  666. package/modules/reports/src/client/api.ts +65 -0
  667. package/modules/reports/src/client/contribution.tsrx +36 -0
  668. package/modules/reports/src/client/index.ts +18 -0
  669. package/modules/reports/src/client/navigation.ts +39 -0
  670. package/modules/reports/src/client/presentation.ts +123 -0
  671. package/modules/reports/src/client/state.ts +28 -0
  672. package/modules/reports/src/domain/providers.ts +115 -0
  673. package/modules/reports/src/domain/types.ts +44 -0
  674. package/modules/reports/src/index.ts +47 -0
  675. package/modules/reports/src/platform.ts +37 -0
  676. package/modules/reports/src/server/index.ts +21 -0
  677. package/modules/reports/src/server/runtime.ts +40 -0
  678. package/modules/reports/src/services/index.ts +17 -0
  679. package/modules/reports/src/services/provider-registry.ts +144 -0
  680. package/modules/reports/src/services/range.ts +65 -0
  681. package/modules/reports/src/services/reports-service.ts +316 -0
  682. package/modules/reports/src/services/service-error.ts +26 -0
  683. package/modules/reports/src/settings.ts +49 -0
  684. package/modules/reports/translations/en.json +35 -0
  685. package/modules/reports/translations/pl.json +35 -0
  686. package/modules/sandbox/module.json +4 -4
  687. package/modules/sandbox/package.json +2 -2
  688. package/modules/sandbox/spec/module.yaml +3 -3
  689. package/modules/sandbox/src/services/database-repository.ts +14 -23
  690. package/modules/sandbox/src/services/directory.ts +16 -0
  691. package/modules/sandbox/src/services/sandbox-service.ts +13 -11
  692. package/modules/search/migrations/0001_search_core.down.sql +1 -0
  693. package/modules/search/migrations/0001_search_core.up.sql +22 -0
  694. package/modules/search/migrations/0002_search_recent_export.down.sql +3 -0
  695. package/modules/search/migrations/0002_search_recent_export.up.sql +5 -0
  696. package/modules/search/migrations/README.md +3 -0
  697. package/modules/search/module.json +28 -0
  698. package/modules/search/package.json +43 -0
  699. package/modules/search/spec/module.yaml +177 -0
  700. package/modules/search/src/acl/permissions.ts +5 -0
  701. package/modules/search/src/api/endpoints.ts +221 -0
  702. package/modules/search/src/client/SearchView.tsrx +346 -0
  703. package/modules/search/src/client/api.ts +157 -0
  704. package/modules/search/src/client/contribution.tsrx +31 -0
  705. package/modules/search/src/client/index.ts +17 -0
  706. package/modules/search/src/client/navigation.ts +97 -0
  707. package/modules/search/src/client/search.css +56 -0
  708. package/modules/search/src/client/state.ts +113 -0
  709. package/modules/search/src/domain/data-classes.ts +73 -0
  710. package/modules/search/src/domain/providers.ts +88 -0
  711. package/modules/search/src/domain/types.ts +56 -0
  712. package/modules/search/src/index.ts +46 -0
  713. package/modules/search/src/platform.ts +42 -0
  714. package/modules/search/src/server/index.ts +25 -0
  715. package/modules/search/src/server/runtime.ts +123 -0
  716. package/modules/search/src/services/database-repository.ts +163 -0
  717. package/modules/search/src/services/index.ts +21 -0
  718. package/modules/search/src/services/migration.ts +65 -0
  719. package/modules/search/src/services/provider-registry.ts +137 -0
  720. package/modules/search/src/services/repository.ts +43 -0
  721. package/modules/search/src/services/search-service.ts +442 -0
  722. package/modules/search/src/services/service-error.ts +35 -0
  723. package/modules/search/src/settings.ts +68 -0
  724. package/modules/search/translations/en.json +45 -0
  725. package/modules/search/translations/pl.json +45 -0
  726. package/modules/system/module.json +2 -2
  727. package/modules/system/package.json +2 -2
  728. package/modules/system/spec/module.yaml +30 -3
  729. package/modules/system/src/client/FlagsPanel.tsrx +127 -0
  730. package/modules/system/src/client/ModuleSettingRow.tsrx +17 -7
  731. package/modules/system/src/client/ModuleSettingsSection.tsrx +10 -71
  732. package/modules/system/src/client/ModulesView.tsrx +91 -65
  733. package/modules/system/src/client/flags.ts +35 -0
  734. package/modules/system/src/client/index.ts +8 -0
  735. package/modules/system/src/client/navigation.ts +8 -0
  736. package/modules/system/src/client/settings-state.ts +159 -0
  737. package/modules/system/src/client/state.ts +2 -0
  738. package/modules/system/src/domain/time-zone.ts +105 -0
  739. package/modules/system/src/index.ts +12 -0
  740. package/modules/system/src/platform.ts +5 -1
  741. package/modules/system/src/server/endpoints.ts +72 -21
  742. package/modules/system/src/settings.ts +28 -0
  743. package/modules/system/translations/en.json +17 -0
  744. package/modules/system/translations/pl.json +17 -0
  745. package/modules/users/module.json +30 -4
  746. package/modules/users/package.json +5 -2
  747. package/modules/users/spec/module.yaml +290 -7
  748. package/modules/users/src/api/endpoints.ts +228 -15
  749. package/modules/users/src/client/InviteMemberForm.tsrx +2 -2
  750. package/modules/users/src/client/MemberDrawer.tsrx +142 -26
  751. package/modules/users/src/client/UserAccountForm.tsrx +2 -2
  752. package/modules/users/src/client/UsersView.tsrx +432 -71
  753. package/modules/users/src/client/api.ts +106 -12
  754. package/modules/users/src/client/contribution.tsrx +5 -0
  755. package/modules/users/src/client/member-columns.tsrx +22 -9
  756. package/modules/users/src/client/state.ts +172 -7
  757. package/modules/users/src/domain/lists.ts +9 -0
  758. package/modules/users/src/index.ts +11 -0
  759. package/modules/users/src/platform.ts +50 -1
  760. package/modules/users/src/services/member-export.ts +51 -0
  761. package/modules/users/src/services/member-import.ts +249 -0
  762. package/modules/users/src/services/member-search.ts +145 -0
  763. package/modules/users/src/services/users-service.ts +117 -4
  764. package/modules/users/translations/en.json +57 -8
  765. package/modules/users/translations/pl.json +57 -8
  766. package/modules/workflows/migrations/0004_payload_rotation_inventory.up.sql +17 -0
  767. package/modules/workflows/migrations/0005_workflows_human_approval.up.sql +28 -0
  768. package/modules/workflows/migrations/0006_workflows_retention_indexes.up.sql +11 -0
  769. package/modules/workflows/migrations/0007_workflows_erasure_subject_indexes.up.sql +17 -0
  770. package/modules/workflows/migrations/0008_workflows_subject_account.up.sql +24 -0
  771. package/modules/workflows/migrations/0009_workflows_definition_list_indexes.up.sql +9 -0
  772. package/modules/workflows/module.json +34 -6
  773. package/modules/workflows/package.json +3 -2
  774. package/modules/workflows/spec/module.yaml +26 -9
  775. package/modules/workflows/src/api/endpoints.ts +131 -25
  776. package/modules/workflows/src/cli/commands.json +17 -0
  777. package/modules/workflows/src/cli/index.ts +114 -0
  778. package/modules/workflows/src/client/WorkflowExecutionHistory.tsrx +36 -27
  779. package/modules/workflows/src/client/WorkflowInspector.tsrx +116 -0
  780. package/modules/workflows/src/client/WorkflowNodePalette.tsrx +1 -0
  781. package/modules/workflows/src/client/WorkflowsView.tsrx +236 -99
  782. package/modules/workflows/src/client/api.ts +54 -34
  783. package/modules/workflows/src/client/canvas-model.ts +12 -0
  784. package/modules/workflows/src/client/state.ts +95 -0
  785. package/modules/workflows/src/domain/events.ts +25 -11
  786. package/modules/workflows/src/domain/graph.ts +135 -1
  787. package/modules/workflows/src/domain/types.ts +91 -4
  788. package/modules/workflows/src/platform.ts +12 -0
  789. package/modules/workflows/src/server/runtime.ts +78 -5
  790. package/modules/workflows/src/services/approvals.ts +62 -0
  791. package/modules/workflows/src/services/cursors.ts +37 -0
  792. package/modules/workflows/src/services/data-classes.ts +232 -0
  793. package/modules/workflows/src/services/database-repository.ts +660 -171
  794. package/modules/workflows/src/services/migration.ts +240 -0
  795. package/modules/workflows/src/services/notifications.ts +98 -0
  796. package/modules/workflows/src/services/payload-codec.ts +51 -28
  797. package/modules/workflows/src/services/payload-rotation.ts +171 -0
  798. package/modules/workflows/src/services/repository.ts +123 -8
  799. package/modules/workflows/src/services/simulation.ts +7 -0
  800. package/modules/workflows/src/services/worker.ts +446 -25
  801. package/modules/workflows/src/services/workflows-service.ts +264 -103
  802. package/modules/workflows/translations/en.json +12 -4
  803. package/modules/workflows/translations/pl.json +12 -4
  804. package/modules.json +48 -4
  805. package/package.json +66 -7
  806. package/packages/client/package.json +2 -1
  807. package/packages/client/src/ApplicationShell.tsrx +43 -0
  808. package/packages/client/src/contributions.ts +55 -0
  809. package/packages/client/src/i18n/locales/en.json +5 -0
  810. package/packages/client/src/i18n/locales/pl.json +5 -0
  811. package/packages/client/src/index.ts +13 -1
  812. package/packages/client/src/routing.ts +22 -0
  813. package/packages/client/src/shell/CommandPalette.tsrx +223 -11
  814. package/packages/client/src/shell/command-search.ts +126 -0
  815. package/packages/client/src/shell/command.ts +30 -0
  816. package/packages/client/src/shell/navigation.ts +12 -0
  817. package/packages/client/src/shell/palette-keys.ts +27 -0
  818. package/packages/client/src/shell/shell.css +7 -0
  819. package/packages/contracts/schemas/module-spec.schema.json +287 -1
  820. package/packages/contracts/schemas/module.schema.json +25 -0
  821. package/packages/contracts/src/index.ts +144 -1
  822. package/packages/database/src/backup.ts +179 -0
  823. package/packages/database/src/decoders.ts +25 -0
  824. package/packages/database/src/index.ts +17 -0
  825. package/packages/database/src/record-history.ts +5 -14
  826. package/packages/dev-console/package.json +2 -1
  827. package/packages/dev-console/src/brand.d.mts +8 -0
  828. package/packages/dev-console/src/brand.mjs +33 -0
  829. package/packages/dev-console/src/index.mjs +2 -1
  830. package/packages/harness/src/index.ts +2 -0
  831. package/packages/harness/src/runtime.ts +218 -1
  832. package/packages/harness/src/tool-adapters.ts +19 -1
  833. package/packages/kernel/src/acl.ts +179 -0
  834. package/packages/kernel/src/capability-registry.ts +69 -2
  835. package/packages/kernel/src/data-class-registry.ts +354 -0
  836. package/packages/kernel/src/index.ts +65 -4
  837. package/packages/kernel/src/keyring.ts +208 -0
  838. package/packages/kernel/src/module-compatibility.ts +35 -4
  839. package/packages/kernel/src/module-registry.ts +49 -2
  840. package/packages/kernel/src/module-settings.ts +189 -27
  841. package/packages/server/package.json +2 -1
  842. package/packages/server/src/composition.ts +38 -0
  843. package/packages/server/src/endpoint.ts +104 -47
  844. package/packages/server/src/export/csv.ts +34 -0
  845. package/packages/server/src/export/definition.ts +346 -0
  846. package/packages/server/src/export/index.ts +61 -0
  847. package/packages/server/src/export/run.ts +116 -0
  848. package/packages/server/src/index.ts +155 -0
  849. package/packages/server/src/jobs/index.ts +93 -0
  850. package/packages/server/src/jobs/runner.ts +486 -0
  851. package/packages/server/src/log.ts +291 -0
  852. package/packages/server/src/mail/config.ts +184 -0
  853. package/packages/server/src/mail/contracts.ts +251 -0
  854. package/packages/server/src/mail/index.ts +31 -0
  855. package/packages/server/src/mail/port.ts +101 -0
  856. package/packages/server/src/mail/smtp.ts +191 -0
  857. package/packages/server/src/mail/template.ts +105 -0
  858. package/packages/server/src/metrics.ts +447 -0
  859. package/packages/server/src/pagination.ts +246 -0
  860. package/packages/server/src/trace/context.ts +103 -0
  861. package/packages/server/src/trace/egress.ts +66 -0
  862. package/packages/server/src/trace/error-sink.ts +296 -0
  863. package/packages/server/src/trace/exporter.ts +295 -0
  864. package/packages/server/src/trace/index.ts +60 -0
  865. package/packages/server/src/trace/job-sink.ts +172 -0
  866. package/packages/server/src/trace/tracer.ts +338 -0
  867. package/packages/server/src/web.ts +5 -1
  868. package/packages/storage/package.json +40 -0
  869. package/packages/storage/src/config.ts +189 -0
  870. package/packages/storage/src/content-type.ts +124 -0
  871. package/packages/storage/src/contracts.ts +112 -0
  872. package/packages/storage/src/envelope.ts +189 -0
  873. package/packages/storage/src/index.ts +43 -0
  874. package/packages/storage/src/local.ts +75 -0
  875. package/packages/storage/src/port.ts +251 -0
  876. package/packages/storage/src/read-token.ts +130 -0
  877. package/packages/storage/src/s3.ts +144 -0
  878. package/packages/storage/src/scanner.ts +22 -0
  879. package/packages/storage/src/sigv4.ts +104 -0
  880. package/packages/storage/src/store.ts +13 -0
  881. package/packages/ui/package.json +1 -0
  882. package/packages/ui/src/components/Button.tsrx +3 -0
  883. package/packages/ui/src/components/ConfirmDialog.tsrx +13 -2
  884. package/packages/ui/src/components/DateField.tsrx +92 -0
  885. package/packages/ui/src/components/DatePicker.tsrx +525 -0
  886. package/packages/ui/src/components/DateRangeField.tsrx +93 -0
  887. package/packages/ui/src/components/Drawer.tsrx +10 -1
  888. package/packages/ui/src/components/FileUpload.tsrx +158 -0
  889. package/packages/ui/src/components/Pagination.tsrx +130 -0
  890. package/packages/ui/src/components/Select.tsrx +78 -0
  891. package/packages/ui/src/components/Table.tsrx +498 -81
  892. package/packages/ui/src/components/Tabs.tsrx +53 -0
  893. package/packages/ui/src/components/ToastHost.tsrx +48 -0
  894. package/packages/ui/src/components/calendar.ts +243 -0
  895. package/packages/ui/src/components/date-field.ts +62 -0
  896. package/packages/ui/src/components/file-upload.ts +49 -0
  897. package/packages/ui/src/components/focus-trap.ts +62 -0
  898. package/packages/ui/src/components/pagination.ts +68 -0
  899. package/packages/ui/src/components/table-sorting.ts +49 -0
  900. package/packages/ui/src/components/tabs.ts +61 -0
  901. package/packages/ui/src/components/toast-store.ts +116 -0
  902. package/packages/ui/src/icons/Icon.tsrx +5 -0
  903. package/packages/ui/src/index.ts +45 -0
  904. package/packages/ui/src/styles/components.css +598 -0
  905. package/modules/automations-workflows-integration/module.json +0 -27
  906. package/modules/automations-workflows-integration/spec/module.yaml +0 -93
  907. package/modules/automations-workflows-integration/src/index.ts +0 -14
  908. package/modules/automations-workflows-integration/src/platform.ts +0 -24
  909. package/modules/automations-workflows-integration/src/server/index.ts +0 -11
  910. package/modules/automations-workflows-integration/translations/en.json +0 -21
  911. package/modules/automations-workflows-integration/translations/pl.json +0 -21
  912. package/modules/workflows/src/services/cursor-codec.ts +0 -62
@@ -0,0 +1,1420 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import type { TenantMember } from '@flowdular/sdk/modules/auth';
3
+ import { AuthServiceError } from '@flowdular/sdk/modules/auth/server';
4
+ import { serverLogger } from '@flowdular/sdk/server';
5
+ import {
6
+ listResponse,
7
+ parseFilter,
8
+ parsePage,
9
+ parsePatch,
10
+ ScimError,
11
+ scimMeta,
12
+ SCIM_SCHEMAS,
13
+ type ScimPatchOperation,
14
+ } from '../domain/scim.ts';
15
+ import {
16
+ DIRECTORY_REASONS,
17
+ type ProvisioningOperation,
18
+ type ProvisioningOutcome,
19
+ type ScimGroupMapping,
20
+ type ScimToken,
21
+ type ScimUserMapping,
22
+ } from '../domain/types.ts';
23
+ import { scimActor, type DirectoryAuthPort } from './auth-port.ts';
24
+ import { DirectoryUniqueViolation } from './database-repository.ts';
25
+ import type {
26
+ DirectoryRepository,
27
+ GroupMemberRow,
28
+ GroupMemberStep,
29
+ } from './repository.ts';
30
+
31
+ const USER_FILTER_ATTRIBUTES = ['userName', 'externalId', 'id'] as const;
32
+ const GROUP_FILTER_ATTRIBUTES = ['displayName', 'externalId', 'id'] as const;
33
+ const MAX_USER_NAME = 320;
34
+ const MAX_EXTERNAL_ID = 256;
35
+ const MAX_DISPLAY_NAME = 200;
36
+ /** Members one group write may carry; the SCIM body ceiling is derived from it. */
37
+ export const MAX_GROUP_MEMBERS = 1_000;
38
+ /** New groups start unmapped and in the middle of the range an owner can use. */
39
+ const DEFAULT_GROUP_PRECEDENCE = 100;
40
+ /**
41
+ * How many members one group carries in a response. A group is unbounded in
42
+ * storage, so without this a single listing could load a whole workspace's
43
+ * membership into one body. A larger group is truncated by userName; the
44
+ * complete list belongs to a paged member query this version does not offer.
45
+ */
46
+ const MAX_EMBEDDED_MEMBERS = 1_000;
47
+ const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
48
+
49
+ /** Everything one authenticated SCIM request acts within. */
50
+ export interface ScimRequestContext {
51
+ readonly tenantId: string;
52
+ readonly token: ScimToken;
53
+ /** Absolute base of this workspace's SCIM surface, for `meta.location`. */
54
+ readonly baseUrl: string;
55
+ readonly pageSizeMax: number;
56
+ readonly defaultRole: string;
57
+ }
58
+
59
+ function invalid(reason: string, detail: string): ScimError {
60
+ return new ScimError(400, 'invalidValue', reason, detail);
61
+ }
62
+
63
+ function notFound(reason: string, detail: string): ScimError {
64
+ return new ScimError(404, null, reason, detail);
65
+ }
66
+
67
+ /**
68
+ * auth.core refusals carry the decision this module has to report; mapping them
69
+ * here keeps one translation instead of one per call site.
70
+ */
71
+ function fromAuthError(error: unknown): ScimError | null {
72
+ if (!(error instanceof AuthServiceError)) return null;
73
+ switch (error.code) {
74
+ case 'LAST_OWNER':
75
+ return new ScimError(
76
+ 400,
77
+ 'mutability',
78
+ DIRECTORY_REASONS.lastOwner,
79
+ 'A workspace must keep at least one active owner; change it in the administration screen.',
80
+ );
81
+ case 'ACCOUNT_EXISTS':
82
+ return new ScimError(
83
+ 409,
84
+ 'uniqueness',
85
+ DIRECTORY_REASONS.accountInUse,
86
+ 'The address already belongs to an account that holds another membership.',
87
+ );
88
+ case 'ROLE_NOT_FOUND':
89
+ return invalid(
90
+ DIRECTORY_REASONS.roleUnknown,
91
+ 'The mapped role does not exist in this workspace.',
92
+ );
93
+ case 'ACCOUNT_NOT_FOUND':
94
+ return notFound(
95
+ DIRECTORY_REASONS.memberNotFound,
96
+ 'The mapped account is no longer a member of this workspace.',
97
+ );
98
+ default:
99
+ /* Anything else auth.core refuses is still the caller's request, not a
100
+ fault: answering 500 would tell a provider to retry forever. A 5xx
101
+ from auth.core stays a fault and reaches the generic handler. */
102
+ return error.status >= 400 && error.status < 500
103
+ ? invalid(DIRECTORY_REASONS.invalidValue, error.message)
104
+ : null;
105
+ }
106
+ }
107
+
108
+ function text(
109
+ value: unknown,
110
+ field: string,
111
+ maximum: number,
112
+ reason = DIRECTORY_REASONS.invalidValue,
113
+ ): string {
114
+ if (typeof value !== 'string') {
115
+ throw invalid(reason, `${field} must be a string.`);
116
+ }
117
+ const normalized = value.trim();
118
+ if (normalized.length === 0 || normalized.length > maximum) {
119
+ throw invalid(reason, `${field} must contain 1 to ${maximum} characters.`);
120
+ }
121
+ return normalized;
122
+ }
123
+
124
+ function optionalText(
125
+ value: unknown,
126
+ field: string,
127
+ maximum: number,
128
+ ): string | null {
129
+ if (value === undefined || value === null || value === '') return null;
130
+ return text(value, field, maximum);
131
+ }
132
+
133
+ function booleanValue(value: unknown, field: string): boolean {
134
+ if (typeof value === 'boolean') return value;
135
+ /* Providers send active as the string "True" or "false" often enough that
136
+ refusing it would break provisioning for a spelling difference. */
137
+ if (typeof value === 'string') {
138
+ const folded = value.trim().toLowerCase();
139
+ if (folded === 'true') return true;
140
+ if (folded === 'false') return false;
141
+ }
142
+ throw invalid(DIRECTORY_REASONS.invalidValue, `${field} must be a boolean.`);
143
+ }
144
+
145
+ /** userName is the address auth.core keys the account on. */
146
+ function userName(value: unknown): string {
147
+ const normalized = text(value, 'userName', MAX_USER_NAME).toLowerCase();
148
+ if (!EMAIL_PATTERN.test(normalized)) {
149
+ throw invalid(
150
+ DIRECTORY_REASONS.invalidValue,
151
+ 'userName must be an e-mail address.',
152
+ );
153
+ }
154
+ return normalized;
155
+ }
156
+
157
+ function displayNameFrom(
158
+ body: Record<string, unknown>,
159
+ fallback: string,
160
+ ): string {
161
+ const direct = optionalText(body.displayName, 'displayName', 80);
162
+ if (direct) return direct;
163
+ const name = (body.name ?? {}) as Record<string, unknown>;
164
+ const formatted = optionalText(name.formatted, 'name.formatted', 80);
165
+ if (formatted) return formatted;
166
+ const given = optionalText(name.givenName, 'name.givenName', 40);
167
+ const family = optionalText(name.familyName, 'name.familyName', 40);
168
+ /* Two 40 character halves join past the 80 auth.core accepts. */
169
+ const joined = [given, family].filter(Boolean).join(' ').slice(0, 80).trim();
170
+ if (joined.length >= 2) return joined;
171
+ /* auth.core requires two characters; the address is the only other name a
172
+ provider always sends. */
173
+ const local = fallback.slice(0, fallback.indexOf('@'));
174
+ return local.length >= 2 ? local : fallback;
175
+ }
176
+
177
+ function immutableEmail(): ScimError {
178
+ return new ScimError(
179
+ 400,
180
+ 'mutability',
181
+ DIRECTORY_REASONS.immutableEmail,
182
+ 'The primary e-mail address cannot be changed through SCIM.',
183
+ );
184
+ }
185
+
186
+ /**
187
+ * The primary e-mail is the userName; auth.core exposes no address change. A
188
+ * provider may send the whole array or one scalar through a sub-attribute path
189
+ * such as `emails[type eq "work"].value`, and neither may move the address.
190
+ */
191
+ function assertEmailsMatch(value: unknown, expected: string): void {
192
+ if (value === undefined || value === null) return;
193
+ if (typeof value === 'string') {
194
+ if (value.trim().toLowerCase() !== expected) throw immutableEmail();
195
+ return;
196
+ }
197
+ if (!Array.isArray(value) || value.length > 16) {
198
+ throw invalid(DIRECTORY_REASONS.invalidValue, 'emails must be an array.');
199
+ }
200
+ for (const entry of value) {
201
+ const record = (entry ?? {}) as Record<string, unknown>;
202
+ if (record.primary !== true) continue;
203
+ const address = optionalText(record.value, 'emails.value', MAX_USER_NAME);
204
+ if (address !== null && address.toLowerCase() !== expected) {
205
+ throw immutableEmail();
206
+ }
207
+ }
208
+ }
209
+
210
+ const MEMBER_PATH_FILTER = /^members\[\s*value\s+eq\s+"([^"]{1,128})"\s*\]$/i;
211
+
212
+ /** `members[value eq "<id>"]`, the single-member form providers send. */
213
+ function memberPathFilter(path: string | null): string | null {
214
+ if (path === null) return null;
215
+ return MEMBER_PATH_FILTER.exec(path)?.[1] ?? null;
216
+ }
217
+
218
+ function memberIds(value: unknown): readonly string[] {
219
+ if (value === undefined || value === null) return [];
220
+ if (!Array.isArray(value) || value.length > MAX_GROUP_MEMBERS) {
221
+ throw invalid(
222
+ DIRECTORY_REASONS.invalidValue,
223
+ `members must be an array of at most ${MAX_GROUP_MEMBERS} entries.`,
224
+ );
225
+ }
226
+ return value.map((entry) => {
227
+ const record = (entry ?? {}) as Record<string, unknown>;
228
+ return text(record.value ?? entry, 'members.value', 128);
229
+ });
230
+ }
231
+
232
+ /**
233
+ * The workspace as one request sees it. Members and roles are read once and
234
+ * the overlay carries what this request itself changed, so a request that
235
+ * touches many users still costs one membership read.
236
+ */
237
+ class WorkspaceView {
238
+ #members: Promise<Map<string, TenantMember>> | undefined;
239
+ #roles: Promise<ReadonlySet<string>> | undefined;
240
+ readonly #roleOverlay = new Map<string, string>();
241
+ readonly #statusOverlay = new Map<string, 'active' | 'disabled'>();
242
+
243
+ constructor(
244
+ private readonly auth: DirectoryAuthPort,
245
+ private readonly tenantId: string,
246
+ ) {}
247
+
248
+ members(): Promise<Map<string, TenantMember>> {
249
+ return (this.#members ??= this.auth
250
+ .listMembers(this.tenantId)
251
+ .then(
252
+ (members) =>
253
+ new Map(members.map((member) => [member.accountId, member])),
254
+ ));
255
+ }
256
+
257
+ roles(): Promise<ReadonlySet<string>> {
258
+ return (this.#roles ??= this.auth
259
+ .listRoleKeys(this.tenantId)
260
+ .then((keys) => new Set(keys)));
261
+ }
262
+
263
+ async member(accountId: string): Promise<TenantMember | null> {
264
+ return (await this.members()).get(accountId) ?? null;
265
+ }
266
+
267
+ async roleOf(accountId: string): Promise<string | null> {
268
+ const overlaid = this.#roleOverlay.get(accountId);
269
+ if (overlaid !== undefined) return overlaid;
270
+ return (await this.member(accountId))?.role ?? null;
271
+ }
272
+
273
+ async statusOf(accountId: string): Promise<'active' | 'disabled' | null> {
274
+ const overlaid = this.#statusOverlay.get(accountId);
275
+ if (overlaid !== undefined) return overlaid;
276
+ return (await this.member(accountId))?.membershipStatus ?? null;
277
+ }
278
+
279
+ recordRole(accountId: string, roleKey: string): void {
280
+ this.#roleOverlay.set(accountId, roleKey);
281
+ }
282
+
283
+ recordStatus(accountId: string, status: 'active' | 'disabled'): void {
284
+ this.#statusOverlay.set(accountId, status);
285
+ }
286
+ }
287
+
288
+ interface UserPatch {
289
+ readonly externalId?: string | null | undefined;
290
+ readonly displayName?: string | undefined;
291
+ readonly active?: boolean | undefined;
292
+ }
293
+
294
+ interface ScimWrite {
295
+ readonly outcome: ProvisioningOutcome;
296
+ readonly resource: Record<string, unknown>;
297
+ /** Set when only the run can tell which operation actually happened. */
298
+ readonly operation?: ProvisioningOperation;
299
+ /** Set when the subject only exists once the run created it. */
300
+ readonly subject?: string;
301
+ }
302
+
303
+ export class ScimProvisioningService {
304
+ constructor(
305
+ private readonly repository: DirectoryRepository,
306
+ private readonly auth: DirectoryAuthPort,
307
+ private readonly now: () => number = Date.now,
308
+ ) {}
309
+
310
+ async listUsers(
311
+ context: ScimRequestContext,
312
+ url: URL,
313
+ ): Promise<Record<string, unknown>> {
314
+ const filter = parseFilter(
315
+ url.searchParams.get('filter'),
316
+ USER_FILTER_ATTRIBUTES,
317
+ );
318
+ const page = parsePage(url, context.pageSizeMax);
319
+ const slice = await this.repository.listUsers(
320
+ context.tenantId,
321
+ {
322
+ ...(filter?.attribute === 'userName'
323
+ ? { userName: filter.value.toLowerCase() }
324
+ : {}),
325
+ ...(filter?.attribute === 'externalId'
326
+ ? { externalId: filter.value }
327
+ : {}),
328
+ ...(filter?.attribute === 'id' ? { id: filter.value } : {}),
329
+ },
330
+ page,
331
+ );
332
+ const view = new WorkspaceView(this.auth, context.tenantId);
333
+ const resources: Record<string, unknown>[] = [];
334
+ for (const mapping of slice.records) {
335
+ resources.push(
336
+ this.#userResource(
337
+ context,
338
+ mapping,
339
+ await view.member(mapping.accountId),
340
+ ),
341
+ );
342
+ }
343
+ return listResponse({
344
+ totalResults: slice.totalResults,
345
+ startIndex: page.startIndex,
346
+ resources,
347
+ });
348
+ }
349
+
350
+ async getUser(
351
+ context: ScimRequestContext,
352
+ id: string,
353
+ ): Promise<Record<string, unknown>> {
354
+ const mapping = await this.#requireUser(context, id);
355
+ const view = new WorkspaceView(this.auth, context.tenantId);
356
+ return this.#userResource(
357
+ context,
358
+ mapping,
359
+ await view.member(mapping.accountId),
360
+ );
361
+ }
362
+
363
+ async createUser(
364
+ context: ScimRequestContext,
365
+ body: Record<string, unknown>,
366
+ ): Promise<Record<string, unknown>> {
367
+ const address = userName(body.userName);
368
+ return this.#recorded(context, 'user-create', address, async () => {
369
+ if (await this.repository.findUserByUserName(context.tenantId, address)) {
370
+ throw new ScimError(
371
+ 409,
372
+ 'uniqueness',
373
+ DIRECTORY_REASONS.userExists,
374
+ 'A SCIM user with this userName already exists in the workspace.',
375
+ );
376
+ }
377
+ /* Refused before the first write: a workspace nobody can sign in to
378
+ must not end up holding provisioned members. */
379
+ if (!(await this.auth.hasEnabledIdentityProvider(context.tenantId))) {
380
+ throw invalid(
381
+ DIRECTORY_REASONS.noEnabledProvider,
382
+ 'The workspace has no enabled identity provider, so a provisioned user could never sign in.',
383
+ );
384
+ }
385
+ const memberships = await this.auth.countAccountMemberships(address);
386
+ if (memberships !== null && memberships > 0) {
387
+ throw new ScimError(
388
+ 409,
389
+ 'uniqueness',
390
+ DIRECTORY_REASONS.accountInUse,
391
+ 'The address already belongs to an account that holds another membership.',
392
+ );
393
+ }
394
+ assertEmailsMatch(body.emails, address);
395
+ const view = new WorkspaceView(this.auth, context.tenantId);
396
+ const role = await this.#requireRole(view, context.defaultRole, true);
397
+ const member = await this.#throughAuth(() =>
398
+ this.auth.createMember(scimActor(context.tenantId, context.token.id), {
399
+ email: address,
400
+ displayName: displayNameFrom(body, address),
401
+ role,
402
+ }),
403
+ );
404
+ const createdAt = this.now();
405
+ const mapping: ScimUserMapping = {
406
+ id: randomUUID(),
407
+ tenantId: context.tenantId,
408
+ externalId: optionalText(
409
+ body.externalId,
410
+ 'externalId',
411
+ MAX_EXTERNAL_ID,
412
+ ),
413
+ userName: address,
414
+ accountId: member.accountId,
415
+ active: true,
416
+ createdAt,
417
+ lastSyncedAt: createdAt,
418
+ };
419
+ await this.#insertMapping(mapping);
420
+ return {
421
+ outcome: 'applied' as const,
422
+ resource: this.#userResource(context, mapping, member),
423
+ };
424
+ });
425
+ }
426
+
427
+ async replaceUser(
428
+ context: ScimRequestContext,
429
+ id: string,
430
+ body: Record<string, unknown>,
431
+ ): Promise<Record<string, unknown>> {
432
+ const mapping = await this.#requireUser(context, id);
433
+ return this.#writeUser(context, mapping, () => {
434
+ if (
435
+ body.userName !== undefined &&
436
+ userName(body.userName) !== mapping.userName
437
+ ) {
438
+ throw new ScimError(
439
+ 400,
440
+ 'mutability',
441
+ DIRECTORY_REASONS.immutableUserName,
442
+ 'userName cannot be changed through SCIM.',
443
+ );
444
+ }
445
+ assertEmailsMatch(body.emails, mapping.userName);
446
+ return {
447
+ externalId:
448
+ body.externalId === undefined
449
+ ? undefined
450
+ : optionalText(body.externalId, 'externalId', MAX_EXTERNAL_ID),
451
+ displayName:
452
+ body.displayName === undefined && body.name === undefined
453
+ ? undefined
454
+ : displayNameFrom(body, mapping.userName),
455
+ /* A replace without `active` keeps the current membership state: a
456
+ missing attribute must never silently reactivate a deprovisioned
457
+ member, which strict replace semantics would do. */
458
+ active:
459
+ body.active === undefined
460
+ ? undefined
461
+ : booleanValue(body.active, 'active'),
462
+ };
463
+ });
464
+ }
465
+
466
+ async patchUser(
467
+ context: ScimRequestContext,
468
+ id: string,
469
+ body: Record<string, unknown>,
470
+ ): Promise<Record<string, unknown>> {
471
+ const mapping = await this.#requireUser(context, id);
472
+ return this.#writeUser(context, mapping, () => {
473
+ const patch = parsePatch(body);
474
+ let externalId: string | null | undefined;
475
+ let displayName: string | undefined;
476
+ let active: boolean | undefined;
477
+ for (const operation of patch) {
478
+ for (const [attribute, value] of this.#userPatchTargets(operation)) {
479
+ if (attribute === 'active') {
480
+ active =
481
+ operation.op === 'remove' ? false : booleanValue(value, 'active');
482
+ } else if (attribute === 'displayName') {
483
+ displayName =
484
+ operation.op === 'remove'
485
+ ? mapping.userName
486
+ : displayNameFrom({ displayName: value }, mapping.userName);
487
+ } else if (attribute === 'name') {
488
+ displayName =
489
+ operation.op === 'remove'
490
+ ? mapping.userName
491
+ : displayNameFrom({ name: value }, mapping.userName);
492
+ } else if (attribute === 'externalId') {
493
+ externalId =
494
+ operation.op === 'remove'
495
+ ? null
496
+ : optionalText(value, 'externalId', MAX_EXTERNAL_ID);
497
+ } else if (attribute === 'emails') {
498
+ assertEmailsMatch(value, mapping.userName);
499
+ } else if (attribute === 'userName') {
500
+ /* A provider that echoes the current userName has changed
501
+ nothing, and a repeat of the current state must succeed. */
502
+ if (
503
+ operation.op === 'remove' ||
504
+ userName(value) !== mapping.userName
505
+ ) {
506
+ throw new ScimError(
507
+ 400,
508
+ 'mutability',
509
+ DIRECTORY_REASONS.immutableUserName,
510
+ 'userName cannot be changed through SCIM.',
511
+ );
512
+ }
513
+ }
514
+ }
515
+ }
516
+ return { externalId, displayName, active };
517
+ });
518
+ }
519
+
520
+ async deleteUser(context: ScimRequestContext, id: string): Promise<void> {
521
+ const mapping = await this.#requireUser(context, id);
522
+ /* DELETE deprovisions: the membership is disabled, the account and every
523
+ other workspace it belongs to stay untouched. */
524
+ await this.#writeUser(context, mapping, () => ({ active: false }));
525
+ }
526
+
527
+ /**
528
+ * Every attribute a PATCH operation targets, from either the path or the
529
+ * value object of a pathless operation.
530
+ */
531
+ #userPatchTargets(
532
+ operation: ScimPatchOperation,
533
+ ): readonly (readonly [string, unknown])[] {
534
+ if (operation.path === null) {
535
+ const value = (operation.value ?? {}) as Record<string, unknown>;
536
+ if (typeof operation.value !== 'object' || operation.value === null) {
537
+ throw invalid(
538
+ DIRECTORY_REASONS.invalidValue,
539
+ 'A patch without a path must carry an object value.',
540
+ );
541
+ }
542
+ return Object.entries(value).map(
543
+ ([key, entry]) => [this.#userAttribute(key), entry] as const,
544
+ );
545
+ }
546
+ return [[this.#userAttribute(operation.path), operation.value] as const];
547
+ }
548
+
549
+ #userAttribute(path: string): string {
550
+ const attribute = path.split('.')[0]!.split('[')[0]!.trim();
551
+ const known = [
552
+ 'active',
553
+ 'displayName',
554
+ 'name',
555
+ 'emails',
556
+ 'externalId',
557
+ 'userName',
558
+ ];
559
+ const matched = known.find(
560
+ (candidate) => candidate.toLowerCase() === attribute.toLowerCase(),
561
+ );
562
+ if (!matched) {
563
+ throw new ScimError(
564
+ 400,
565
+ 'noTarget',
566
+ DIRECTORY_REASONS.unsupportedPath,
567
+ `${attribute} is not an attribute this service provider maintains.`,
568
+ );
569
+ }
570
+ return matched;
571
+ }
572
+
573
+ async #writeUser(
574
+ context: ScimRequestContext,
575
+ mapping: ScimUserMapping,
576
+ resolve: () => UserPatch,
577
+ ): Promise<Record<string, unknown>> {
578
+ /* The patch is resolved inside the run so a refusal while reading the
579
+ body is logged with everything else this operation did. */
580
+ return this.#recorded(context, 'user-update', mapping.id, async () => {
581
+ const patch = resolve();
582
+ const view = new WorkspaceView(this.auth, context.tenantId);
583
+ const actor = scimActor(context.tenantId, context.token.id);
584
+ const member = await view.member(mapping.accountId);
585
+ if (!member) {
586
+ throw notFound(
587
+ DIRECTORY_REASONS.memberNotFound,
588
+ 'The mapped account is no longer a member of this workspace.',
589
+ );
590
+ }
591
+ let changed = false;
592
+ let statusMoved = false;
593
+ let displayName = member.displayName;
594
+ if (
595
+ patch.displayName !== undefined &&
596
+ patch.displayName !== member.displayName
597
+ ) {
598
+ await this.#throughAuth(() =>
599
+ this.auth.setDisplayName(actor, member.accountId, patch.displayName!),
600
+ );
601
+ displayName = patch.displayName;
602
+ changed = true;
603
+ }
604
+ if (patch.active !== undefined) {
605
+ const target = patch.active ? 'active' : 'disabled';
606
+ if ((await view.statusOf(member.accountId)) !== target) {
607
+ await this.#throughAuth(() =>
608
+ this.auth.setMembershipStatus(actor, member.accountId, target),
609
+ );
610
+ view.recordStatus(member.accountId, target);
611
+ statusMoved = true;
612
+ changed = true;
613
+ }
614
+ }
615
+ const active =
616
+ patch.active ?? (await view.statusOf(member.accountId)) === 'active';
617
+ if (
618
+ (patch.externalId !== undefined &&
619
+ patch.externalId !== mapping.externalId) ||
620
+ active !== mapping.active
621
+ ) {
622
+ await this.repository.updateUser({
623
+ tenantId: context.tenantId,
624
+ id: mapping.id,
625
+ externalId:
626
+ patch.externalId === undefined
627
+ ? mapping.externalId
628
+ : patch.externalId,
629
+ active,
630
+ lastSyncedAt: this.now(),
631
+ });
632
+ changed = true;
633
+ }
634
+ const stored =
635
+ (await this.repository.findUserById(context.tenantId, mapping.id)) ??
636
+ mapping;
637
+ return {
638
+ outcome: changed ? ('applied' as const) : ('unchanged' as const),
639
+ /* A status that did not move, next to a change that did, is an
640
+ update; a request that changed nothing keeps the name of what the
641
+ provider asked for. */
642
+ operation:
643
+ patch.active === undefined
644
+ ? 'user-update'
645
+ : statusMoved || !changed
646
+ ? patch.active
647
+ ? 'user-reactivate'
648
+ : 'user-deactivate'
649
+ : 'user-update',
650
+ /* The member is rendered from what this run just applied rather
651
+ than from a second full listing of the workspace. */
652
+ resource: this.#userResource(context, stored, {
653
+ ...member,
654
+ displayName,
655
+ membershipStatus: active ? 'active' : 'disabled',
656
+ }),
657
+ };
658
+ });
659
+ }
660
+
661
+ async listGroups(
662
+ context: ScimRequestContext,
663
+ url: URL,
664
+ ): Promise<Record<string, unknown>> {
665
+ const filter = parseFilter(
666
+ url.searchParams.get('filter'),
667
+ GROUP_FILTER_ATTRIBUTES,
668
+ );
669
+ const page = parsePage(url, context.pageSizeMax);
670
+ const slice = await this.repository.listGroups(
671
+ context.tenantId,
672
+ {
673
+ ...(filter?.attribute === 'displayName'
674
+ ? { displayName: filter.value }
675
+ : {}),
676
+ ...(filter?.attribute === 'externalId'
677
+ ? { externalId: filter.value }
678
+ : {}),
679
+ ...(filter?.attribute === 'id' ? { id: filter.value } : {}),
680
+ },
681
+ page,
682
+ );
683
+ const members = await this.repository.listMembersOfGroups(
684
+ context.tenantId,
685
+ slice.records.map((group) => group.id),
686
+ MAX_EMBEDDED_MEMBERS,
687
+ );
688
+ /* Grouped once rather than filtered per group: a page of 200 groups must
689
+ not cost 200 passes over every member row it read. */
690
+ const byGroup = new Map<string, GroupMemberRow[]>();
691
+ for (const member of members) {
692
+ const held = byGroup.get(member.groupId);
693
+ if (held) held.push(member);
694
+ else byGroup.set(member.groupId, [member]);
695
+ }
696
+ return listResponse({
697
+ totalResults: slice.totalResults,
698
+ startIndex: page.startIndex,
699
+ resources: slice.records.map((group) =>
700
+ this.#groupResource(context, group, byGroup.get(group.id) ?? []),
701
+ ),
702
+ });
703
+ }
704
+
705
+ async getGroup(
706
+ context: ScimRequestContext,
707
+ id: string,
708
+ ): Promise<Record<string, unknown>> {
709
+ const group = await this.#requireGroup(context, id);
710
+ return this.#groupResource(
711
+ context,
712
+ group,
713
+ await this.repository.listMembersOfGroups(
714
+ context.tenantId,
715
+ [group.id],
716
+ MAX_EMBEDDED_MEMBERS,
717
+ ),
718
+ );
719
+ }
720
+
721
+ async createGroup(
722
+ context: ScimRequestContext,
723
+ body: Record<string, unknown>,
724
+ ): Promise<Record<string, unknown>> {
725
+ /* Parsed here so a refused create still names what it tried to create;
726
+ the run replaces the subject with the group id once one exists. */
727
+ const requestedName = optionalText(
728
+ body.displayName,
729
+ 'displayName',
730
+ MAX_DISPLAY_NAME,
731
+ );
732
+ return this.#recorded(
733
+ context,
734
+ 'group-create',
735
+ requestedName ?? '',
736
+ async () => {
737
+ const displayName = text(
738
+ body.displayName,
739
+ 'displayName',
740
+ MAX_DISPLAY_NAME,
741
+ );
742
+ const createdAt = this.now();
743
+ const group = {
744
+ id: randomUUID(),
745
+ tenantId: context.tenantId,
746
+ externalId: optionalText(
747
+ body.externalId,
748
+ 'externalId',
749
+ MAX_EXTERNAL_ID,
750
+ ),
751
+ displayName,
752
+ /* A new group grants nothing until an owner maps it to a role. */
753
+ roleKey: null,
754
+ precedence: DEFAULT_GROUP_PRECEDENCE,
755
+ createdAt,
756
+ updatedAt: createdAt,
757
+ };
758
+ const requested = await this.#resolveMembers(
759
+ context,
760
+ memberIds(body.members),
761
+ );
762
+ try {
763
+ await this.repository.insertGroup(group);
764
+ } catch (error) {
765
+ throw this.#groupConflict(error);
766
+ }
767
+ const change = await this.repository.applyGroupMembers({
768
+ tenantId: context.tenantId,
769
+ groupId: group.id,
770
+ steps: [{ kind: 'set', members: requested }],
771
+ now: createdAt,
772
+ });
773
+ await this.#recalculate(context, [...change.added, ...change.removed]);
774
+ return {
775
+ outcome: 'applied' as const,
776
+ subject: group.id,
777
+ resource: await this.#groupWrite(context, group.id, true).then(
778
+ (write) => write.resource,
779
+ ),
780
+ };
781
+ },
782
+ );
783
+ }
784
+
785
+ async replaceGroup(
786
+ context: ScimRequestContext,
787
+ id: string,
788
+ body: Record<string, unknown>,
789
+ ): Promise<Record<string, unknown>> {
790
+ const group = await this.#requireGroup(context, id);
791
+ return this.#recorded(context, 'group-update', group.id, async () => {
792
+ const displayName =
793
+ body.displayName === undefined
794
+ ? group.displayName
795
+ : text(body.displayName, 'displayName', MAX_DISPLAY_NAME);
796
+ const members =
797
+ body.members === undefined
798
+ ? null
799
+ : await this.#resolveMembers(context, memberIds(body.members));
800
+ const changed = await this.#applyGroupChange(context, group, {
801
+ displayName,
802
+ externalId:
803
+ body.externalId === undefined
804
+ ? group.externalId
805
+ : optionalText(body.externalId, 'externalId', MAX_EXTERNAL_ID),
806
+ steps: members === null ? [] : [{ kind: 'set', members }],
807
+ });
808
+ return this.#groupWrite(context, group.id, changed);
809
+ });
810
+ }
811
+
812
+ async patchGroup(
813
+ context: ScimRequestContext,
814
+ id: string,
815
+ body: Record<string, unknown>,
816
+ ): Promise<Record<string, unknown>> {
817
+ const group = await this.#requireGroup(context, id);
818
+ /* The operation is decided before the body is walked, because the walk
819
+ is what can refuse, and the refusal belongs in the log under the
820
+ operation the provider attempted. */
821
+ const patch = parsePatch(body);
822
+ const touchesMembers = patch.some((operation) =>
823
+ this.#groupPatchTargets(operation).some(
824
+ ([attribute]) => attribute === 'members',
825
+ ),
826
+ );
827
+ return this.#recorded(
828
+ context,
829
+ touchesMembers ? 'membership-change' : 'group-update',
830
+ group.id,
831
+ async () => {
832
+ let displayName = group.displayName;
833
+ let externalId = group.externalId;
834
+ const steps: GroupMemberStep[] = [];
835
+ for (const operation of patch) {
836
+ for (const [attribute, value] of this.#groupPatchTargets(operation)) {
837
+ if (attribute === 'members') {
838
+ /* Okta and Entra address a single member through the path
839
+ filter rather than the value, so both spellings resolve. */
840
+ const targeted = memberPathFilter(operation.path);
841
+ const ids =
842
+ targeted === null
843
+ ? memberIds(value)
844
+ : [targeted, ...memberIds(value)];
845
+ const resolved = await this.#resolveMembers(context, ids);
846
+ if (operation.op === 'replace') {
847
+ steps.push({ kind: 'set', members: resolved });
848
+ } else if (operation.op === 'add') {
849
+ steps.push({ kind: 'add', members: resolved });
850
+ } else if (resolved.length === 0) {
851
+ /* `remove` with no value and no filter clears the
852
+ membership, which is how a provider empties a group. */
853
+ steps.push({ kind: 'set', members: [] });
854
+ } else {
855
+ steps.push({ kind: 'remove', members: resolved });
856
+ }
857
+ } else if (attribute === 'displayName') {
858
+ displayName =
859
+ operation.op === 'remove'
860
+ ? group.displayName
861
+ : text(value, 'displayName', MAX_DISPLAY_NAME);
862
+ } else if (attribute === 'externalId') {
863
+ externalId =
864
+ operation.op === 'remove'
865
+ ? null
866
+ : optionalText(value, 'externalId', MAX_EXTERNAL_ID);
867
+ }
868
+ }
869
+ }
870
+ const changed = await this.#applyGroupChange(context, group, {
871
+ displayName,
872
+ externalId,
873
+ steps,
874
+ });
875
+ return this.#groupWrite(context, group.id, changed);
876
+ },
877
+ );
878
+ }
879
+
880
+ async deleteGroup(context: ScimRequestContext, id: string): Promise<void> {
881
+ const group = await this.#requireGroup(context, id);
882
+ await this.#recorded(context, 'group-delete', group.id, async () => {
883
+ const members = await this.repository.listGroupMemberIds(
884
+ context.tenantId,
885
+ group.id,
886
+ );
887
+ const removed = await this.repository.deleteGroup(
888
+ context.tenantId,
889
+ group.id,
890
+ );
891
+ try {
892
+ await this.#recalculate(context, members);
893
+ } catch (error) {
894
+ /* Same all-or-nothing rule as a membership change: a refused role
895
+ change puts the group and its members back, and a restore that
896
+ fails is reported instead of leaving the group gone in silence. */
897
+ if (removed) {
898
+ await this.#unwind(
899
+ context,
900
+ [
901
+ async () => {
902
+ await this.repository.insertGroup(group);
903
+ await this.repository.applyGroupMembers({
904
+ tenantId: context.tenantId,
905
+ groupId: group.id,
906
+ steps: [{ kind: 'set', members }],
907
+ now: this.now(),
908
+ });
909
+ },
910
+ ],
911
+ group.id,
912
+ 'group-delete',
913
+ );
914
+ }
915
+ throw error;
916
+ }
917
+ return {
918
+ outcome: removed ? ('applied' as const) : ('unchanged' as const),
919
+ resource: {},
920
+ };
921
+ });
922
+ }
923
+
924
+ #groupPatchTargets(
925
+ operation: ScimPatchOperation,
926
+ ): readonly (readonly [string, unknown])[] {
927
+ if (operation.path === null) {
928
+ if (
929
+ typeof operation.value !== 'object' ||
930
+ operation.value === null ||
931
+ Array.isArray(operation.value)
932
+ ) {
933
+ throw new ScimError(
934
+ 400,
935
+ 'noTarget',
936
+ DIRECTORY_REASONS.unsupportedPath,
937
+ 'A group patch without a path must carry an object value.',
938
+ );
939
+ }
940
+ return Object.entries(operation.value as Record<string, unknown>).map(
941
+ ([key, entry]) => [this.#knownGroupAttribute(key), entry] as const,
942
+ );
943
+ }
944
+ return [
945
+ [this.#knownGroupAttribute(operation.path), operation.value] as const,
946
+ ];
947
+ }
948
+
949
+ #knownGroupAttribute(path: string): string {
950
+ const attribute = path.split('[')[0]!.split('.')[0]!.trim();
951
+ const matched = ['members', 'displayName', 'externalId'].find(
952
+ (candidate) => candidate.toLowerCase() === attribute.toLowerCase(),
953
+ );
954
+ if (!matched) {
955
+ throw new ScimError(
956
+ 400,
957
+ 'noTarget',
958
+ DIRECTORY_REASONS.unsupportedPath,
959
+ `${attribute} is not an attribute this service provider maintains.`,
960
+ );
961
+ }
962
+ return matched;
963
+ }
964
+
965
+ async #applyGroupChange(
966
+ context: ScimRequestContext,
967
+ group: ScimGroupMapping,
968
+ change: {
969
+ readonly displayName: string;
970
+ readonly externalId: string | null;
971
+ readonly steps: readonly GroupMemberStep[];
972
+ },
973
+ ): Promise<boolean> {
974
+ /* Role assignment is a second system, so it cannot join this module's
975
+ transaction. Each applied step records how to undo itself, and a
976
+ refusal from auth.core unwinds them: the provider sees the group and
977
+ the roles exactly as they were. */
978
+ const undo: (() => Promise<unknown>)[] = [];
979
+ let changed = false;
980
+ try {
981
+ if (
982
+ change.displayName !== group.displayName ||
983
+ change.externalId !== group.externalId
984
+ ) {
985
+ await this.#renameGroup(context, group, {
986
+ displayName: change.displayName,
987
+ externalId: change.externalId,
988
+ });
989
+ undo.push(() =>
990
+ this.#renameGroup(context, group, {
991
+ displayName: group.displayName,
992
+ externalId: group.externalId,
993
+ }),
994
+ );
995
+ changed = true;
996
+ }
997
+ const applied =
998
+ change.steps.length === 0
999
+ ? { added: [], removed: [] }
1000
+ : await this.repository.applyGroupMembers({
1001
+ tenantId: context.tenantId,
1002
+ groupId: group.id,
1003
+ steps: change.steps,
1004
+ now: this.now(),
1005
+ });
1006
+ const touched = [...applied.added, ...applied.removed];
1007
+ if (touched.length > 0) {
1008
+ undo.push(() =>
1009
+ this.repository.applyGroupMembers({
1010
+ tenantId: context.tenantId,
1011
+ groupId: group.id,
1012
+ steps: [
1013
+ { kind: 'remove', members: applied.added },
1014
+ { kind: 'add', members: applied.removed },
1015
+ ],
1016
+ now: this.now(),
1017
+ }),
1018
+ );
1019
+ changed = true;
1020
+ }
1021
+ await this.#recalculate(context, touched);
1022
+ return changed;
1023
+ } catch (error) {
1024
+ await this.#unwind(context, undo, group.id, 'group-update');
1025
+ throw error;
1026
+ }
1027
+ }
1028
+
1029
+ /**
1030
+ * Runs the undo steps of a refused change. A step that fails leaves the
1031
+ * workspace between two states, which nobody would otherwise see: it is
1032
+ * reported as a fault rather than answered as a clean refusal.
1033
+ */
1034
+ async #unwind(
1035
+ context: ScimRequestContext,
1036
+ undo: readonly (() => Promise<unknown>)[],
1037
+ subject: string,
1038
+ operation: ProvisioningOperation,
1039
+ ): Promise<void> {
1040
+ for (const step of [...undo].reverse()) {
1041
+ try {
1042
+ await step();
1043
+ } catch (error) {
1044
+ serverLogger().error('directory rollback failed', {
1045
+ module: 'directory.core',
1046
+ err: { name: error instanceof Error ? error.name : 'non-error' },
1047
+ fields: { subject, tenantId: context.tenantId },
1048
+ });
1049
+ /* The provisioning log is the operator's own surface, so the
1050
+ inconsistency is recorded there as well as in the server log. */
1051
+ await this.#append(
1052
+ context,
1053
+ operation,
1054
+ subject,
1055
+ 'refused',
1056
+ DIRECTORY_REASONS.rollbackFailed,
1057
+ ).catch(() => undefined);
1058
+ throw new ScimError(
1059
+ 500,
1060
+ null,
1061
+ DIRECTORY_REASONS.rollbackFailed,
1062
+ 'The change was refused and could not be undone; the workspace needs an operator.',
1063
+ );
1064
+ }
1065
+ }
1066
+ }
1067
+
1068
+ async #renameGroup(
1069
+ context: ScimRequestContext,
1070
+ group: ScimGroupMapping,
1071
+ to: { readonly displayName: string; readonly externalId: string | null },
1072
+ ): Promise<void> {
1073
+ try {
1074
+ await this.repository.updateGroup({
1075
+ tenantId: context.tenantId,
1076
+ id: group.id,
1077
+ externalId: to.externalId,
1078
+ displayName: to.displayName,
1079
+ roleKey: group.roleKey,
1080
+ precedence: group.precedence,
1081
+ updatedAt: this.now(),
1082
+ });
1083
+ } catch (error) {
1084
+ throw this.#groupConflict(error);
1085
+ }
1086
+ }
1087
+
1088
+ async #groupWrite(
1089
+ context: ScimRequestContext,
1090
+ id: string,
1091
+ changed: boolean,
1092
+ ): Promise<ScimWrite> {
1093
+ const stored = await this.repository.findGroupById(context.tenantId, id);
1094
+ if (!stored) {
1095
+ throw notFound(
1096
+ DIRECTORY_REASONS.groupNotFound,
1097
+ 'The group no longer exists in this workspace.',
1098
+ );
1099
+ }
1100
+ return {
1101
+ outcome: changed ? 'applied' : 'unchanged',
1102
+ resource: this.#groupResource(
1103
+ context,
1104
+ stored,
1105
+ await this.repository.listMembersOfGroups(
1106
+ context.tenantId,
1107
+ [id],
1108
+ MAX_EMBEDDED_MEMBERS,
1109
+ ),
1110
+ ),
1111
+ };
1112
+ }
1113
+
1114
+ #groupConflict(error: unknown): unknown {
1115
+ if (error instanceof DirectoryUniqueViolation) {
1116
+ return new ScimError(
1117
+ 409,
1118
+ 'uniqueness',
1119
+ DIRECTORY_REASONS.groupExists,
1120
+ 'A group with this displayName already exists in the workspace.',
1121
+ );
1122
+ }
1123
+ return error;
1124
+ }
1125
+
1126
+ /** One query for the whole member list, refusing the first id that is unknown. */
1127
+ async #resolveMembers(
1128
+ context: ScimRequestContext,
1129
+ ids: readonly string[],
1130
+ ): Promise<readonly string[]> {
1131
+ const unique = [...new Set(ids)];
1132
+ if (unique.length === 0) return [];
1133
+ const known = new Set(
1134
+ (await this.repository.findUsersByIds(context.tenantId, unique)).map(
1135
+ (mapping) => mapping.id,
1136
+ ),
1137
+ );
1138
+ for (const id of unique) {
1139
+ if (!known.has(id)) {
1140
+ throw invalid(
1141
+ DIRECTORY_REASONS.userNotFound,
1142
+ 'A member value names a SCIM user that does not exist in this workspace.',
1143
+ );
1144
+ }
1145
+ }
1146
+ return unique;
1147
+ }
1148
+
1149
+ /**
1150
+ * Re-derives the role of every user whose group membership moved. Precedence
1151
+ * comes from the one repository query the administration screen uses, so the
1152
+ * two paths can never disagree on which mapped group wins. A user whose
1153
+ * resolved role already matches is left alone, so a repeat of the same
1154
+ * membership state writes nothing. A refusal partway through undoes the role
1155
+ * changes this pass already made, and the log is written only once the pass
1156
+ * has succeeded, so it never records a change that was rolled back.
1157
+ */
1158
+ async #recalculate(
1159
+ context: ScimRequestContext,
1160
+ userIds: readonly string[],
1161
+ ): Promise<void> {
1162
+ if (userIds.length === 0) return;
1163
+ const view = new WorkspaceView(this.auth, context.tenantId);
1164
+ const actor = scimActor(context.tenantId, context.token.id);
1165
+ const applied: { accountId: string; previous: string }[] = [];
1166
+ const events: { subject: string; outcome: ProvisioningOutcome }[] = [];
1167
+ const resolved = await this.repository.resolvedRolesForUsers(
1168
+ context.tenantId,
1169
+ userIds,
1170
+ );
1171
+ try {
1172
+ for (const member of resolved) {
1173
+ const status = await view.statusOf(member.accountId);
1174
+ if (status === null) {
1175
+ /* The mapping outlived the membership. Visible in the log rather
1176
+ than failing every other member of the same request. */
1177
+ events.push({ subject: member.userId, outcome: 'refused' });
1178
+ continue;
1179
+ }
1180
+ /* A disabled membership holds no role anyone can act on, and
1181
+ auth.core counts it out of the owner tally, so recomputing it
1182
+ would refuse a change the workspace can make. */
1183
+ if (status !== 'active') continue;
1184
+ const current = await view.roleOf(member.accountId);
1185
+ if (current === null) continue;
1186
+ const target = await this.#requireRole(
1187
+ view,
1188
+ member.roleKey ?? context.defaultRole,
1189
+ member.roleKey === null,
1190
+ );
1191
+ if (current === target) continue;
1192
+ await this.#throughAuth(() =>
1193
+ this.auth.assignRole(actor, member.accountId, target),
1194
+ );
1195
+ applied.push({ accountId: member.accountId, previous: current });
1196
+ view.recordRole(member.accountId, target);
1197
+ events.push({ subject: member.userId, outcome: 'applied' });
1198
+ }
1199
+ } catch (error) {
1200
+ for (const change of applied.reverse()) {
1201
+ /* Restoring a role the workspace already held cannot be refused; a
1202
+ failure here is reported by the original error, not replaced. */
1203
+ await this.auth
1204
+ .assignRole(actor, change.accountId, change.previous)
1205
+ .catch(() => undefined);
1206
+ }
1207
+ throw error;
1208
+ }
1209
+ await this.#appendAll(
1210
+ context,
1211
+ events.map((event) => ({
1212
+ operation: 'membership-change' as const,
1213
+ subject: event.subject,
1214
+ outcome: event.outcome,
1215
+ reason:
1216
+ event.outcome === 'refused' ? DIRECTORY_REASONS.memberNotFound : null,
1217
+ })),
1218
+ );
1219
+ }
1220
+
1221
+ async #requireRole(
1222
+ view: WorkspaceView,
1223
+ roleKey: string,
1224
+ isDefault: boolean,
1225
+ ): Promise<string> {
1226
+ if ((await view.roles()).has(roleKey)) return roleKey;
1227
+ throw invalid(
1228
+ isDefault
1229
+ ? DIRECTORY_REASONS.defaultRoleUnknown
1230
+ : DIRECTORY_REASONS.roleUnknown,
1231
+ `The role "${roleKey}" does not exist in this workspace.`,
1232
+ );
1233
+ }
1234
+
1235
+ async #insertMapping(mapping: ScimUserMapping): Promise<void> {
1236
+ try {
1237
+ await this.repository.insertUser(mapping);
1238
+ } catch (error) {
1239
+ if (error instanceof DirectoryUniqueViolation) {
1240
+ throw new ScimError(
1241
+ 409,
1242
+ 'uniqueness',
1243
+ DIRECTORY_REASONS.accountInUse,
1244
+ 'The account is already mapped to a SCIM user in this workspace.',
1245
+ );
1246
+ }
1247
+ throw error;
1248
+ }
1249
+ }
1250
+
1251
+ async #requireUser(
1252
+ context: ScimRequestContext,
1253
+ id: string,
1254
+ ): Promise<ScimUserMapping> {
1255
+ const mapping =
1256
+ id.length <= 128
1257
+ ? await this.repository.findUserById(context.tenantId, id)
1258
+ : null;
1259
+ if (!mapping) {
1260
+ throw notFound(
1261
+ DIRECTORY_REASONS.userNotFound,
1262
+ 'The SCIM user does not exist in this workspace.',
1263
+ );
1264
+ }
1265
+ return mapping;
1266
+ }
1267
+
1268
+ async #requireGroup(
1269
+ context: ScimRequestContext,
1270
+ id: string,
1271
+ ): Promise<ScimGroupMapping> {
1272
+ const group =
1273
+ id.length <= 128
1274
+ ? await this.repository.findGroupById(context.tenantId, id)
1275
+ : null;
1276
+ if (!group) {
1277
+ throw notFound(
1278
+ DIRECTORY_REASONS.groupNotFound,
1279
+ 'The group does not exist in this workspace.',
1280
+ );
1281
+ }
1282
+ return group;
1283
+ }
1284
+
1285
+ async #throughAuth<T>(run: () => Promise<T>): Promise<T> {
1286
+ try {
1287
+ return await run();
1288
+ } catch (error) {
1289
+ const translated = fromAuthError(error);
1290
+ if (translated) throw translated;
1291
+ throw error;
1292
+ }
1293
+ }
1294
+
1295
+ /**
1296
+ * Runs one SCIM operation and writes exactly one provisioning event for it,
1297
+ * including the refusal. The event carries the token id and the stable
1298
+ * reason, never the token value.
1299
+ */
1300
+ async #recorded(
1301
+ context: ScimRequestContext,
1302
+ operation: ProvisioningOperation,
1303
+ subject: string,
1304
+ run: () => Promise<ScimWrite>,
1305
+ ): Promise<Record<string, unknown>> {
1306
+ try {
1307
+ const result = await run();
1308
+ await this.#append(
1309
+ context,
1310
+ result.operation ?? operation,
1311
+ result.subject ?? subject,
1312
+ result.outcome,
1313
+ null,
1314
+ );
1315
+ return result.resource;
1316
+ } catch (error) {
1317
+ if (error instanceof ScimError) {
1318
+ await this.#append(
1319
+ context,
1320
+ operation,
1321
+ subject,
1322
+ 'refused',
1323
+ error.reason,
1324
+ );
1325
+ }
1326
+ throw error;
1327
+ }
1328
+ }
1329
+
1330
+ async #append(
1331
+ context: ScimRequestContext,
1332
+ operation: ProvisioningOperation,
1333
+ subject: string,
1334
+ outcome: ProvisioningOutcome,
1335
+ reason: string | null,
1336
+ ): Promise<void> {
1337
+ await this.#appendAll(context, [{ operation, subject, outcome, reason }]);
1338
+ }
1339
+
1340
+ /* One write for the whole pass: a membership change over a large group would
1341
+ otherwise cost one transaction per member it recorded. */
1342
+ async #appendAll(
1343
+ context: ScimRequestContext,
1344
+ entries: readonly {
1345
+ readonly operation: ProvisioningOperation;
1346
+ readonly subject: string;
1347
+ readonly outcome: ProvisioningOutcome;
1348
+ readonly reason: string | null;
1349
+ }[],
1350
+ ): Promise<void> {
1351
+ if (entries.length === 0) return;
1352
+ const occurredAt = this.now();
1353
+ await this.repository.appendEvents(
1354
+ entries.map((entry) => ({
1355
+ id: randomUUID(),
1356
+ tenantId: context.tenantId,
1357
+ tokenId: context.token.id,
1358
+ operation: entry.operation,
1359
+ subject: entry.subject.slice(0, 320),
1360
+ outcome: entry.outcome,
1361
+ reason: entry.reason,
1362
+ occurredAt,
1363
+ })),
1364
+ );
1365
+ }
1366
+
1367
+ #userResource(
1368
+ context: ScimRequestContext,
1369
+ mapping: ScimUserMapping,
1370
+ member: TenantMember | null,
1371
+ ): Record<string, unknown> {
1372
+ const displayName = member?.displayName ?? mapping.userName;
1373
+ return {
1374
+ schemas: [SCIM_SCHEMAS.user],
1375
+ id: mapping.id,
1376
+ ...(mapping.externalId === null
1377
+ ? {}
1378
+ : { externalId: mapping.externalId }),
1379
+ userName: mapping.userName,
1380
+ displayName,
1381
+ name: { formatted: displayName },
1382
+ emails: [{ value: mapping.userName, primary: true, type: 'work' }],
1383
+ /* The workspace's own membership status is the truth; the stored flag
1384
+ is the mirror the screens and the log read. */
1385
+ active: member ? member.membershipStatus === 'active' : mapping.active,
1386
+ meta: scimMeta({
1387
+ resourceType: 'User',
1388
+ createdAt: mapping.createdAt,
1389
+ updatedAt: mapping.lastSyncedAt,
1390
+ baseUrl: context.baseUrl,
1391
+ id: mapping.id,
1392
+ }),
1393
+ };
1394
+ }
1395
+
1396
+ #groupResource(
1397
+ context: ScimRequestContext,
1398
+ group: ScimGroupMapping,
1399
+ members: readonly { groupId: string; userId: string; userName: string }[],
1400
+ ): Record<string, unknown> {
1401
+ return {
1402
+ schemas: [SCIM_SCHEMAS.group],
1403
+ id: group.id,
1404
+ ...(group.externalId === null ? {} : { externalId: group.externalId }),
1405
+ displayName: group.displayName,
1406
+ members: members.map((member) => ({
1407
+ value: member.userId,
1408
+ display: member.userName,
1409
+ $ref: `${context.baseUrl}/Users/${member.userId}`,
1410
+ })),
1411
+ meta: scimMeta({
1412
+ resourceType: 'Group',
1413
+ createdAt: group.createdAt,
1414
+ updatedAt: group.updatedAt,
1415
+ baseUrl: context.baseUrl,
1416
+ id: group.id,
1417
+ }),
1418
+ };
1419
+ }
1420
+ }