@hilbras/keystone 1.6.0 → 1.9.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 (559) hide show
  1. package/CHANGELOG.md +239 -0
  2. package/README.md +55 -25
  3. package/dist/cli.js +25 -27
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +15 -1
  6. package/dist/config.d.ts.map +1 -1
  7. package/dist/config.js +45 -8
  8. package/dist/config.js.map +1 -1
  9. package/dist/container.d.ts +4 -1
  10. package/dist/container.d.ts.map +1 -1
  11. package/dist/container.js.map +1 -1
  12. package/dist/db/migrations/0008_flashy_firestar.sql +5 -0
  13. package/dist/db/migrations/0009_daffy_changeling.sql +1 -0
  14. package/dist/db/migrations/0010_wealthy_shadow_king.sql +37 -0
  15. package/dist/db/migrations/0011_overrated_vector.sql +18 -0
  16. package/dist/db/migrations/0012_brave_darkhawk.sql +28 -0
  17. package/dist/db/migrations/0013_wonderful_chimera.sql +5 -0
  18. package/dist/db/migrations/0014_magical_morph.sql +48 -0
  19. package/{src/db/migrations/meta/0007_snapshot.json → dist/db/migrations/meta/0008_snapshot.json} +136 -32
  20. package/{src/db/migrations/meta/0006_snapshot.json → dist/db/migrations/meta/0009_snapshot.json} +359 -30
  21. package/{src/db/migrations/meta/0005_snapshot.json → dist/db/migrations/meta/0010_snapshot.json} +459 -29
  22. package/{src/db/migrations/meta/0004_snapshot.json → dist/db/migrations/meta/0011_snapshot.json} +594 -30
  23. package/dist/db/migrations/meta/0012_snapshot.json +3891 -0
  24. package/dist/db/migrations/meta/0013_snapshot.json +3919 -0
  25. package/dist/db/migrations/meta/0014_snapshot.json +4311 -0
  26. package/dist/db/migrations/meta/_journal.json +49 -0
  27. package/dist/db/reencryptOidcSecrets.d.ts +2 -0
  28. package/dist/db/reencryptOidcSecrets.d.ts.map +1 -0
  29. package/dist/db/reencryptOidcSecrets.js +41 -0
  30. package/dist/db/reencryptOidcSecrets.js.map +1 -0
  31. package/dist/db/schema.d.ts +2521 -53
  32. package/dist/db/schema.d.ts.map +1 -1
  33. package/dist/db/schema.js +118 -3
  34. package/dist/db/schema.js.map +1 -1
  35. package/dist/db/seed.js +67 -40
  36. package/dist/db/seed.js.map +1 -1
  37. package/dist/di.d.ts.map +1 -1
  38. package/dist/di.js +11 -3
  39. package/dist/di.js.map +1 -1
  40. package/dist/index.d.ts +2 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -2
  43. package/dist/index.js.map +1 -1
  44. package/dist/lib/errors.d.ts +2 -1
  45. package/dist/lib/errors.d.ts.map +1 -1
  46. package/dist/lib/errors.js +10 -0
  47. package/dist/lib/errors.js.map +1 -1
  48. package/dist/plugins/appContext.d.ts.map +1 -1
  49. package/dist/plugins/appContext.js +3 -12
  50. package/dist/plugins/appContext.js.map +1 -1
  51. package/dist/plugins/audit.d.ts.map +1 -1
  52. package/dist/plugins/audit.js +26 -6
  53. package/dist/plugins/audit.js.map +1 -1
  54. package/dist/plugins/auth.d.ts.map +1 -1
  55. package/dist/plugins/auth.js +32 -7
  56. package/dist/plugins/auth.js.map +1 -1
  57. package/dist/plugins/metrics.d.ts.map +1 -1
  58. package/dist/plugins/mtls.d.ts.map +1 -1
  59. package/dist/plugins/permissions.d.ts.map +1 -1
  60. package/dist/plugins/permissions.js +29 -9
  61. package/dist/plugins/permissions.js.map +1 -1
  62. package/dist/plugins/rateLimit.d.ts +15 -0
  63. package/dist/plugins/rateLimit.d.ts.map +1 -1
  64. package/dist/plugins/rateLimit.js +11 -1
  65. package/dist/plugins/rateLimit.js.map +1 -1
  66. package/dist/plugins/requestLogger.d.ts.map +1 -1
  67. package/dist/repositories/apiKey.d.ts.map +1 -1
  68. package/dist/repositories/application.d.ts.map +1 -1
  69. package/dist/repositories/application.js +1 -1
  70. package/dist/repositories/application.js.map +1 -1
  71. package/dist/repositories/audit.d.ts.map +1 -1
  72. package/dist/repositories/index.d.ts +3 -0
  73. package/dist/repositories/index.d.ts.map +1 -1
  74. package/dist/repositories/index.js +3 -0
  75. package/dist/repositories/index.js.map +1 -1
  76. package/dist/repositories/mfaChallenge.d.ts +23 -0
  77. package/dist/repositories/mfaChallenge.d.ts.map +1 -0
  78. package/dist/repositories/mfaChallenge.js +71 -0
  79. package/dist/repositories/mfaChallenge.js.map +1 -0
  80. package/dist/repositories/oidcConnection.d.ts +27 -43
  81. package/dist/repositories/oidcConnection.d.ts.map +1 -1
  82. package/dist/repositories/oidcConnection.js +15 -6
  83. package/dist/repositories/oidcConnection.js.map +1 -1
  84. package/dist/repositories/organization.d.ts +9 -9
  85. package/dist/repositories/organization.d.ts.map +1 -1
  86. package/dist/repositories/organization.js +91 -37
  87. package/dist/repositories/organization.js.map +1 -1
  88. package/dist/repositories/permission.d.ts.map +1 -1
  89. package/dist/repositories/permission.js +27 -5
  90. package/dist/repositories/permission.js.map +1 -1
  91. package/dist/repositories/samlConnection.d.ts +28 -28
  92. package/dist/repositories/samlConnection.d.ts.map +1 -1
  93. package/dist/repositories/samlConnection.js +12 -4
  94. package/dist/repositories/samlConnection.js.map +1 -1
  95. package/dist/repositories/scimConnection.d.ts +38 -0
  96. package/dist/repositories/scimConnection.d.ts.map +1 -0
  97. package/dist/repositories/scimConnection.js +123 -0
  98. package/dist/repositories/scimConnection.js.map +1 -0
  99. package/dist/repositories/scimGroup.d.ts +36 -0
  100. package/dist/repositories/scimGroup.d.ts.map +1 -0
  101. package/dist/repositories/scimGroup.js +113 -0
  102. package/dist/repositories/scimGroup.js.map +1 -0
  103. package/dist/repositories/session.d.ts +23 -13
  104. package/dist/repositories/session.d.ts.map +1 -1
  105. package/dist/repositories/session.js +13 -1
  106. package/dist/repositories/session.js.map +1 -1
  107. package/dist/repositories/types.d.ts +112 -5
  108. package/dist/repositories/types.d.ts.map +1 -1
  109. package/dist/repositories/types.js +7 -1
  110. package/dist/repositories/types.js.map +1 -1
  111. package/dist/repositories/user.d.ts +23 -1
  112. package/dist/repositories/user.d.ts.map +1 -1
  113. package/dist/repositories/user.js +180 -4
  114. package/dist/repositories/user.js.map +1 -1
  115. package/dist/routes/admin/billing.js +4 -4
  116. package/dist/routes/admin/billing.js.map +1 -1
  117. package/dist/routes/admin/helpers.d.ts +16 -20
  118. package/dist/routes/admin/helpers.d.ts.map +1 -1
  119. package/dist/routes/admin/helpers.js +53 -10
  120. package/dist/routes/admin/helpers.js.map +1 -1
  121. package/dist/routes/admin/organizations.d.ts.map +1 -1
  122. package/dist/routes/admin/organizations.js +76 -69
  123. package/dist/routes/admin/organizations.js.map +1 -1
  124. package/dist/routes/admin/permissions.d.ts.map +1 -1
  125. package/dist/routes/admin/permissions.js +48 -14
  126. package/dist/routes/admin/permissions.js.map +1 -1
  127. package/dist/routes/admin/platform.d.ts.map +1 -1
  128. package/dist/routes/admin/platform.js +86 -35
  129. package/dist/routes/admin/platform.js.map +1 -1
  130. package/dist/routes/admin/sso.d.ts.map +1 -1
  131. package/dist/routes/admin/sso.js +143 -12
  132. package/dist/routes/admin/sso.js.map +1 -1
  133. package/dist/routes/admin/webhooks.js +8 -8
  134. package/dist/routes/admin/webhooks.js.map +1 -1
  135. package/dist/routes/apiKeys.d.ts.map +1 -1
  136. package/dist/routes/apiKeys.js +23 -5
  137. package/dist/routes/apiKeys.js.map +1 -1
  138. package/dist/routes/auth.d.ts.map +1 -1
  139. package/dist/routes/auth.js +112 -22
  140. package/dist/routes/auth.js.map +1 -1
  141. package/dist/routes/authz.d.ts.map +1 -1
  142. package/dist/routes/authz.js +16 -4
  143. package/dist/routes/authz.js.map +1 -1
  144. package/dist/routes/config.d.ts.map +1 -1
  145. package/dist/routes/config.js +16 -14
  146. package/dist/routes/config.js.map +1 -1
  147. package/dist/routes/federation.d.ts.map +1 -1
  148. package/dist/routes/federation.js +17 -3
  149. package/dist/routes/federation.js.map +1 -1
  150. package/dist/routes/magicLinks.d.ts.map +1 -1
  151. package/dist/routes/magicLinks.js +15 -3
  152. package/dist/routes/magicLinks.js.map +1 -1
  153. package/dist/routes/oauth.d.ts.map +1 -1
  154. package/dist/routes/oauth.js +22 -3
  155. package/dist/routes/oauth.js.map +1 -1
  156. package/dist/routes/oauth2.d.ts.map +1 -1
  157. package/dist/routes/oauth2.js +104 -11
  158. package/dist/routes/oauth2.js.map +1 -1
  159. package/dist/routes/oidcEnterprise.d.ts.map +1 -1
  160. package/dist/routes/oidcEnterprise.js +67 -21
  161. package/dist/routes/oidcEnterprise.js.map +1 -1
  162. package/dist/routes/password.d.ts.map +1 -1
  163. package/dist/routes/password.js.map +1 -1
  164. package/dist/routes/profile.js +4 -4
  165. package/dist/routes/profile.js.map +1 -1
  166. package/dist/routes/saml.d.ts +9 -0
  167. package/dist/routes/saml.d.ts.map +1 -1
  168. package/dist/routes/saml.js +187 -11
  169. package/dist/routes/saml.js.map +1 -1
  170. package/dist/routes/scim.d.ts +13 -0
  171. package/dist/routes/scim.d.ts.map +1 -1
  172. package/dist/routes/scim.js +885 -110
  173. package/dist/routes/scim.js.map +1 -1
  174. package/dist/routes/sdk.d.ts.map +1 -1
  175. package/dist/routes/sdk.js +24 -8
  176. package/dist/routes/sdk.js.map +1 -1
  177. package/dist/routes/serviceAccounts.d.ts.map +1 -1
  178. package/dist/routes/serviceAccounts.js +2 -1
  179. package/dist/routes/serviceAccounts.js.map +1 -1
  180. package/dist/routes/setup.d.ts.map +1 -1
  181. package/dist/routes/setup.js +15 -16
  182. package/dist/routes/setup.js.map +1 -1
  183. package/dist/routes/smsOtp.js +1 -1
  184. package/dist/routes/smsOtp.js.map +1 -1
  185. package/dist/routes/totp.d.ts.map +1 -1
  186. package/dist/routes/totp.js +113 -23
  187. package/dist/routes/totp.js.map +1 -1
  188. package/dist/routes/webauthn.d.ts.map +1 -1
  189. package/dist/routes/webauthn.js +44 -8
  190. package/dist/routes/webauthn.js.map +1 -1
  191. package/dist/routes/workflows.d.ts.map +1 -1
  192. package/dist/routes/workflows.js +58 -25
  193. package/dist/routes/workflows.js.map +1 -1
  194. package/dist/sdk/index.d.ts.map +1 -1
  195. package/dist/sdk/index.js +141 -40
  196. package/dist/sdk/index.js.map +1 -1
  197. package/dist/sdk/types.d.ts +60 -29
  198. package/dist/sdk/types.d.ts.map +1 -1
  199. package/dist/services/application/authentication.d.ts +39 -1
  200. package/dist/services/application/authentication.d.ts.map +1 -1
  201. package/dist/services/application/authentication.js +43 -1
  202. package/dist/services/application/authentication.js.map +1 -1
  203. package/dist/services/application/identity.d.ts +10 -7
  204. package/dist/services/application/identity.d.ts.map +1 -1
  205. package/dist/services/application/identity.js +13 -10
  206. package/dist/services/application/identity.js.map +1 -1
  207. package/dist/services/application/organization.d.ts +11 -7
  208. package/dist/services/application/organization.d.ts.map +1 -1
  209. package/dist/services/application/organization.js +160 -12
  210. package/dist/services/application/organization.js.map +1 -1
  211. package/dist/services/applications.js +2 -2
  212. package/dist/services/applications.js.map +1 -1
  213. package/dist/services/audit.d.ts +1 -0
  214. package/dist/services/audit.d.ts.map +1 -1
  215. package/dist/services/audit.js +13 -0
  216. package/dist/services/audit.js.map +1 -1
  217. package/dist/services/cache.d.ts.map +1 -1
  218. package/dist/services/configuration/profiles.d.ts +4 -0
  219. package/dist/services/configuration/profiles.d.ts.map +1 -1
  220. package/dist/services/configuration/profiles.js +50 -0
  221. package/dist/services/configuration/profiles.js.map +1 -1
  222. package/dist/services/configuration.d.ts.map +1 -1
  223. package/dist/services/configuration.js.map +1 -1
  224. package/dist/services/connectors/google.d.ts.map +1 -1
  225. package/dist/services/connectors/google.js +3 -2
  226. package/dist/services/connectors/google.js.map +1 -1
  227. package/dist/services/connectors/oidc.d.ts.map +1 -1
  228. package/dist/services/connectors/oidc.js +6 -3
  229. package/dist/services/connectors/oidc.js.map +1 -1
  230. package/dist/services/connectors/zitadel.d.ts.map +1 -1
  231. package/dist/services/connectors/zitadel.js +4 -3
  232. package/dist/services/connectors/zitadel.js.map +1 -1
  233. package/dist/services/domain/authentication.d.ts +43 -4
  234. package/dist/services/domain/authentication.d.ts.map +1 -1
  235. package/dist/services/domain/authentication.js +82 -20
  236. package/dist/services/domain/authentication.js.map +1 -1
  237. package/dist/services/domain/authorization.d.ts +11 -0
  238. package/dist/services/domain/authorization.d.ts.map +1 -1
  239. package/dist/services/domain/authorization.js +36 -1
  240. package/dist/services/domain/authorization.js.map +1 -1
  241. package/dist/services/domain/identity.d.ts +21 -9
  242. package/dist/services/domain/identity.d.ts.map +1 -1
  243. package/dist/services/domain/identity.js +187 -10
  244. package/dist/services/domain/identity.js.map +1 -1
  245. package/dist/services/domain/organization.d.ts +6 -9
  246. package/dist/services/domain/organization.d.ts.map +1 -1
  247. package/dist/services/domain/organization.js +48 -30
  248. package/dist/services/domain/organization.js.map +1 -1
  249. package/dist/services/emailVerification.d.ts +2 -2
  250. package/dist/services/enterpriseSso.d.ts +13 -6
  251. package/dist/services/enterpriseSso.d.ts.map +1 -1
  252. package/dist/services/enterpriseSso.js +75 -6
  253. package/dist/services/enterpriseSso.js.map +1 -1
  254. package/dist/services/events/subscribers/auditLog.js +1 -1
  255. package/dist/services/events/subscribers/auditLog.js.map +1 -1
  256. package/dist/services/events/types.d.ts +7 -1
  257. package/dist/services/events/types.d.ts.map +1 -1
  258. package/dist/services/events/types.js.map +1 -1
  259. package/dist/services/events/validate.d.ts.map +1 -1
  260. package/dist/services/events/validate.js +27 -0
  261. package/dist/services/events/validate.js.map +1 -1
  262. package/dist/services/features.d.ts.map +1 -1
  263. package/dist/services/features.js.map +1 -1
  264. package/dist/services/federation.d.ts +6 -1
  265. package/dist/services/federation.d.ts.map +1 -1
  266. package/dist/services/federation.js +31 -4
  267. package/dist/services/federation.js.map +1 -1
  268. package/dist/services/ipControls.js +2 -2
  269. package/dist/services/ipControls.js.map +1 -1
  270. package/dist/services/magicLinks.d.ts +2 -2
  271. package/dist/services/mfa.d.ts +53 -0
  272. package/dist/services/mfa.d.ts.map +1 -0
  273. package/dist/services/mfa.js +157 -0
  274. package/dist/services/mfa.js.map +1 -0
  275. package/dist/services/oauth2.d.ts +16 -12
  276. package/dist/services/oauth2.d.ts.map +1 -1
  277. package/dist/services/oauth2.js +22 -13
  278. package/dist/services/oauth2.js.map +1 -1
  279. package/dist/services/oidcSecretFormat.d.ts +3 -0
  280. package/dist/services/oidcSecretFormat.d.ts.map +1 -0
  281. package/dist/services/oidcSecretFormat.js +5 -0
  282. package/dist/services/oidcSecretFormat.js.map +1 -0
  283. package/dist/services/organizations.d.ts +4 -16
  284. package/dist/services/organizations.d.ts.map +1 -1
  285. package/dist/services/organizations.js +0 -62
  286. package/dist/services/organizations.js.map +1 -1
  287. package/dist/services/permissions.d.ts.map +1 -1
  288. package/dist/services/permissions.js +9 -1
  289. package/dist/services/permissions.js.map +1 -1
  290. package/dist/services/plugins/registry.d.ts.map +1 -1
  291. package/dist/services/plugins/registry.js.map +1 -1
  292. package/dist/services/plugins/types.d.ts.map +1 -1
  293. package/dist/services/queue/bullmq.d.ts.map +1 -1
  294. package/dist/services/queue/inProcess.d.ts.map +1 -1
  295. package/dist/services/redis.d.ts +1 -1
  296. package/dist/services/redis.d.ts.map +1 -1
  297. package/dist/services/scimCredentials.d.ts +46 -0
  298. package/dist/services/scimCredentials.d.ts.map +1 -0
  299. package/dist/services/scimCredentials.js +123 -0
  300. package/dist/services/scimCredentials.js.map +1 -0
  301. package/dist/services/scimLegacyMigration.d.ts +12 -0
  302. package/dist/services/scimLegacyMigration.d.ts.map +1 -0
  303. package/dist/services/scimLegacyMigration.js +44 -0
  304. package/dist/services/scimLegacyMigration.js.map +1 -0
  305. package/dist/services/secrets/awsKms.d.ts +2 -2
  306. package/dist/services/secrets/awsKms.d.ts.map +1 -1
  307. package/dist/services/secrets/awsKms.js +3 -5
  308. package/dist/services/secrets/awsKms.js.map +1 -1
  309. package/dist/services/secrets/azureKeyVault.d.ts.map +1 -1
  310. package/dist/services/secrets/database.d.ts.map +1 -1
  311. package/dist/services/secrets/database.js.map +1 -1
  312. package/dist/services/secrets/environment.d.ts.map +1 -1
  313. package/dist/services/secrets/environment.js.map +1 -1
  314. package/dist/services/secrets/index.d.ts.map +1 -1
  315. package/dist/services/secrets/password.d.ts.map +1 -1
  316. package/dist/services/secrets/password.js +0 -4
  317. package/dist/services/secrets/password.js.map +1 -1
  318. package/dist/services/secrets/provider.d.ts +2 -3
  319. package/dist/services/secrets/provider.d.ts.map +1 -1
  320. package/dist/services/secrets/vault.d.ts.map +1 -1
  321. package/dist/services/setup/configWriter.d.ts.map +1 -1
  322. package/dist/services/setup/configWriter.js.map +1 -1
  323. package/dist/services/ssoEndpointPolicy.d.ts +8 -0
  324. package/dist/services/ssoEndpointPolicy.d.ts.map +1 -0
  325. package/dist/services/ssoEndpointPolicy.js +136 -0
  326. package/dist/services/ssoEndpointPolicy.js.map +1 -0
  327. package/dist/services/stepUp.d.ts +35 -0
  328. package/dist/services/stepUp.d.ts.map +1 -0
  329. package/dist/services/stepUp.js +65 -0
  330. package/dist/services/stepUp.js.map +1 -0
  331. package/dist/services/tokens.d.ts +37 -11
  332. package/dist/services/tokens.d.ts.map +1 -1
  333. package/dist/services/tokens.js +75 -14
  334. package/dist/services/tokens.js.map +1 -1
  335. package/dist/services/totp.d.ts +46 -0
  336. package/dist/services/totp.d.ts.map +1 -1
  337. package/dist/services/totp.js +178 -41
  338. package/dist/services/totp.js.map +1 -1
  339. package/dist/services/users.d.ts +20 -12
  340. package/dist/services/users.d.ts.map +1 -1
  341. package/dist/services/users.js +24 -51
  342. package/dist/services/users.js.map +1 -1
  343. package/dist/services/webauthn.d.ts +8 -4
  344. package/dist/services/webauthn.d.ts.map +1 -1
  345. package/dist/services/webauthn.js +4 -4
  346. package/dist/services/webauthn.js.map +1 -1
  347. package/dist/services/webhooks.d.ts +20 -20
  348. package/dist/services/workflows/engine.d.ts +4 -2
  349. package/dist/services/workflows/engine.d.ts.map +1 -1
  350. package/dist/services/workflows/engine.js +181 -12
  351. package/dist/services/workflows/engine.js.map +1 -1
  352. package/dist/services/workflows/steps.d.ts +2 -0
  353. package/dist/services/workflows/steps.d.ts.map +1 -1
  354. package/dist/services/workflows/steps.js +8 -75
  355. package/dist/services/workflows/steps.js.map +1 -1
  356. package/dist/setup-server.js.map +1 -1
  357. package/dist/types.d.ts +33 -4
  358. package/dist/types.d.ts.map +1 -1
  359. package/dist/types.js +26 -2
  360. package/dist/types.js.map +1 -1
  361. package/package.json +26 -3
  362. package/dist/tests/errors.test.d.ts +0 -2
  363. package/dist/tests/errors.test.d.ts.map +0 -1
  364. package/dist/tests/errors.test.js +0 -64
  365. package/dist/tests/errors.test.js.map +0 -1
  366. package/dist/tests/events.test.d.ts +0 -2
  367. package/dist/tests/events.test.d.ts.map +0 -1
  368. package/dist/tests/events.test.js +0 -45
  369. package/dist/tests/events.test.js.map +0 -1
  370. package/dist/tests/featureFlags.test.d.ts +0 -2
  371. package/dist/tests/featureFlags.test.d.ts.map +0 -1
  372. package/dist/tests/featureFlags.test.js +0 -24
  373. package/dist/tests/featureFlags.test.js.map +0 -1
  374. package/dist/tests/integration/queue.test.d.ts +0 -2
  375. package/dist/tests/integration/queue.test.d.ts.map +0 -1
  376. package/dist/tests/integration/queue.test.js +0 -49
  377. package/dist/tests/integration/queue.test.js.map +0 -1
  378. package/dist/tests/plugins.test.d.ts +0 -2
  379. package/dist/tests/plugins.test.d.ts.map +0 -1
  380. package/dist/tests/plugins.test.js +0 -56
  381. package/dist/tests/plugins.test.js.map +0 -1
  382. package/dist/tests/result.test.d.ts +0 -2
  383. package/dist/tests/result.test.d.ts.map +0 -1
  384. package/dist/tests/result.test.js +0 -46
  385. package/dist/tests/result.test.js.map +0 -1
  386. package/dist/tests/setup/configWriter.test.d.ts +0 -2
  387. package/dist/tests/setup/configWriter.test.d.ts.map +0 -1
  388. package/dist/tests/setup/configWriter.test.js +0 -55
  389. package/dist/tests/setup/configWriter.test.js.map +0 -1
  390. package/dist/tests/setup/validation.test.d.ts +0 -2
  391. package/dist/tests/setup/validation.test.d.ts.map +0 -1
  392. package/dist/tests/setup/validation.test.js +0 -26
  393. package/dist/tests/setup/validation.test.js.map +0 -1
  394. package/dist/tests/smoke.test.d.ts +0 -2
  395. package/dist/tests/smoke.test.d.ts.map +0 -1
  396. package/dist/tests/smoke.test.js +0 -124
  397. package/dist/tests/smoke.test.js.map +0 -1
  398. package/src/cli.ts +0 -172
  399. package/src/config.ts +0 -143
  400. package/src/container.ts +0 -48
  401. package/src/db/index.ts +0 -53
  402. package/src/db/migrations/0000_skinny_ben_parker.sql +0 -553
  403. package/src/db/migrations/0001_brief_joseph.sql +0 -10
  404. package/src/db/migrations/0002_add_account_lockout.sql +0 -3
  405. package/src/db/migrations/0003_steep_rachel_grey.sql +0 -1
  406. package/src/db/migrations/0004_clever_bill_hollister.sql +0 -20
  407. package/src/db/migrations/0005_steep_the_call.sql +0 -2
  408. package/src/db/migrations/0006_ambitious_boomer.sql +0 -17
  409. package/src/db/migrations/0007_greedy_vindicator.sql +0 -40
  410. package/src/db/migrations/meta/0000_snapshot.json +0 -2964
  411. package/src/db/migrations/meta/0001_snapshot.json +0 -3031
  412. package/src/db/migrations/meta/0003_snapshot.json +0 -3051
  413. package/src/db/migrations/meta/_journal.json +0 -62
  414. package/src/db/schema.ts +0 -676
  415. package/src/db/seed.ts +0 -146
  416. package/src/di.ts +0 -81
  417. package/src/index.ts +0 -326
  418. package/src/lib/errors.ts +0 -139
  419. package/src/lib/result.ts +0 -44
  420. package/src/lib/webhookSignature.ts +0 -41
  421. package/src/plugins/appContext.ts +0 -76
  422. package/src/plugins/audit.ts +0 -42
  423. package/src/plugins/auth.ts +0 -230
  424. package/src/plugins/metrics.ts +0 -41
  425. package/src/plugins/mtls.ts +0 -94
  426. package/src/plugins/permissions.ts +0 -29
  427. package/src/plugins/rateLimit.ts +0 -133
  428. package/src/plugins/requestLogger.ts +0 -25
  429. package/src/plugins/tracing.ts +0 -22
  430. package/src/repositories/apiKey.ts +0 -158
  431. package/src/repositories/application.ts +0 -70
  432. package/src/repositories/audit.ts +0 -32
  433. package/src/repositories/index.ts +0 -9
  434. package/src/repositories/oidcConnection.ts +0 -111
  435. package/src/repositories/organization.ts +0 -167
  436. package/src/repositories/permission.ts +0 -153
  437. package/src/repositories/samlConnection.ts +0 -96
  438. package/src/repositories/session.ts +0 -86
  439. package/src/repositories/types.ts +0 -134
  440. package/src/repositories/user.ts +0 -161
  441. package/src/routes/admin/billing.ts +0 -48
  442. package/src/routes/admin/helpers.ts +0 -50
  443. package/src/routes/admin/index.ts +0 -16
  444. package/src/routes/admin/organizations.ts +0 -299
  445. package/src/routes/admin/permissions.ts +0 -80
  446. package/src/routes/admin/platform.ts +0 -335
  447. package/src/routes/admin/sso.ts +0 -138
  448. package/src/routes/admin/webhooks.ts +0 -73
  449. package/src/routes/admin.ts +0 -1
  450. package/src/routes/apiKeys.ts +0 -60
  451. package/src/routes/auth.ts +0 -214
  452. package/src/routes/authz.ts +0 -38
  453. package/src/routes/config.ts +0 -67
  454. package/src/routes/emailVerification.ts +0 -73
  455. package/src/routes/federation.ts +0 -156
  456. package/src/routes/helpers.ts +0 -17
  457. package/src/routes/magicLinks.ts +0 -107
  458. package/src/routes/oauth.ts +0 -167
  459. package/src/routes/oauth2.ts +0 -284
  460. package/src/routes/oidcEnterprise.ts +0 -166
  461. package/src/routes/password.ts +0 -90
  462. package/src/routes/profile.ts +0 -44
  463. package/src/routes/saml.ts +0 -209
  464. package/src/routes/scim.ts +0 -199
  465. package/src/routes/sdk.ts +0 -141
  466. package/src/routes/serviceAccounts.ts +0 -124
  467. package/src/routes/sessions.ts +0 -62
  468. package/src/routes/setup.ts +0 -315
  469. package/src/routes/smsOtp.ts +0 -67
  470. package/src/routes/totp.ts +0 -122
  471. package/src/routes/webauthn.ts +0 -147
  472. package/src/routes/workflows.ts +0 -157
  473. package/src/sdk/index.ts +0 -152
  474. package/src/sdk/types.ts +0 -54
  475. package/src/services/anomalyDetection.ts +0 -73
  476. package/src/services/application/authentication.ts +0 -75
  477. package/src/services/application/identity.ts +0 -56
  478. package/src/services/application/index.ts +0 -3
  479. package/src/services/application/organization.ts +0 -77
  480. package/src/services/applications.ts +0 -89
  481. package/src/services/audit.ts +0 -41
  482. package/src/services/auditExport.ts +0 -59
  483. package/src/services/billing.ts +0 -87
  484. package/src/services/cache.ts +0 -131
  485. package/src/services/configuration/profiles.ts +0 -115
  486. package/src/services/configuration.ts +0 -120
  487. package/src/services/connectors/google.ts +0 -92
  488. package/src/services/connectors/oidc.ts +0 -111
  489. package/src/services/connectors/registry.ts +0 -124
  490. package/src/services/connectors/types.ts +0 -38
  491. package/src/services/connectors/zitadel.ts +0 -77
  492. package/src/services/devices.ts +0 -76
  493. package/src/services/domain/authentication.ts +0 -279
  494. package/src/services/domain/authorization.ts +0 -49
  495. package/src/services/domain/identity.ts +0 -108
  496. package/src/services/domain/index.ts +0 -4
  497. package/src/services/domain/organization.ts +0 -114
  498. package/src/services/email.ts +0 -167
  499. package/src/services/emailVerification.ts +0 -75
  500. package/src/services/enterpriseSso.ts +0 -58
  501. package/src/services/events/bus.ts +0 -43
  502. package/src/services/events/subscribers/anomaly.ts +0 -13
  503. package/src/services/events/subscribers/auditLog.ts +0 -21
  504. package/src/services/events/subscribers/webhook.ts +0 -39
  505. package/src/services/events/subscribers/webhookDispatch.ts +0 -14
  506. package/src/services/events/types.ts +0 -106
  507. package/src/services/events/validate.ts +0 -118
  508. package/src/services/featureFlags.ts +0 -90
  509. package/src/services/features.ts +0 -22
  510. package/src/services/federation.ts +0 -30
  511. package/src/services/hibp.ts +0 -28
  512. package/src/services/ipControls.ts +0 -99
  513. package/src/services/magicLinks.ts +0 -51
  514. package/src/services/oauth2.ts +0 -216
  515. package/src/services/organizations.ts +0 -171
  516. package/src/services/permissions.ts +0 -146
  517. package/src/services/plugins/registry.ts +0 -242
  518. package/src/services/plugins/types.ts +0 -98
  519. package/src/services/queue/bullmq.ts +0 -104
  520. package/src/services/queue/inProcess.ts +0 -75
  521. package/src/services/queue/index.ts +0 -19
  522. package/src/services/queue/types.ts +0 -26
  523. package/src/services/redis.ts +0 -16
  524. package/src/services/secrets/awsKms.ts +0 -97
  525. package/src/services/secrets/azureKeyVault.ts +0 -171
  526. package/src/services/secrets/database.ts +0 -166
  527. package/src/services/secrets/environment.ts +0 -117
  528. package/src/services/secrets/index.ts +0 -50
  529. package/src/services/secrets/password.ts +0 -73
  530. package/src/services/secrets/provider.ts +0 -36
  531. package/src/services/secrets/vault.ts +0 -149
  532. package/src/services/secrets.ts +0 -2
  533. package/src/services/serviceAccounts.ts +0 -74
  534. package/src/services/setup/configWriter.ts +0 -200
  535. package/src/services/setup/diagnostics.ts +0 -76
  536. package/src/services/setup/token.ts +0 -42
  537. package/src/services/setup/types.ts +0 -40
  538. package/src/services/setup/validation.ts +0 -205
  539. package/src/services/sms.ts +0 -81
  540. package/src/services/smsOtp.ts +0 -74
  541. package/src/services/tokens.ts +0 -287
  542. package/src/services/totp.ts +0 -113
  543. package/src/services/users.ts +0 -239
  544. package/src/services/webauthn.ts +0 -193
  545. package/src/services/webhooks.ts +0 -190
  546. package/src/services/workflows/engine.ts +0 -125
  547. package/src/services/workflows/steps.ts +0 -129
  548. package/src/services/zitadel.ts +0 -277
  549. package/src/setup-server.ts +0 -71
  550. package/src/tests/errors.test.ts +0 -74
  551. package/src/tests/events.test.ts +0 -52
  552. package/src/tests/featureFlags.test.ts +0 -26
  553. package/src/tests/integration/queue.test.ts +0 -54
  554. package/src/tests/plugins.test.ts +0 -73
  555. package/src/tests/result.test.ts +0 -52
  556. package/src/tests/setup/configWriter.test.ts +0 -69
  557. package/src/tests/setup/validation.test.ts +0 -29
  558. package/src/tests/smoke.test.ts +0 -136
  559. package/src/types.ts +0 -70
@@ -1,16 +1,111 @@
1
1
  import { z } from "zod";
2
- import { eq, and } from "drizzle-orm";
3
- import { db } from "../db/index.js";
4
- import { orgMemberships, users } from "../db/schema.js";
5
- const ScimUserSchema = z.object({
6
- userName: z.string().email(),
7
- name: z.object({ givenName: z.string().optional(), familyName: z.string().optional() }).optional(),
2
+ import { hashScimToken } from "../services/scimCredentials.js";
3
+ import { rateLimit, isAllowed, clientAddress } from "../plugins/rateLimit.js";
4
+ import { config } from "../config.js";
5
+ import { emit } from "../services/events/bus.js";
6
+ const SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User";
7
+ const SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group";
8
+ const SCIM_LIST_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
9
+ const SCIM_ERROR_SCHEMA = "urn:ietf:params:scim:api:messages:2.0:Error";
10
+ /**
11
+ * SCIM 2.0 provisioning.
12
+ *
13
+ * Every request resolves to exactly one organization through its bearer
14
+ * credential, and every read and write below is filtered by that organization.
15
+ * There is no global SCIM configuration and no code path that looks a user or
16
+ * group up by id alone.
17
+ *
18
+ * Tenant rule for shared users: a user row is global, so an organization may
19
+ * only mutate global attributes for a user whose *only* membership is that
20
+ * organization. For a user who belongs elsewhere, SCIM manages the membership
21
+ * and refuses to touch the shared account. See `removeFromOrg`.
22
+ */
23
+ const UserBodySchema = z.object({
24
+ userName: z.string().email().max(255),
25
+ name: z
26
+ .object({ givenName: z.string().max(255).optional(), familyName: z.string().max(255).optional() })
27
+ .optional(),
8
28
  emails: z.array(z.object({ value: z.string().email(), primary: z.boolean().optional() })).optional(),
9
29
  active: z.boolean().optional(),
30
+ externalId: z.string().max(255).optional(),
10
31
  });
32
+ /** PATCH with `active` / `name.*` / `userName` operations, per RFC 7644 §3.5.2. */
33
+ const UserPatchSchema = z.object({
34
+ schemas: z.array(z.string()).optional(),
35
+ Operations: z
36
+ .array(z.object({
37
+ op: z.enum(["add", "replace", "remove"]),
38
+ path: z.string().optional(),
39
+ value: z.unknown().optional(),
40
+ }))
41
+ .min(1),
42
+ });
43
+ const GroupBodySchema = z.object({
44
+ displayName: z.string().min(1).max(255),
45
+ description: z.string().max(1024).optional(),
46
+ externalId: z.string().max(255).optional(),
47
+ members: z
48
+ .array(z.object({ value: z.string().min(1) }))
49
+ .optional(),
50
+ });
51
+ const GroupPatchSchema = z.object({
52
+ Operations: z
53
+ .array(z.object({
54
+ op: z.enum(["add", "replace", "remove"]),
55
+ path: z.string().optional(),
56
+ value: z.unknown().optional(),
57
+ }))
58
+ .min(1),
59
+ });
60
+ const MemberBodySchema = z.object({
61
+ value: z.string().min(1),
62
+ });
63
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
64
+ /**
65
+ * Read a path id, rejecting anything that is not a uuid before it reaches the
66
+ * database. A malformed id is reported as "not found" rather than "bad
67
+ * request", so the endpoint does not advertise how it validates identifiers.
68
+ */
69
+ function pathId(request, key = "id") {
70
+ const value = request.params[key];
71
+ if (!value || !UUID_PATTERN.test(value))
72
+ throw new ScimNotFound("Resource not found");
73
+ return value;
74
+ }
75
+ const ListQuerySchema = z.object({
76
+ filter: z.string().optional(),
77
+ startIndex: z.coerce.number().int().min(1).optional(),
78
+ count: z.coerce.number().int().min(0).max(500).optional(),
79
+ });
80
+ /**
81
+ * Only attributes each handler genuinely implements. Adding one here without
82
+ * implementing it downstream would make clients believe a filter was honoured
83
+ * when it had been silently dropped.
84
+ */
85
+ const USER_FILTER_ATTRIBUTES = new Set(["username"]);
86
+ const GROUP_FILTER_ATTRIBUTES = new Set(["displayname", "externalid"]);
87
+ /**
88
+ * Parse the single-attribute `eq` form that IdMs actually send, e.g.
89
+ * `userName eq "a@b.com"`.
90
+ *
91
+ * Anything outside `supported` — including operators this parser does not
92
+ * understand — is rejected, so a client is never handed an unfiltered listing
93
+ * while believing its filter was applied.
94
+ */
95
+ export function parseEqFilter(filter, supported) {
96
+ if (!filter)
97
+ return { attribute: "", value: "" };
98
+ const match = /^\s*([A-Za-z][A-Za-z0-9_]*)\s+eq\s+"?([^"]*)"?\s*$/i.exec(filter);
99
+ if (!match)
100
+ return "unsupported";
101
+ const attribute = match[1].toLowerCase();
102
+ if (!supported.has(attribute))
103
+ return "unsupported";
104
+ return { attribute, value: match[2] };
105
+ }
11
106
  function scimUserResponse(user) {
12
107
  return {
13
- schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
108
+ schemas: [SCIM_USER_SCHEMA],
14
109
  id: user.id,
15
110
  userName: user.email,
16
111
  name: {
@@ -18,66 +113,254 @@ function scimUserResponse(user) {
18
113
  familyName: user.name?.split(" ").slice(1).join(" ") || "",
19
114
  },
20
115
  emails: [{ value: user.email, primary: true }],
21
- active: true,
22
- meta: {
23
- resourceType: "User",
24
- },
116
+ active: user.isActive !== false,
117
+ meta: { resourceType: "User" },
25
118
  };
26
119
  }
27
- function scimGroupResponse(group) {
120
+ function scimGroupResponse(group, members) {
28
121
  return {
29
- schemas: ["urn:ietf:params:scim:schemas:core:2.0:Group"],
122
+ schemas: [SCIM_GROUP_SCHEMA],
30
123
  id: group.id,
31
124
  displayName: group.displayName,
32
- members: group.members.map((m) => ({
33
- value: m.value,
34
- display: m.display,
35
- $ref: `Users/${m.value}`,
36
- })),
37
- meta: {
38
- resourceType: "Group",
39
- },
125
+ ...(group.description ? { description: group.description } : {}),
126
+ ...(group.externalId ? { externalId: group.externalId } : {}),
127
+ members: members.map((m) => ({ value: m.value, display: m.display, $ref: `Users/${m.value}` })),
128
+ meta: { resourceType: "Group" },
40
129
  };
41
130
  }
42
- function scimError(status, detail) {
131
+ function scimError(status, detail, scimType) {
43
132
  return {
44
- schemas: ["urn:ietf:params:scim:api:messages:2.0:Error"],
133
+ schemas: [SCIM_ERROR_SCHEMA],
45
134
  status: String(status),
135
+ ...(scimType ? { scimType } : {}),
46
136
  detail,
47
137
  };
48
138
  }
139
+ /** Thrown for a cross-tenant or otherwise unauthorized target; surfaces as 404. */
140
+ class ScimNotFound extends Error {
141
+ }
142
+ /** Thrown when the target is real but this credential may not act on it. */
143
+ class ScimForbidden extends Error {
144
+ scimType;
145
+ constructor(scimType, message) {
146
+ super(message);
147
+ this.scimType = scimType;
148
+ }
149
+ }
49
150
  export default async function scimRoutes(app) {
151
+ // SCIM clients expect a SCIM `Error` object for every failure. Without this
152
+ // scoped handler a ZodError or a driver error would escape as a generic
153
+ // Fastify payload, and outside production the raw message would be returned.
154
+ app.setErrorHandler((error, _request, reply) => {
155
+ if (error instanceof ScimNotFound) {
156
+ return reply.status(404).send(scimError(404, error.message));
157
+ }
158
+ if (error instanceof ScimForbidden) {
159
+ return reply.status(409).send(scimError(409, error.message, error.scimType));
160
+ }
161
+ if (error && typeof error === "object" && error.name === "ZodError") {
162
+ return reply.status(400).send(scimError(400, "Invalid request", "invalidValue"));
163
+ }
164
+ // Postgres uuid cast failures are caused by a malformed path parameter.
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ if (message.includes("invalid input syntax for type uuid")) {
167
+ return reply.status(404).send(scimError(404, "Resource not found"));
168
+ }
169
+ console.error("[scim] unhandled error:", error);
170
+ return reply.status(500).send(scimError(500, "Internal error"));
171
+ });
172
+ /**
173
+ * Authenticate the credential and pin the request to one organization.
174
+ *
175
+ * The presented token is hashed and looked up by digest, so the comparison
176
+ * happens inside the database index rather than in application code and
177
+ * carries no timing signal about the secret.
178
+ */
50
179
  app.addHook("onRequest", async (request, reply) => {
180
+ // This hook runs before the per-credential limiter, and it emits audit and
181
+ // webhook events on failure. Unbounded unauthenticated traffic would
182
+ // therefore amplify writes, so budget it by client address first.
183
+ const guard = await isAllowed(`scim-auth:${clientAddress(request)}`, config.SCIM_AUTH_FAILURE_MAX, config.SCIM_AUTH_FAILURE_WINDOW_SECONDS);
184
+ if (!guard) {
185
+ return reply
186
+ .header("Retry-After", String(config.SCIM_AUTH_FAILURE_WINDOW_SECONDS))
187
+ .status(429)
188
+ .send(scimError(429, "Too many requests"));
189
+ }
51
190
  const auth = request.headers.authorization;
52
- const expected = process.env.SCIM_BEARER_TOKEN;
53
- if (!expected) {
54
- return reply.status(501).send(scimError(501, "SCIM not configured"));
191
+ if (!auth || !auth.startsWith("Bearer ")) {
192
+ await emit({ type: "scim_authentication_failed", payload: { reason: "missing_bearer" } });
193
+ return reply.status(401).send(scimError(401, "Unauthorized"));
194
+ }
195
+ const presented = auth.slice(7).trim();
196
+ if (presented.length < 16) {
197
+ await emit({ type: "scim_authentication_failed", payload: { reason: "malformed_token" } });
198
+ return reply.status(401).send(scimError(401, "Unauthorized"));
55
199
  }
56
- if (!auth || !auth.startsWith("Bearer ") || auth.slice(7) !== expected) {
200
+ const now = new Date();
201
+ const connection = await app.container.scimConnectionRepository.findByTokenHash(hashScimToken(presented), now);
202
+ if (!connection) {
203
+ await emit({ type: "scim_authentication_failed", payload: { reason: "unknown_or_revoked_token" } });
57
204
  return reply.status(401).send(scimError(401, "Unauthorized"));
58
205
  }
206
+ if (connection.expiresAt && connection.expiresAt <= now) {
207
+ await emit({
208
+ type: "scim_authentication_failed",
209
+ payload: { orgId: connection.orgId, connectionId: connection.id, reason: "expired" },
210
+ });
211
+ return reply.status(401).send(scimError(401, "Unauthorized"));
212
+ }
213
+ const organization = await app.container.organizationRepository.findById(connection.orgId);
214
+ if (!organization) {
215
+ await emit({
216
+ type: "scim_authentication_failed",
217
+ payload: { orgId: connection.orgId, connectionId: connection.id, reason: "org_missing" },
218
+ });
219
+ return reply.status(503).send(scimError(503, "SCIM organization not found"));
220
+ }
221
+ request.state.scimOrgId = connection.orgId;
222
+ request.state.scimConnectionId = connection.id;
223
+ request.state.org = organization;
224
+ await app.container.scimConnectionRepository.touch(connection.id, now);
225
+ });
226
+ // A dedicated budget, keyed on the credential, applied to every SCIM route.
227
+ const scimRateLimit = rateLimit({
228
+ keyPrefix: "scim",
229
+ maxAttempts: config.SCIM_RATE_LIMIT_MAX,
230
+ windowSeconds: config.SCIM_RATE_LIMIT_WINDOW_SECONDS,
231
+ keyFrom: (request) => request.state.scimConnectionId ?? request.ip,
59
232
  });
60
- app.get("/scim/v2/Users", async () => {
61
- const allUsers = await app.container.userRepository.listAll();
233
+ /**
234
+ * Resolve a target inside the credential's organization, or fail closed.
235
+ *
236
+ * `forWrite` additionally refuses accounts SCIM must never change. Reading a
237
+ * platform owner who happens to be a member of the organization is
238
+ * legitimate; mutating one is not.
239
+ */
240
+ async function requireOrgUser(request, userId, { forWrite = false } = {}) {
241
+ const orgId = request.state.scimOrgId;
242
+ const user = await app.container.userRepository.findByIdInOrg(orgId, userId);
243
+ if (!user) {
244
+ // Deliberately the same response for "does not exist" and "belongs to
245
+ // another organization", so the endpoint is not a tenant oracle.
246
+ throw new ScimNotFound("User not found");
247
+ }
248
+ if (!forWrite)
249
+ return user;
250
+ if (user.role === "owner") {
251
+ throw new ScimForbidden("mutability", "Platform owners cannot be modified through SCIM");
252
+ }
253
+ if (user.accountReviewRequired) {
254
+ throw new ScimForbidden("mutability", "This account is pending platform review and cannot be modified through SCIM");
255
+ }
256
+ return user;
257
+ }
258
+ /**
259
+ * A user row is global. Writing global attributes for someone who also
260
+ * belongs to another organization would change that other tenant's view
261
+ * without any authorization from it, so SCIM refuses.
262
+ */
263
+ async function assertSoleMembership(request, user, action) {
264
+ const orgIds = await app.container.userRepository.listOrgIdsForUser(user.id);
265
+ if (orgIds.length > 1) {
266
+ await emit({
267
+ type: "scim_access_denied",
268
+ payload: {
269
+ orgId: request.state.scimOrgId,
270
+ targetUserId: user.id,
271
+ reason: "shared_user",
272
+ action,
273
+ },
274
+ });
275
+ throw new ScimForbidden("mutability", "This user also belongs to another organization; remove the membership from this organization instead of modifying the shared account");
276
+ }
277
+ }
278
+ function handleError(reply, error) {
279
+ if (error instanceof ScimNotFound) {
280
+ return reply.status(404).send(scimError(404, error.message));
281
+ }
282
+ if (error instanceof ScimForbidden) {
283
+ return reply.status(409).send(scimError(409, error.message, error.scimType));
284
+ }
285
+ if (error && typeof error === "object" && error.name === "ZodError") {
286
+ return reply.status(400).send(scimError(400, "Invalid request body"));
287
+ }
288
+ throw error;
289
+ }
290
+ // --- Users -------------------------------------------------------------
291
+ app.get("/scim/v2/Users", { preHandler: [scimRateLimit] }, async (request, reply) => {
292
+ const query = ListQuerySchema.parse(request.query ?? {});
293
+ const filter = parseEqFilter(query.filter, USER_FILTER_ATTRIBUTES);
294
+ if (filter === "unsupported") {
295
+ return reply.status(400).send(scimError(400, "Unsupported filter expression", "invalidFilter"));
296
+ }
297
+ const all = (await app.container.userRepository.listByOrg(request.state.scimOrgId)).filter((u) => u.isActive);
298
+ const matching = filter.attribute === "username"
299
+ ? all.filter((u) => u.email.toLowerCase() === filter.value.toLowerCase())
300
+ : all;
301
+ const start = (query.startIndex ?? 1) - 1;
302
+ const count = query.count ?? matching.length;
62
303
  return {
63
- schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
64
- totalResults: allUsers.length,
65
- Resources: allUsers.map(scimUserResponse),
304
+ schemas: [SCIM_LIST_SCHEMA],
305
+ totalResults: matching.length,
306
+ startIndex: start + 1,
307
+ itemsPerPage: Math.max(0, Math.min(count, matching.length - start)),
308
+ Resources: matching.slice(start, start + count).map(scimUserResponse),
66
309
  };
67
310
  });
68
- app.get("/scim/v2/Users/:userId", async (request, reply) => {
69
- const { userId } = request.params;
70
- const user = await app.container.userRepository.findById(userId);
71
- if (!user) {
72
- return reply.status(404).send(scimError(404, "User not found"));
311
+ app.get("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
312
+ const userId = pathId(request, "userId");
313
+ try {
314
+ return scimUserResponse(await requireOrgUser(request, userId));
315
+ }
316
+ catch (error) {
317
+ return handleError(reply, error);
73
318
  }
74
- return scimUserResponse(user);
75
319
  });
76
- app.post("/scim/v2/Users", async (request, reply) => {
77
- const body = ScimUserSchema.parse(request.body);
320
+ app.post("/scim/v2/Users", { preHandler: [scimRateLimit] }, async (request, reply) => {
321
+ const body = UserBodySchema.parse(request.body);
322
+ const orgId = request.state.scimOrgId;
78
323
  const email = body.userName.toLowerCase().trim();
79
- let user = await app.container.userRepository.findByEmail(email);
324
+ // Resolve the target through the organization. `userName` is globally
325
+ // unique, so a user that exists elsewhere has to be classified before we
326
+ // can act: one with no memberships left was removed from *this*
327
+ // organization and can be re-provisioned, while one that still belongs
328
+ // to another organization must not be adopted.
329
+ const global = await app.container.userRepository.findByEmail(email);
330
+ const existing = global
331
+ ? await app.container.userRepository.findByIdInOrg(orgId, global.id)
332
+ : undefined;
333
+ let user = existing;
80
334
  let isNewUser = false;
335
+ let isReattached = false;
336
+ if (global && !existing) {
337
+ const otherOrgIds = await app.container.userRepository.listOrgIdsForUser(global.id);
338
+ if (otherOrgIds.length > 0) {
339
+ await emit({
340
+ type: "scim_access_denied",
341
+ payload: { orgId, reason: "user_exists_outside_organization" },
342
+ });
343
+ // Deliberately does not say which organization holds the account.
344
+ return reply
345
+ .status(409)
346
+ .send(scimError(409, "A user with that userName already exists", "uniqueness"));
347
+ }
348
+ // No memberships remain anywhere: this user was removed from this
349
+ // organization, so re-provisioning must be able to bring them back
350
+ // rather than 409 forever.
351
+ if (global.accountReviewRequired) {
352
+ return reply.status(409).send(scimError(409, "Account review must be completed by a platform owner", "mutability"));
353
+ }
354
+ if (global.role === "owner") {
355
+ return reply.status(409).send(scimError(409, "Platform owners cannot be provisioned through SCIM", "mutability"));
356
+ }
357
+ await app.container.organizationRepository.addMembership({ orgId, userId: global.id, role: "member" });
358
+ if (!global.isActive) {
359
+ await app.container.userRepository.updateInOrg(orgId, global.id, { isActive: true });
360
+ }
361
+ user = (await app.container.userRepository.findByIdInOrg(orgId, global.id));
362
+ isReattached = true;
363
+ }
81
364
  if (!user) {
82
365
  const base = email.split("@")[0];
83
366
  const username = await app.container.userRepository.ensureUniqueUsername(base);
@@ -91,85 +374,577 @@ export default async function scimRoutes(app) {
91
374
  provider: "scim",
92
375
  emailVerified: true,
93
376
  });
377
+ await app.container.organizationRepository.addMembership({
378
+ orgId,
379
+ userId: user.id,
380
+ role: "member",
381
+ });
94
382
  isNewUser = true;
95
383
  }
96
- await request.audit(isNewUser ? "scim_user_created" : "scim_user_updated", { userId: user.id, email: user.email });
384
+ if (!isNewUser && !isReattached) {
385
+ // IdMs reconcile with POST, so an existing member must be updated too.
386
+ await assertSoleMembership(request, user, "update");
387
+ user =
388
+ (await app.container.userRepository.updateInOrg(orgId, user.id, {
389
+ ...(body.name
390
+ ? { name: `${body.name.givenName || ""} ${body.name.familyName || ""}`.trim() }
391
+ : {}),
392
+ ...(body.active === undefined ? {} : { isActive: body.active }),
393
+ })) ?? user;
394
+ }
395
+ if (body.active === false && user.isActive) {
396
+ const removed = await app.container.userRepository.removeFromOrg(orgId, user.id);
397
+ if (!removed || removed.outcome === "not_found") {
398
+ return reply.status(404).send(scimError(404, "User not found"));
399
+ }
400
+ if (removed.outcome === "last_owner") {
401
+ return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
402
+ }
403
+ user = { ...user, isActive: false };
404
+ }
405
+ else if (body.active === true && !user.isActive) {
406
+ if (user.accountReviewRequired) {
407
+ return reply.status(409).send(scimError(409, "Account review must be completed by a platform owner", "mutability"));
408
+ }
409
+ // Reactivating a shared account would hand someone access to the other
410
+ // organizations they belong to, without those organizations' consent.
411
+ await assertSoleMembership(request, user, "reactivate");
412
+ user = (await app.container.userRepository.updateInOrg(orgId, user.id, { isActive: true })) ?? user;
413
+ }
414
+ await request.audit(isNewUser ? "scim_user_created" : "scim_user_updated", {
415
+ targetUserId: user.id,
416
+ email: user.email,
417
+ actorType: "scim",
418
+ ...(isReattached ? { reattached: true } : {}),
419
+ scimConnectionId: request.state.scimConnectionId,
420
+ scimOrgId: orgId,
421
+ });
97
422
  return reply.status(201).send(scimUserResponse(user));
98
423
  });
99
- app.put("/scim/v2/Users/:userId", async (request, reply) => {
100
- const { userId } = request.params;
101
- const body = ScimUserSchema.parse(request.body);
102
- const updated = await app.container.userRepository.update(userId, {
103
- email: body.userName.toLowerCase().trim(),
104
- name: body.name
105
- ? `${body.name.givenName || ""} ${body.name.familyName || ""}`.trim()
106
- : undefined,
107
- });
108
- if (!updated) {
109
- return reply.status(404).send(scimError(404, "User not found"));
110
- }
111
- await request.audit("scim_user_updated", { userId: updated.id, email: updated.email });
112
- return scimUserResponse(updated);
113
- });
114
- app.delete("/scim/v2/Users/:userId", async (request, reply) => {
115
- const { userId } = request.params;
116
- const user = await app.container.userRepository.findById(userId);
117
- await app.container.userRepository.deleteById(userId);
118
- await request.audit("scim_user_deleted", { userId, email: user?.email });
119
- return reply.status(204).send();
120
- });
121
- app.get("/scim/v2/Groups", async () => {
122
- const memberships = await db
123
- .select({ orgId: orgMemberships.orgId, role: orgMemberships.role })
124
- .from(orgMemberships);
125
- const groupMap = new Map();
126
- for (const m of memberships) {
127
- const key = `${m.orgId}:${m.role}`;
128
- if (!groupMap.has(key)) {
129
- groupMap.set(key, { orgId: m.orgId, role: m.role, members: [] });
130
- }
131
- }
132
- const groups = [];
133
- for (const [key, group] of groupMap) {
134
- const org = await app.container.organizationRepository.findById(group.orgId);
135
- const orgName = org?.name || group.orgId;
136
- const members = await db
137
- .select({ userId: orgMemberships.userId, email: users.email })
138
- .from(orgMemberships)
139
- .where(and(eq(orgMemberships.orgId, group.orgId), eq(orgMemberships.role, group.role)))
140
- .innerJoin(users, eq(orgMemberships.userId, users.id));
141
- groups.push({
142
- id: key,
143
- displayName: `${orgName}:${group.role}`,
144
- members: members.map((m) => ({ value: m.userId, display: m.email })),
424
+ app.put("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
425
+ const userId = pathId(request, "userId");
426
+ const body = UserBodySchema.parse(request.body);
427
+ const orgId = request.state.scimOrgId;
428
+ try {
429
+ const existing = await requireOrgUser(request, userId, { forWrite: true });
430
+ if (body.active === false) {
431
+ const removed = await app.container.userRepository.removeFromOrg(orgId, existing.id);
432
+ if (!removed || removed.outcome === "not_found") {
433
+ return reply.status(404).send(scimError(404, "User not found"));
434
+ }
435
+ if (removed.outcome === "last_owner") {
436
+ return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
437
+ }
438
+ // The membership is gone, so re-reading through the organization
439
+ // would 404. Report the deactivation from the resolved user instead.
440
+ await request.audit("scim_user_updated", {
441
+ targetUserId: existing.id,
442
+ email: existing.email,
443
+ actorType: "scim",
444
+ operation: "put",
445
+ outcome: removed.outcome,
446
+ scimConnectionId: request.state.scimConnectionId,
447
+ scimOrgId: orgId,
448
+ });
449
+ return scimUserResponse({ ...existing, isActive: false });
450
+ }
451
+ {
452
+ await assertSoleMembership(request, existing, "update");
453
+ const updated = await app.container.userRepository.updateInOrg(orgId, existing.id, {
454
+ email: body.userName.toLowerCase().trim(),
455
+ name: body.name
456
+ ? `${body.name.givenName || ""} ${body.name.familyName || ""}`.trim()
457
+ : undefined,
458
+ isActive: body.active,
459
+ });
460
+ if (!updated)
461
+ return reply.status(404).send(scimError(404, "User not found"));
462
+ }
463
+ const updated = await app.container.userRepository.findByIdInOrg(orgId, userId);
464
+ if (!updated)
465
+ return reply.status(404).send(scimError(404, "User not found"));
466
+ await request.audit("scim_user_updated", {
467
+ targetUserId: updated.id,
468
+ email: updated.email,
469
+ actorType: "scim",
470
+ scimConnectionId: request.state.scimConnectionId,
471
+ scimOrgId: orgId,
472
+ });
473
+ return scimUserResponse(updated);
474
+ }
475
+ catch (error) {
476
+ return handleError(reply, error);
477
+ }
478
+ });
479
+ app.patch("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
480
+ const userId = pathId(request, "userId");
481
+ const body = UserPatchSchema.parse(request.body);
482
+ const orgId = request.state.scimOrgId;
483
+ try {
484
+ const existing = await requireOrgUser(request, userId, { forWrite: true });
485
+ const patch = collapsePatch(body.Operations);
486
+ const nameValue = patch.name;
487
+ const nameParts = nameValue && typeof nameValue === "object" && !Array.isArray(nameValue)
488
+ ? nameValue
489
+ : undefined;
490
+ const name = nameParts
491
+ ? {
492
+ givenName: typeof nameParts.givenName === "string" ? nameParts.givenName : undefined,
493
+ familyName: typeof nameParts.familyName === "string" ? nameParts.familyName : undefined,
494
+ }
495
+ : undefined;
496
+ const activePatch = typeof patch.active === "boolean" ? patch.active : undefined;
497
+ if (activePatch === false) {
498
+ const removed = await app.container.userRepository.removeFromOrg(orgId, existing.id);
499
+ if (!removed || removed.outcome === "not_found") {
500
+ return reply.status(404).send(scimError(404, "User not found"));
501
+ }
502
+ if (removed.outcome === "last_owner") {
503
+ return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
504
+ }
505
+ await request.audit("scim_user_updated", {
506
+ targetUserId: existing.id,
507
+ email: existing.email,
508
+ actorType: "scim",
509
+ operation: "patch",
510
+ outcome: removed.outcome,
511
+ scimConnectionId: request.state.scimConnectionId,
512
+ scimOrgId: orgId,
513
+ });
514
+ return scimUserResponse({ ...existing, isActive: false });
515
+ }
516
+ else {
517
+ await assertSoleMembership(request, existing, "patch");
518
+ const updated = await app.container.userRepository.updateInOrg(orgId, existing.id, {
519
+ ...(patch.userName ? { email: String(patch.userName).toLowerCase().trim() } : {}),
520
+ ...(name ? { name: `${name.givenName || ""} ${name.familyName || ""}`.trim() } : {}),
521
+ ...(activePatch === undefined ? {} : { isActive: activePatch }),
522
+ });
523
+ if (!updated)
524
+ return reply.status(404).send(scimError(404, "User not found"));
525
+ }
526
+ const updated = await app.container.userRepository.findByIdInOrg(orgId, userId);
527
+ if (!updated)
528
+ return reply.status(404).send(scimError(404, "User not found"));
529
+ await request.audit("scim_user_updated", {
530
+ targetUserId: updated.id,
531
+ email: updated.email,
532
+ actorType: "scim",
533
+ operation: "patch",
534
+ scimConnectionId: request.state.scimConnectionId,
535
+ scimOrgId: orgId,
145
536
  });
537
+ return scimUserResponse(updated);
146
538
  }
539
+ catch (error) {
540
+ return handleError(reply, error);
541
+ }
542
+ });
543
+ app.delete("/scim/v2/Users/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
544
+ const userId = pathId(request, "userId");
545
+ const orgId = request.state.scimOrgId;
546
+ try {
547
+ const user = await requireOrgUser(request, userId, { forWrite: true });
548
+ const result = await app.container.userRepository.removeFromOrg(orgId, user.id);
549
+ if (!result || result.outcome === "not_found") {
550
+ return reply.status(404).send(scimError(404, "User not found"));
551
+ }
552
+ if (result.outcome === "last_owner") {
553
+ return reply.status(409).send(scimError(409, "The last platform owner cannot be deactivated", "mutability"));
554
+ }
555
+ await request.audit("scim_user_deleted", {
556
+ targetUserId: user.id,
557
+ email: user.email,
558
+ actorType: "scim",
559
+ outcome: result.outcome,
560
+ scimConnectionId: request.state.scimConnectionId,
561
+ scimOrgId: orgId,
562
+ });
563
+ return reply.status(204).send();
564
+ }
565
+ catch (error) {
566
+ return handleError(reply, error);
567
+ }
568
+ });
569
+ // --- Groups ------------------------------------------------------------
570
+ async function loadGroup(request, groupId) {
571
+ const group = await app.container.scimGroupRepository.findByIdInOrg(request.state.scimOrgId, groupId);
572
+ // A group from another organization is reported as missing, not forbidden.
573
+ if (!group)
574
+ throw new ScimNotFound("Group not found");
575
+ return group;
576
+ }
577
+ async function groupWithMembers(request, groupId) {
578
+ const group = await loadGroup(request, groupId);
579
+ const members = await app.container.scimGroupRepository.listMembers(request.state.scimOrgId, group.id);
580
+ return scimGroupResponse(group, members.map((m) => ({ value: m.userId, display: m.email })));
581
+ }
582
+ /** Only users who are members of this credential's organization can join. */
583
+ async function assertMemberOfOrg(request, userId) {
584
+ const user = await app.container.userRepository.findByIdInOrg(request.state.scimOrgId, userId);
585
+ if (!user)
586
+ throw new ScimNotFound("User not found");
587
+ }
588
+ app.get("/scim/v2/Groups", { preHandler: [scimRateLimit] }, async (request, reply) => {
589
+ const query = ListQuerySchema.parse(request.query ?? {});
590
+ const filter = parseEqFilter(query.filter, GROUP_FILTER_ATTRIBUTES);
591
+ if (filter === "unsupported") {
592
+ return reply.status(400).send(scimError(400, "Unsupported filter expression", "invalidFilter"));
593
+ }
594
+ const orgId = request.state.scimOrgId;
595
+ let groups = await app.container.scimGroupRepository.listByOrg(orgId);
596
+ if (filter.attribute === "displayname") {
597
+ const wanted = filter.value.toLowerCase();
598
+ groups = groups.filter((g) => g.displayName.toLowerCase() === wanted);
599
+ }
600
+ else if (filter.attribute === "externalid") {
601
+ const wanted = filter.value;
602
+ groups = groups.filter((g) => g.externalId === wanted);
603
+ }
604
+ const start = (query.startIndex ?? 1) - 1;
605
+ const count = query.count ?? groups.length;
606
+ const page = groups.slice(start, start + count);
607
+ const Resources = await Promise.all(page.map(async (group) => {
608
+ const members = await app.container.scimGroupRepository.listMembers(orgId, group.id);
609
+ return scimGroupResponse(group, members.map((m) => ({ value: m.userId, display: m.email })));
610
+ }));
147
611
  return {
148
- schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
612
+ schemas: [SCIM_LIST_SCHEMA],
149
613
  totalResults: groups.length,
150
- Resources: groups.map(scimGroupResponse),
614
+ startIndex: start + 1,
615
+ itemsPerPage: Resources.length,
616
+ Resources,
151
617
  };
152
618
  });
153
- app.get("/scim/v2/Groups/:groupId", async (request, reply) => {
154
- const { groupId } = request.params;
155
- const [orgId, role] = groupId.split(":");
156
- if (!orgId || !role) {
157
- return reply.status(400).send(scimError(400, "Invalid group ID format. Expected orgId:role"));
158
- }
159
- const org = await app.container.organizationRepository.findById(orgId);
160
- if (!org) {
161
- return reply.status(404).send(scimError(404, "Organization not found"));
162
- }
163
- const members = await db
164
- .select({ userId: orgMemberships.userId, email: users.email })
165
- .from(orgMemberships)
166
- .where(and(eq(orgMemberships.orgId, orgId), eq(orgMemberships.role, role)))
167
- .innerJoin(users, eq(orgMemberships.userId, users.id));
168
- return scimGroupResponse({
169
- id: groupId,
170
- displayName: `${org.name}:${role}`,
171
- members: members.map((m) => ({ value: m.userId, display: m.email })),
172
- });
619
+ app.get("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
620
+ const groupId = pathId(request, "groupId");
621
+ try {
622
+ return await groupWithMembers(request, groupId);
623
+ }
624
+ catch (error) {
625
+ return handleError(reply, error);
626
+ }
627
+ });
628
+ app.post("/scim/v2/Groups", { preHandler: [scimRateLimit] }, async (request, reply) => {
629
+ const body = GroupBodySchema.parse(request.body);
630
+ const orgId = request.state.scimOrgId;
631
+ try {
632
+ const group = await app.container.scimGroupRepository.create({
633
+ orgId,
634
+ displayName: body.displayName,
635
+ description: body.description ?? null,
636
+ externalId: body.externalId ?? null,
637
+ });
638
+ for (const member of body.members ?? []) {
639
+ await assertMemberOfOrg(request, member.value);
640
+ await app.container.scimGroupRepository.addMember(orgId, group.id, member.value);
641
+ }
642
+ await request.audit("scim_group_created", {
643
+ groupId: group.id,
644
+ displayName: group.displayName,
645
+ actorType: "scim",
646
+ scimConnectionId: request.state.scimConnectionId,
647
+ scimOrgId: orgId,
648
+ });
649
+ return reply.status(201).send(await groupWithMembers(request, group.id));
650
+ }
651
+ catch (error) {
652
+ return handleError(reply, error);
653
+ }
173
654
  });
655
+ app.put("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
656
+ const groupId = pathId(request, "groupId");
657
+ const body = GroupBodySchema.parse(request.body);
658
+ const orgId = request.state.scimOrgId;
659
+ try {
660
+ const existing = await loadGroup(request, groupId);
661
+ await app.container.scimGroupRepository.updateInOrg(orgId, existing.id, {
662
+ displayName: body.displayName,
663
+ description: body.description ?? null,
664
+ externalId: body.externalId ?? null,
665
+ });
666
+ if (body.members) {
667
+ for (const member of body.members) {
668
+ await assertMemberOfOrg(request, member.value);
669
+ await app.container.scimGroupRepository.addMember(orgId, existing.id, member.value);
670
+ }
671
+ const current = await app.container.scimGroupRepository.listMembers(orgId, existing.id);
672
+ const wanted = new Set(body.members.map((m) => m.value));
673
+ for (const member of current) {
674
+ if (!wanted.has(member.userId)) {
675
+ await app.container.scimGroupRepository.removeMember(orgId, existing.id, member.userId);
676
+ }
677
+ }
678
+ }
679
+ await request.audit("scim_group_updated", {
680
+ groupId: existing.id,
681
+ actorType: "scim",
682
+ operation: "put",
683
+ scimConnectionId: request.state.scimConnectionId,
684
+ scimOrgId: orgId,
685
+ });
686
+ return await groupWithMembers(request, existing.id);
687
+ }
688
+ catch (error) {
689
+ return handleError(reply, error);
690
+ }
691
+ });
692
+ app.patch("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
693
+ const groupId = pathId(request, "groupId");
694
+ const body = GroupPatchSchema.parse(request.body);
695
+ const orgId = request.state.scimOrgId;
696
+ try {
697
+ const existing = await loadGroup(request, groupId);
698
+ const patch = collapsePatch(body.Operations);
699
+ if (typeof patch.displayName === "string") {
700
+ await app.container.scimGroupRepository.updateInOrg(orgId, existing.id, {
701
+ displayName: patch.displayName,
702
+ });
703
+ }
704
+ if (typeof patch.description === "string") {
705
+ await app.container.scimGroupRepository.updateInOrg(orgId, existing.id, {
706
+ description: patch.description,
707
+ });
708
+ }
709
+ const members = Array.isArray(patch.members) ? patch.members : undefined;
710
+ if (members) {
711
+ for (const entry of members) {
712
+ const value = typeof entry === "string" ? entry : entry?.value;
713
+ if (typeof value !== "string")
714
+ continue;
715
+ await assertMemberOfOrg(request, value);
716
+ await app.container.scimGroupRepository.addMember(orgId, existing.id, value);
717
+ }
718
+ }
719
+ const removeMembers = Array.isArray(patch["members.remove"])
720
+ ? patch["members.remove"].map((entry) => (typeof entry === "string" ? entry : entry?.value))
721
+ : undefined;
722
+ for (const value of removeMembers ?? []) {
723
+ if (typeof value === "string") {
724
+ await app.container.scimGroupRepository.removeMember(orgId, existing.id, value);
725
+ }
726
+ }
727
+ await request.audit("scim_group_updated", {
728
+ groupId: existing.id,
729
+ actorType: "scim",
730
+ operation: "patch",
731
+ scimConnectionId: request.state.scimConnectionId,
732
+ scimOrgId: orgId,
733
+ });
734
+ return await groupWithMembers(request, existing.id);
735
+ }
736
+ catch (error) {
737
+ return handleError(reply, error);
738
+ }
739
+ });
740
+ app.delete("/scim/v2/Groups/:groupId", { preHandler: [scimRateLimit] }, async (request, reply) => {
741
+ const groupId = pathId(request, "groupId");
742
+ const orgId = request.state.scimOrgId;
743
+ try {
744
+ const group = await loadGroup(request, groupId);
745
+ const removed = await app.container.scimGroupRepository.deleteInOrg(orgId, group.id);
746
+ if (!removed)
747
+ return reply.status(404).send(scimError(404, "Group not found"));
748
+ await request.audit("scim_group_deleted", {
749
+ groupId: group.id,
750
+ displayName: group.displayName,
751
+ actorType: "scim",
752
+ scimConnectionId: request.state.scimConnectionId,
753
+ scimOrgId: orgId,
754
+ });
755
+ return reply.status(204).send();
756
+ }
757
+ catch (error) {
758
+ return handleError(reply, error);
759
+ }
760
+ });
761
+ app.get("/scim/v2/Groups/:groupId/members", { preHandler: [scimRateLimit] }, async (request, reply) => {
762
+ const groupId = pathId(request, "groupId");
763
+ try {
764
+ const group = await loadGroup(request, groupId);
765
+ const members = await app.container.scimGroupRepository.listMembers(request.state.scimOrgId, group.id);
766
+ return {
767
+ schemas: [SCIM_LIST_SCHEMA],
768
+ totalResults: members.length,
769
+ Resources: members.map((m) => ({ value: m.userId, display: m.email, $ref: `Users/${m.userId}` })),
770
+ };
771
+ }
772
+ catch (error) {
773
+ return handleError(reply, error);
774
+ }
775
+ });
776
+ app.post("/scim/v2/Groups/:groupId/members", { preHandler: [scimRateLimit] }, async (request, reply) => {
777
+ const groupId = pathId(request, "groupId");
778
+ const body = z.union([MemberBodySchema, z.object({ members: z.array(MemberBodySchema) })]).parse(request.body);
779
+ const orgId = request.state.scimOrgId;
780
+ try {
781
+ const group = await loadGroup(request, groupId);
782
+ const values = "value" in body ? [body.value] : body.members.map((m) => m.value);
783
+ let added = 0;
784
+ for (const value of values) {
785
+ await assertMemberOfOrg(request, value);
786
+ if (await app.container.scimGroupRepository.addMember(orgId, group.id, value))
787
+ added++;
788
+ }
789
+ if (added === 0)
790
+ return reply.status(409).send(scimError(409, "No new members were added"));
791
+ await request.audit("scim_group_updated", {
792
+ groupId: group.id,
793
+ actorType: "scim",
794
+ operation: "add_members",
795
+ added,
796
+ scimConnectionId: request.state.scimConnectionId,
797
+ scimOrgId: orgId,
798
+ });
799
+ return reply.status(201).send(await groupWithMembers(request, group.id));
800
+ }
801
+ catch (error) {
802
+ return handleError(reply, error);
803
+ }
804
+ });
805
+ app.delete("/scim/v2/Groups/:groupId/members/:userId", { preHandler: [scimRateLimit] }, async (request, reply) => {
806
+ const groupId = pathId(request, "groupId");
807
+ const userId = pathId(request, "userId");
808
+ const orgId = request.state.scimOrgId;
809
+ try {
810
+ const group = await loadGroup(request, groupId);
811
+ const removed = await app.container.scimGroupRepository.removeMember(orgId, group.id, userId);
812
+ if (!removed)
813
+ return reply.status(404).send(scimError(404, "Member not found"));
814
+ await request.audit("scim_group_updated", {
815
+ groupId: group.id,
816
+ actorType: "scim",
817
+ operation: "remove_member",
818
+ targetUserId: userId,
819
+ scimConnectionId: request.state.scimConnectionId,
820
+ scimOrgId: orgId,
821
+ });
822
+ return reply.status(204).send();
823
+ }
824
+ catch (error) {
825
+ return handleError(reply, error);
826
+ }
827
+ });
828
+ // SCIM service provider configuration, scoped to the authenticated tenant.
829
+ app.get("/scim/v2/ServiceProviderConfig", { preHandler: [scimRateLimit] }, async (request) => {
830
+ const base = `${request.protocol}://${request.hostname}`;
831
+ return {
832
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
833
+ documentationUri: "https://docs.hilbras.ai/keystone/scim",
834
+ patch: { supported: true },
835
+ bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
836
+ filter: { supported: true, maxResults: 500 },
837
+ changePassword: { supported: false },
838
+ sort: { supported: false },
839
+ etag: { supported: false },
840
+ authenticationSchemes: [
841
+ {
842
+ type: "oauthbearertoken",
843
+ name: "OAuth Bearer Token",
844
+ description: "Per-organization SCIM credential",
845
+ specUri: "http://www.rfc-editor.org/info/rfc6750",
846
+ primary: true,
847
+ },
848
+ ],
849
+ meta: {
850
+ resourceType: "ServiceProviderConfig",
851
+ organization: `${base} (${request.state.scimOrgId})`,
852
+ location: `${base}/scim/v2/ServiceProviderConfig`,
853
+ },
854
+ };
855
+ });
856
+ app.get("/scim/v2/ResourceTypes", { preHandler: [scimRateLimit] }, async () => [
857
+ {
858
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
859
+ id: "User",
860
+ name: "User",
861
+ endpoint: "/Users",
862
+ schema: SCIM_USER_SCHEMA,
863
+ meta: { resourceType: "ResourceType", location: "/ResourceTypes/User" },
864
+ },
865
+ {
866
+ schemas: ["urn:ietf:params:scim:schemas:core:2.0:ResourceType"],
867
+ id: "Group",
868
+ name: "Group",
869
+ endpoint: "/Groups",
870
+ schema: SCIM_GROUP_SCHEMA,
871
+ meta: { resourceType: "ResourceType", location: "/ResourceTypes/Group" },
872
+ },
873
+ ]);
874
+ // `.search` is the POST form of the list endpoints, required by RFC 7644 §3.4.3.
875
+ app.post("/scim/v2/Users/.search", { preHandler: [scimRateLimit] }, async (request, reply) => {
876
+ const body = z
877
+ .object({
878
+ filter: z.string().optional(),
879
+ startIndex: z.number().int().min(1).optional(),
880
+ count: z.number().int().min(0).max(500).optional(),
881
+ })
882
+ .parse(request.body ?? {});
883
+ const filter = parseEqFilter(body.filter, USER_FILTER_ATTRIBUTES);
884
+ if (filter === "unsupported") {
885
+ return reply.status(400).send(scimError(400, "Unsupported filter expression", "invalidFilter"));
886
+ }
887
+ const all = (await app.container.userRepository.listByOrg(request.state.scimOrgId)).filter((u) => u.isActive);
888
+ const matching = filter.attribute === "username"
889
+ ? all.filter((u) => u.email.toLowerCase() === filter.value.toLowerCase())
890
+ : all;
891
+ const start = (body.startIndex ?? 1) - 1;
892
+ const count = body.count ?? matching.length;
893
+ return {
894
+ schemas: [SCIM_LIST_SCHEMA],
895
+ totalResults: matching.length,
896
+ startIndex: start + 1,
897
+ itemsPerPage: Math.max(0, Math.min(count, matching.length - start)),
898
+ Resources: matching.slice(start, start + count).map(scimUserResponse),
899
+ };
900
+ });
901
+ }
902
+ /**
903
+ * Flatten a SCIM PATCH body into a single attribute map. Operations without a
904
+ * `path` contribute their `value` object directly; `remove` clears the
905
+ * attribute.
906
+ */
907
+ function collapsePatch(operations) {
908
+ const result = {};
909
+ for (const operation of operations) {
910
+ if (operation.op === "remove") {
911
+ if (operation.path) {
912
+ result[normalizePatchPath(operation.path)] = undefined;
913
+ continue;
914
+ }
915
+ if (operation.value && typeof operation.value === "object" && !Array.isArray(operation.value)) {
916
+ for (const key of Object.keys(operation.value))
917
+ result[key] = undefined;
918
+ }
919
+ continue;
920
+ }
921
+ if (!operation.path) {
922
+ if (operation.value && typeof operation.value === "object" && !Array.isArray(operation.value)) {
923
+ Object.assign(result, operation.value);
924
+ }
925
+ continue;
926
+ }
927
+ const path = normalizePatchPath(operation.path);
928
+ const value = operation.value;
929
+ if (path.includes(".")) {
930
+ const [head, tail] = path.split(".");
931
+ const current = (result[head] && typeof result[head] === "object" ? result[head] : {});
932
+ current[tail] = value;
933
+ result[head] = current;
934
+ continue;
935
+ }
936
+ result[path] = value;
937
+ }
938
+ return result;
939
+ }
940
+ /** `active` -> `active`, `name.givenName` -> `name.givenName`, `members` -> `members`. */
941
+ function normalizePatchPath(path) {
942
+ const [head, ...rest] = path.replace(/^\//, "").split(".");
943
+ const headKey = head.toLowerCase();
944
+ if (headKey === "members.remove")
945
+ return "members.remove";
946
+ if (headKey === "members")
947
+ return rest.length ? "members" : "members";
948
+ return rest.length ? `${headKey}.${rest.join(".")}` : headKey;
174
949
  }
175
950
  //# sourceMappingURL=scim.js.map