@contember/engine-tenant-api 2.2.0-alpha.5 → 2.2.0-alpha.6

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 (232) hide show
  1. package/CLAUDE.md +1 -0
  2. package/dist/development/index.cjs +2 -0
  3. package/dist/development/index.cjs.map +1 -1
  4. package/dist/development/index.js +2 -0
  5. package/dist/development/index.js.map +1 -1
  6. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs +18 -0
  7. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs.map +1 -0
  8. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js +19 -0
  9. package/dist/development/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js.map +1 -0
  10. package/dist/development/src/migrations/runner.cjs +3 -1
  11. package/dist/development/src/migrations/runner.cjs.map +1 -1
  12. package/dist/development/src/migrations/runner.js +3 -1
  13. package/dist/development/src/migrations/runner.js.map +1 -1
  14. package/dist/development/src/migrations/snapshot.cjs +2 -1
  15. package/dist/development/src/migrations/snapshot.cjs.map +1 -1
  16. package/dist/development/src/migrations/snapshot.js +2 -1
  17. package/dist/development/src/migrations/snapshot.js.map +1 -1
  18. package/dist/development/src/model/commands/idp/CreateIdpCommand.cjs +8 -2
  19. package/dist/development/src/model/commands/idp/CreateIdpCommand.cjs.map +1 -1
  20. package/dist/development/src/model/commands/idp/CreateIdpCommand.js +8 -2
  21. package/dist/development/src/model/commands/idp/CreateIdpCommand.js.map +1 -1
  22. package/dist/development/src/model/commands/idp/UpdateIdpCommand.cjs +6 -2
  23. package/dist/development/src/model/commands/idp/UpdateIdpCommand.cjs.map +1 -1
  24. package/dist/development/src/model/commands/idp/UpdateIdpCommand.js +6 -2
  25. package/dist/development/src/model/commands/idp/UpdateIdpCommand.js.map +1 -1
  26. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs +21 -0
  27. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs.map +1 -0
  28. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js +21 -0
  29. package/dist/development/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js.map +1 -0
  30. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.cjs +1 -1
  31. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.cjs.map +1 -1
  32. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.js +1 -1
  33. package/dist/development/src/model/queries/idp/PersonIdentityProvidersQuery.js.map +1 -1
  34. package/dist/development/src/model/queries/idp/helpers.cjs +3 -3
  35. package/dist/development/src/model/queries/idp/helpers.cjs.map +1 -1
  36. package/dist/development/src/model/queries/idp/helpers.js +3 -3
  37. package/dist/development/src/model/queries/idp/helpers.js.map +1 -1
  38. package/dist/development/src/model/service/PasswordResetManager.cjs +5 -0
  39. package/dist/development/src/model/service/PasswordResetManager.cjs.map +1 -1
  40. package/dist/development/src/model/service/PasswordResetManager.js +5 -0
  41. package/dist/development/src/model/service/PasswordResetManager.js.map +1 -1
  42. package/dist/development/src/model/service/PasswordlessSignInManager.cjs +19 -0
  43. package/dist/development/src/model/service/PasswordlessSignInManager.cjs.map +1 -1
  44. package/dist/development/src/model/service/PasswordlessSignInManager.js +19 -0
  45. package/dist/development/src/model/service/PasswordlessSignInManager.js.map +1 -1
  46. package/dist/development/src/model/service/SignInManager.cjs +5 -0
  47. package/dist/development/src/model/service/SignInManager.cjs.map +1 -1
  48. package/dist/development/src/model/service/SignInManager.js +5 -0
  49. package/dist/development/src/model/service/SignInManager.js.map +1 -1
  50. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.cjs +6 -0
  51. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.cjs.map +1 -1
  52. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.js +6 -0
  53. package/dist/development/src/model/service/idp/PersonIdentityProviderManager.js.map +1 -1
  54. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs +2 -1
  55. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs.map +1 -1
  56. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.js +2 -1
  57. package/dist/development/src/resolvers/mutation/idp/AddIDPMutationResolver.js.map +1 -1
  58. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs +2 -1
  59. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs.map +1 -1
  60. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js +2 -1
  61. package/dist/development/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js.map +1 -1
  62. package/dist/development/src/resolvers/mutation/idp/audit.cjs +1 -0
  63. package/dist/development/src/resolvers/mutation/idp/audit.cjs.map +1 -1
  64. package/dist/development/src/resolvers/mutation/idp/audit.js +1 -0
  65. package/dist/development/src/resolvers/mutation/idp/audit.js.map +1 -1
  66. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs +4 -0
  67. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs.map +1 -1
  68. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.js +4 -0
  69. package/dist/development/src/resolvers/mutation/person/PasswordlessMutationResolver.js.map +1 -1
  70. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.cjs +2 -2
  71. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.cjs.map +1 -1
  72. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.js +2 -2
  73. package/dist/development/src/resolvers/mutation/person/SignInMutationResolver.js.map +1 -1
  74. package/dist/development/src/schema/tenant.graphql.cjs +28 -0
  75. package/dist/development/src/schema/tenant.graphql.cjs.map +1 -1
  76. package/dist/development/src/schema/tenant.graphql.js +28 -0
  77. package/dist/development/src/schema/tenant.graphql.js.map +1 -1
  78. package/dist/production/index.cjs +2 -0
  79. package/dist/production/index.cjs.map +1 -1
  80. package/dist/production/index.js +2 -0
  81. package/dist/production/index.js.map +1 -1
  82. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs +18 -0
  83. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.cjs.map +1 -0
  84. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js +19 -0
  85. package/dist/production/src/migrations/2026-09-04-120000-idp-disable-local-authentication.js.map +1 -0
  86. package/dist/production/src/migrations/runner.cjs +3 -1
  87. package/dist/production/src/migrations/runner.cjs.map +1 -1
  88. package/dist/production/src/migrations/runner.js +3 -1
  89. package/dist/production/src/migrations/runner.js.map +1 -1
  90. package/dist/production/src/migrations/snapshot.cjs +2 -1
  91. package/dist/production/src/migrations/snapshot.cjs.map +1 -1
  92. package/dist/production/src/migrations/snapshot.js +2 -1
  93. package/dist/production/src/migrations/snapshot.js.map +1 -1
  94. package/dist/production/src/model/commands/idp/CreateIdpCommand.cjs +8 -2
  95. package/dist/production/src/model/commands/idp/CreateIdpCommand.cjs.map +1 -1
  96. package/dist/production/src/model/commands/idp/CreateIdpCommand.js +8 -2
  97. package/dist/production/src/model/commands/idp/CreateIdpCommand.js.map +1 -1
  98. package/dist/production/src/model/commands/idp/UpdateIdpCommand.cjs +6 -2
  99. package/dist/production/src/model/commands/idp/UpdateIdpCommand.cjs.map +1 -1
  100. package/dist/production/src/model/commands/idp/UpdateIdpCommand.js +6 -2
  101. package/dist/production/src/model/commands/idp/UpdateIdpCommand.js.map +1 -1
  102. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs +21 -0
  103. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.cjs.map +1 -0
  104. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js +21 -0
  105. package/dist/production/src/model/queries/idp/LocalAuthDisablingIdpsQuery.js.map +1 -0
  106. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.cjs +1 -1
  107. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.cjs.map +1 -1
  108. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.js +1 -1
  109. package/dist/production/src/model/queries/idp/PersonIdentityProvidersQuery.js.map +1 -1
  110. package/dist/production/src/model/queries/idp/helpers.cjs +3 -3
  111. package/dist/production/src/model/queries/idp/helpers.cjs.map +1 -1
  112. package/dist/production/src/model/queries/idp/helpers.js +3 -3
  113. package/dist/production/src/model/queries/idp/helpers.js.map +1 -1
  114. package/dist/production/src/model/service/PasswordResetManager.cjs +5 -0
  115. package/dist/production/src/model/service/PasswordResetManager.cjs.map +1 -1
  116. package/dist/production/src/model/service/PasswordResetManager.js +5 -0
  117. package/dist/production/src/model/service/PasswordResetManager.js.map +1 -1
  118. package/dist/production/src/model/service/PasswordlessSignInManager.cjs +19 -0
  119. package/dist/production/src/model/service/PasswordlessSignInManager.cjs.map +1 -1
  120. package/dist/production/src/model/service/PasswordlessSignInManager.js +19 -0
  121. package/dist/production/src/model/service/PasswordlessSignInManager.js.map +1 -1
  122. package/dist/production/src/model/service/SignInManager.cjs +5 -0
  123. package/dist/production/src/model/service/SignInManager.cjs.map +1 -1
  124. package/dist/production/src/model/service/SignInManager.js +5 -0
  125. package/dist/production/src/model/service/SignInManager.js.map +1 -1
  126. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.cjs +6 -0
  127. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.cjs.map +1 -1
  128. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.js +6 -0
  129. package/dist/production/src/model/service/idp/PersonIdentityProviderManager.js.map +1 -1
  130. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs +2 -1
  131. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.cjs.map +1 -1
  132. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.js +2 -1
  133. package/dist/production/src/resolvers/mutation/idp/AddIDPMutationResolver.js.map +1 -1
  134. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs +2 -1
  135. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.cjs.map +1 -1
  136. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js +2 -1
  137. package/dist/production/src/resolvers/mutation/idp/UpdateIDPMutationResolver.js.map +1 -1
  138. package/dist/production/src/resolvers/mutation/idp/audit.cjs +1 -0
  139. package/dist/production/src/resolvers/mutation/idp/audit.cjs.map +1 -1
  140. package/dist/production/src/resolvers/mutation/idp/audit.js +1 -0
  141. package/dist/production/src/resolvers/mutation/idp/audit.js.map +1 -1
  142. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs +4 -0
  143. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.cjs.map +1 -1
  144. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.js +4 -0
  145. package/dist/production/src/resolvers/mutation/person/PasswordlessMutationResolver.js.map +1 -1
  146. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.cjs +2 -2
  147. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.cjs.map +1 -1
  148. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.js +2 -2
  149. package/dist/production/src/resolvers/mutation/person/SignInMutationResolver.js.map +1 -1
  150. package/dist/production/src/schema/tenant.graphql.cjs +28 -0
  151. package/dist/production/src/schema/tenant.graphql.cjs.map +1 -1
  152. package/dist/production/src/schema/tenant.graphql.js +28 -0
  153. package/dist/production/src/schema/tenant.graphql.js.map +1 -1
  154. package/dist/tests/cases/integration/mocked/gql/passwordless.d.ts +3 -0
  155. package/dist/tests/cases/integration/mocked/gql/passwordless.d.ts.map +1 -1
  156. package/dist/tests/cases/integration/mocked/localAuthenticationDisabled.test.d.ts +2 -0
  157. package/dist/tests/cases/integration/mocked/localAuthenticationDisabled.test.d.ts.map +1 -0
  158. package/dist/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.d.ts +10 -0
  159. package/dist/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.d.ts.map +1 -0
  160. package/dist/tests/cases/integration/mocked/sql/personIdentityProvidersSql.d.ts +1 -0
  161. package/dist/tests/cases/integration/mocked/sql/personIdentityProvidersSql.d.ts.map +1 -1
  162. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  163. package/dist/types/migrations/2026-09-04-120000-idp-disable-local-authentication.d.ts +3 -0
  164. package/dist/types/migrations/2026-09-04-120000-idp-disable-local-authentication.d.ts.map +1 -0
  165. package/dist/types/migrations/runner.d.ts.map +1 -1
  166. package/dist/types/migrations/snapshot.d.ts.map +1 -1
  167. package/dist/types/model/commands/idp/CreateIdpCommand.d.ts.map +1 -1
  168. package/dist/types/model/commands/idp/UpdateIdpCommand.d.ts.map +1 -1
  169. package/dist/types/model/queries/idp/LocalAuthDisablingIdpsQuery.d.ts +18 -0
  170. package/dist/types/model/queries/idp/LocalAuthDisablingIdpsQuery.d.ts.map +1 -0
  171. package/dist/types/model/queries/idp/PersonIdentityProvidersQuery.d.ts +1 -0
  172. package/dist/types/model/queries/idp/PersonIdentityProvidersQuery.d.ts.map +1 -1
  173. package/dist/types/model/queries/idp/helpers.d.ts +1 -1
  174. package/dist/types/model/queries/idp/helpers.d.ts.map +1 -1
  175. package/dist/types/model/queries/idp/index.d.ts +1 -0
  176. package/dist/types/model/queries/idp/index.d.ts.map +1 -1
  177. package/dist/types/model/queries/idp/types.d.ts +1 -0
  178. package/dist/types/model/queries/idp/types.d.ts.map +1 -1
  179. package/dist/types/model/service/PasswordResetManager.d.ts.map +1 -1
  180. package/dist/types/model/service/PasswordlessSignInManager.d.ts.map +1 -1
  181. package/dist/types/model/service/SignInManager.d.ts.map +1 -1
  182. package/dist/types/model/service/idp/PersonIdentityProviderManager.d.ts.map +1 -1
  183. package/dist/types/model/type/IdentityProvider.d.ts +2 -0
  184. package/dist/types/model/type/IdentityProvider.d.ts.map +1 -1
  185. package/dist/types/resolvers/mutation/idp/AddIDPMutationResolver.d.ts.map +1 -1
  186. package/dist/types/resolvers/mutation/idp/UpdateIDPMutationResolver.d.ts.map +1 -1
  187. package/dist/types/resolvers/mutation/idp/audit.d.ts.map +1 -1
  188. package/dist/types/resolvers/mutation/person/PasswordlessMutationResolver.d.ts.map +1 -1
  189. package/dist/types/schema/index.d.ts +33 -4
  190. package/dist/types/schema/index.d.ts.map +1 -1
  191. package/dist/types/schema/tenant.graphql.d.ts.map +1 -1
  192. package/dist/types/tsconfig.tsbuildinfo +1 -1
  193. package/package.json +12 -12
  194. package/src/migrations/2026-09-04-120000-idp-disable-local-authentication.ts +18 -0
  195. package/src/migrations/runner.ts +2 -0
  196. package/src/migrations/snapshot.ts +2 -1
  197. package/src/model/commands/idp/CreateIdpCommand.ts +7 -1
  198. package/src/model/commands/idp/UpdateIdpCommand.ts +5 -1
  199. package/src/model/queries/idp/LocalAuthDisablingIdpsQuery.ts +41 -0
  200. package/src/model/queries/idp/PersonIdentityProvidersQuery.ts +2 -0
  201. package/src/model/queries/idp/helpers.ts +3 -2
  202. package/src/model/queries/idp/index.ts +1 -0
  203. package/src/model/queries/idp/types.ts +1 -0
  204. package/src/model/service/PasswordResetManager.ts +9 -1
  205. package/src/model/service/PasswordlessSignInManager.ts +23 -1
  206. package/src/model/service/SignInManager.ts +9 -1
  207. package/src/model/service/idp/PersonIdentityProviderManager.ts +8 -0
  208. package/src/model/type/IdentityProvider.ts +2 -0
  209. package/src/resolvers/mutation/idp/AddIDPMutationResolver.ts +1 -0
  210. package/src/resolvers/mutation/idp/UpdateIDPMutationResolver.ts +1 -0
  211. package/src/resolvers/mutation/idp/audit.ts +1 -0
  212. package/src/resolvers/mutation/person/PasswordlessMutationResolver.ts +7 -0
  213. package/src/resolvers/mutation/person/SignInMutationResolver.ts +2 -2
  214. package/src/schema/index.ts +29 -0
  215. package/src/schema/tenant.graphql.ts +28 -0
  216. package/tests/cases/integration/mocked/gql/passwordless.ts +10 -0
  217. package/tests/cases/integration/mocked/idpClaimMappingConfig.test.ts +4 -4
  218. package/tests/cases/integration/mocked/localAuthenticationDisabled.test.ts +182 -0
  219. package/tests/cases/integration/mocked/personIdentityProviders.test.ts +67 -0
  220. package/tests/cases/integration/mocked/signInAnomalyDetection.test.ts +11 -0
  221. package/tests/cases/integration/mocked/signInIdpClaimMapping.test.ts +14 -0
  222. package/tests/cases/integration/mocked/signInIdpClaimMappingFixes.test.ts +4 -0
  223. package/tests/cases/integration/mocked/signInIdpClaimMappingSafety.test.ts +6 -0
  224. package/tests/cases/integration/mocked/signInIdpMutation.test.ts +5 -0
  225. package/tests/cases/integration/mocked/signInMfaEnforcement.test.ts +7 -0
  226. package/tests/cases/integration/mocked/signInMutation.test.ts +14 -0
  227. package/tests/cases/integration/mocked/signInPasswordlessMutation.test.ts +36 -0
  228. package/tests/cases/integration/mocked/sql/getIdpBySlugSql.ts +1 -1
  229. package/tests/cases/integration/mocked/sql/localAuthDisablingIdpsSql.ts +21 -0
  230. package/tests/cases/integration/mocked/sql/personIdentityProvidersSql.ts +3 -1
  231. package/tests/cases/integration/mocked/trustForwardedClientInfo.test.ts +3 -0
  232. package/tests/cases/unit/idp/idpManagerUnknownConfigKeys.test.ts +8 -1
@@ -0,0 +1,182 @@
1
+ import { expect, test } from 'bun:test'
2
+ import { executeTenantTest } from '../../../src/testTenant.js'
3
+ import { testUuid } from '../../../src/testUuid.js'
4
+ import { signInMutation } from './gql/signIn.js'
5
+ import { initSignInPasswordlessMutation } from './gql/passwordless.js'
6
+ import { createResetPasswordRequestMutation } from './gql/createResetPasswordRequest.js'
7
+ import { getConfigSql } from './sql/getConfigSql.js'
8
+ import { getNextLoginAttemptSql } from './sql/getNextLoginAttemptSql.js'
9
+ import { getPersonByEmailSql } from './sql/getPersonByEmailSql.js'
10
+ import { localAuthDisablingIdpsSql } from './sql/localAuthDisablingIdpsSql.js'
11
+ import { createSessionKeySql } from './sql/createSessionKeySql.js'
12
+ import { getIdentityProjectsSql } from './sql/getIdentityProjectsSql.js'
13
+ import { getIdentityByIdSql } from './sql/getIdentityByIdSql.js'
14
+ import { getAuthPoliciesSql } from './sql/authPolicySql.js'
15
+ import { selectMembershipsSql } from './sql/selectMembershipsSql.js'
16
+ import { sqlTransaction } from './sql/sqlTransaction.js'
17
+
18
+ const email = 'john@doe.com'
19
+ const password = '123'
20
+ const personId = testUuid(7)
21
+ const identityId = testUuid(2)
22
+
23
+ test('signIn - a provider that disables local authentication refuses the password', async () => {
24
+ await executeTenantTest({
25
+ query: signInMutation({ email, password }),
26
+ executes: [
27
+ getConfigSql(),
28
+ getNextLoginAttemptSql(email),
29
+ getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
30
+ localAuthDisablingIdpsSql({ personId, slugs: ['corporateSso'] }),
31
+ ],
32
+ return: {
33
+ data: {
34
+ signIn: {
35
+ ok: false,
36
+ errors: [{ code: 'IDP_REQUIRED' }],
37
+ result: null,
38
+ },
39
+ },
40
+ },
41
+ expectedAuthLog: {
42
+ type: 'login',
43
+ response: expect.objectContaining({ ok: false }),
44
+ },
45
+ })
46
+ })
47
+
48
+ test('signIn - the wrong password is not even consulted, so the answer cannot double as a password oracle', async () => {
49
+ await executeTenantTest({
50
+ query: signInMutation({ email, password: 'not-the-password' }),
51
+ executes: [
52
+ getConfigSql(),
53
+ getNextLoginAttemptSql(email),
54
+ getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
55
+ localAuthDisablingIdpsSql({ personId, slugs: ['corporateSso'] }),
56
+ ],
57
+ return: {
58
+ data: {
59
+ signIn: {
60
+ ok: false,
61
+ errors: [{ code: 'IDP_REQUIRED' }],
62
+ result: null,
63
+ },
64
+ },
65
+ },
66
+ expectedAuthLog: {
67
+ type: 'login',
68
+ response: expect.objectContaining({ ok: false }),
69
+ },
70
+ })
71
+ })
72
+
73
+ test('signIn - revealLoginMethod off collapses it into INVALID_CREDENTIALS', async () => {
74
+ await executeTenantTest({
75
+ query: signInMutation({ email, password }),
76
+ executes: [
77
+ getConfigSql({ login_reveal_login_method: false }),
78
+ getNextLoginAttemptSql(email),
79
+ getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
80
+ localAuthDisablingIdpsSql({ personId, slugs: ['corporateSso'] }),
81
+ ],
82
+ return: {
83
+ data: {
84
+ signIn: {
85
+ ok: false,
86
+ errors: [{ code: 'INVALID_CREDENTIALS' }],
87
+ result: null,
88
+ },
89
+ },
90
+ },
91
+ expectedAuthLog: {
92
+ type: 'login',
93
+ response: expect.objectContaining({ ok: false }),
94
+ },
95
+ })
96
+ })
97
+
98
+ test('signIn - a person with no such link signs in as before', async () => {
99
+ const projectId = testUuid(10)
100
+ const apiKeyId = testUuid(1)
101
+ await executeTenantTest({
102
+ query: signInMutation({ email, password }),
103
+ executes: [
104
+ getConfigSql(),
105
+ getNextLoginAttemptSql(email),
106
+ getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
107
+ // The asserted SQL carries `disabled_at IS NULL`, so a DISABLED provider never
108
+ // reaches this list — turning the provider off is the break-glass.
109
+ localAuthDisablingIdpsSql({ personId }),
110
+ getAuthPoliciesSql(),
111
+ getConfigSql(),
112
+ getIdentityByIdSql({ identityId }),
113
+ getAuthPoliciesSql(),
114
+ createSessionKeySql({ apiKeyId, identityId }),
115
+ getIdentityProjectsSql({ identityId, projectId }),
116
+ selectMembershipsSql({ identityId, projectId, membershipsResponse: [] }),
117
+ ],
118
+ return: {
119
+ data: {
120
+ signIn: {
121
+ ok: true,
122
+ errors: [],
123
+ result: { token: '0000000000000000000000000000000000000000' },
124
+ },
125
+ },
126
+ },
127
+ expectedAuthLog: {
128
+ type: 'login',
129
+ response: expect.objectContaining({ ok: true }),
130
+ },
131
+ })
132
+ })
133
+
134
+ test('initSignInPasswordless - the magic link is refused as well, so no second door stays open', async () => {
135
+ await executeTenantTest({
136
+ query: initSignInPasswordlessMutation({ email }),
137
+ executes: [
138
+ getConfigSql({ passwordless_enabled: 'always' }),
139
+ ...sqlTransaction(
140
+ getConfigSql({ passwordless_enabled: 'always' }),
141
+ getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
142
+ localAuthDisablingIdpsSql({ personId, slugs: ['corporateSso'] }),
143
+ ),
144
+ ],
145
+ return: {
146
+ data: {
147
+ initSignInPasswordless: {
148
+ ok: false,
149
+ error: { code: 'IDP_REQUIRED' },
150
+ },
151
+ },
152
+ },
153
+ expectedAuthLog: {
154
+ type: 'passwordless_login_init',
155
+ response: expect.objectContaining({ ok: false }),
156
+ },
157
+ })
158
+ })
159
+
160
+ test('createResetPasswordRequest - reports ok but sends nothing, because the password it would set is unusable', async () => {
161
+ await executeTenantTest({
162
+ query: createResetPasswordRequestMutation({ email }),
163
+ executes: [
164
+ getConfigSql(),
165
+ getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
166
+ localAuthDisablingIdpsSql({ personId, slugs: ['corporateSso'] }),
167
+ ],
168
+ return: {
169
+ data: {
170
+ createResetPasswordRequest: {
171
+ ok: true,
172
+ },
173
+ },
174
+ },
175
+ expectedAuthLog: {
176
+ type: 'password_reset_init',
177
+ personInput: email,
178
+ personId,
179
+ response: expect.objectContaining({ ok: true }),
180
+ },
181
+ })
182
+ })
@@ -278,6 +278,73 @@ test('refuses to disconnect when the only other connection is to a disabled prov
278
278
  })
279
279
  })
280
280
 
281
+ test('refuses to disconnect a provider that disables local authentication, even though a password remains', async () => {
282
+ const personId = testUuid(1)
283
+ const connectionId = testUuid(2)
284
+ await executeTenantTest({
285
+ query: disconnectMyIdentityProviderMutation({ id: connectionId }),
286
+ executes: [
287
+ // the password would be usable again the moment the link is gone, which is exactly the escape to close
288
+ getPersonByIdentityForIdp({ identityId: authenticatedIdentityId, personId, passwordHash: 'BCRYPTED-123' }),
289
+ ...sqlTransaction(
290
+ getPersonIdentityProvidersSql({
291
+ personId,
292
+ response: [
293
+ { id: connectionId, createdAt, externalIdentifier: 'ext-1', slug: 'corporateSso', type: 'oidc', disableLocalAuthentication: true },
294
+ ],
295
+ }),
296
+ ),
297
+ ],
298
+ return: {
299
+ data: {
300
+ disconnectMyIdentityProvider: {
301
+ ok: false,
302
+ error: {
303
+ code: 'IDP_REQUIRED',
304
+ },
305
+ },
306
+ },
307
+ },
308
+ })
309
+ })
310
+
311
+ test('still refuses while that provider is disabled: break-glass reopens local sign-in, not self-unlinking', async () => {
312
+ const personId = testUuid(1)
313
+ const connectionId = testUuid(2)
314
+ await executeTenantTest({
315
+ query: disconnectMyIdentityProviderMutation({ id: connectionId }),
316
+ executes: [
317
+ getPersonByIdentityForIdp({ identityId: authenticatedIdentityId, personId, passwordHash: 'BCRYPTED-123' }),
318
+ ...sqlTransaction(
319
+ getPersonIdentityProvidersSql({
320
+ personId,
321
+ response: [
322
+ {
323
+ id: connectionId,
324
+ createdAt,
325
+ externalIdentifier: 'ext-1',
326
+ slug: 'corporateSso',
327
+ type: 'oidc',
328
+ disabledAt: new Date('2021-01-01T00:00:00.000Z'),
329
+ disableLocalAuthentication: true,
330
+ },
331
+ ],
332
+ }),
333
+ ),
334
+ ],
335
+ return: {
336
+ data: {
337
+ disconnectMyIdentityProvider: {
338
+ ok: false,
339
+ error: {
340
+ code: 'IDP_REQUIRED',
341
+ },
342
+ },
343
+ },
344
+ },
345
+ })
346
+ })
347
+
281
348
  test('returns NOT_FOUND when the person is not connected to the given connection', async () => {
282
349
  const personId = testUuid(1)
283
350
  await executeTenantTest({
@@ -3,6 +3,7 @@ import { testUuid } from '../../../src/testUuid.js'
3
3
  import { selectMembershipsSql } from './sql/selectMembershipsSql.js'
4
4
  import { signInMutation } from './gql/signIn.js'
5
5
  import { getPersonByEmailSql } from './sql/getPersonByEmailSql.js'
6
+ import { localAuthDisablingIdpsSql } from './sql/localAuthDisablingIdpsSql.js'
6
7
  import { expect, test } from 'bun:test'
7
8
  import { OtpAuthenticator } from '../../../../src/index.js'
8
9
  import { Buffer } from 'buffer'
@@ -51,6 +52,7 @@ test('anomaly off by default: a new country does not add any queries (baseline p
51
52
  getConfigSql(),
52
53
  getNextLoginAttemptSql(email),
53
54
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
55
+ localAuthDisablingIdpsSql({ personId }),
54
56
  getAuthPoliciesSql(),
55
57
  getConfigSql(),
56
58
  getIdentityByIdSql({ identityId }),
@@ -83,6 +85,7 @@ test('anomaly on, new device only (medium): sends UNUSUAL_LOGIN email, still sig
83
85
  anomalyConfig,
84
86
  getNextLoginAttemptSql(email),
85
87
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
88
+ localAuthDisablingIdpsSql({ personId }),
86
89
  // enforceMfaEnrollment (no policies → inert, single query)
87
90
  getAuthPoliciesSql(),
88
91
  // applyRiskPolicy: history (same country, different device → medium 2) then email
@@ -131,6 +134,7 @@ test('anomaly on, new country (high): requires step-up, dispatches email OTP, re
131
134
  anomalyConfig,
132
135
  getNextLoginAttemptSql(email),
133
136
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
137
+ localAuthDisablingIdpsSql({ personId }),
134
138
  // enforceMfaEnrollment (no policies → inert, single query)
135
139
  getAuthPoliciesSql(),
136
140
  // history: previously only ever seen in CZ → US is a new country (high, 3) → step-up.
@@ -185,6 +189,7 @@ test('anomaly on, step-up retry with the emailed code: consumes it and signs in
185
189
  anomalyConfig,
186
190
  getNextLoginAttemptSql(email),
187
191
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
192
+ localAuthDisablingIdpsSql({ personId }),
188
193
  // enforceMfaEnrollment (no policies → inert; otpToken is ignored here)
189
194
  getAuthPoliciesSql(),
190
195
  // previously only ever seen in CZ → US is a new country (high, 3) → step-up.
@@ -235,6 +240,7 @@ test('anomaly on, step-up retry with a wrong code: INVALID_OTP_TOKEN, no session
235
240
  anomalyConfig,
236
241
  getNextLoginAttemptSql(email),
237
242
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
243
+ localAuthDisablingIdpsSql({ personId }),
238
244
  getAuthPoliciesSql(),
239
245
  getLoginHistorySql({ personId, rows: [{ geoCountry: 'CZ', deviceFingerprint: KNOWN_FINGERPRINT, ip: '10.0.0.5' }] }),
240
246
  // wrong code: token is found and an attempt is claimed, but validation fails.
@@ -289,6 +295,7 @@ test('anomaly on, step-up scored for a user who already cleared TOTP this reques
289
295
  anomalyConfig,
290
296
  getNextLoginAttemptSql(email),
291
297
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [], otpUri: otp.uri } }),
298
+ localAuthDisablingIdpsSql({ personId }),
292
299
  // active TOTP + valid token → verified here (no SQL); mfaSatisfiedThisRequest = true.
293
300
  // applyRiskPolicy: US is a new country (high, 3 → stepUp) BUT MFA already satisfied
294
301
  // this request → NO email-OTP challenge (no sendEmailOtpSql/getLatestEmailOtpTokenSql
@@ -343,6 +350,7 @@ test('anomaly on, new IP prefix only (low): the client IP flows into scoring →
343
350
  recordLoginRateLimitSql(clientIp),
344
351
  getNextLoginAttemptSql(email),
345
352
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
353
+ localAuthDisablingIdpsSql({ personId }),
346
354
  getAuthPoliciesSql(),
347
355
  // history IP is 10.0.0.5 (/24 = 10.0.0.0) → current 203.0.113.9 (/24 = 203.0.113.0)
348
356
  // is a new prefix; country + device match → score 1 → email.
@@ -388,6 +396,7 @@ test('anomaly on, custom historySize flows into the baseline query limit', async
388
396
  anomalyConfig3,
389
397
  getNextLoginAttemptSql(email),
390
398
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
399
+ localAuthDisablingIdpsSql({ personId }),
391
400
  getAuthPoliciesSql(),
392
401
  // limit 3, not the default 10. Same country + device → score 0 → clean.
393
402
  getLoginHistorySql({ personId, limit: 3, rows: [{ geoCountry: 'CZ', deviceFingerprint: KNOWN_FINGERPRINT, ip: '10.0.0.5' }] }),
@@ -422,6 +431,7 @@ test('anomaly on, historySize <= 0 is clamped to a limit of 1', async () => {
422
431
  anomalyConfig0,
423
432
  getNextLoginAttemptSql(email),
424
433
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
434
+ localAuthDisablingIdpsSql({ personId }),
425
435
  getAuthPoliciesSql(),
426
436
  getLoginHistorySql({ personId, limit: 1, rows: [{ geoCountry: 'CZ', deviceFingerprint: KNOWN_FINGERPRINT, ip: '10.0.0.5' }] }),
427
437
  anomalyConfig0,
@@ -452,6 +462,7 @@ test('anomaly on, all signals match history: no email, no step-up, signs in clea
452
462
  anomalyConfig,
453
463
  getNextLoginAttemptSql(email),
454
464
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: [] } }),
465
+ localAuthDisablingIdpsSql({ personId }),
455
466
  // enforceMfaEnrollment (no policies → inert, single query)
456
467
  getAuthPoliciesSql(),
457
468
  // same country, same device → score 0 → allow, no email.
@@ -56,6 +56,7 @@ test('claim mapping grants a project membership on sign-in', async () => {
56
56
  initReturnsConfig: false,
57
57
  requireVerifiedEmail: false,
58
58
  assumeEmailVerified: false,
59
+ disableLocalAuthentication: false,
59
60
  configuration: {
60
61
  externalIdentifier,
61
62
  email,
@@ -143,6 +144,7 @@ test('claim mapping grants a membership with a claim-derived variable (mapped va
143
144
  initReturnsConfig: false,
144
145
  requireVerifiedEmail: false,
145
146
  assumeEmailVerified: false,
147
+ disableLocalAuthentication: false,
146
148
  configuration: {
147
149
  externalIdentifier,
148
150
  email,
@@ -242,6 +244,7 @@ test('a granted membership whose declared variable resolves to no values keeps t
242
244
  initReturnsConfig: false,
243
245
  requireVerifiedEmail: false,
244
246
  assumeEmailVerified: false,
247
+ disableLocalAuthentication: false,
245
248
  configuration: {
246
249
  externalIdentifier,
247
250
  email,
@@ -332,6 +335,7 @@ test('sticky sync policy skips re-mapping for an existing person (no membership
332
335
  initReturnsConfig: false,
333
336
  requireVerifiedEmail: false,
334
337
  assumeEmailVerified: false,
338
+ disableLocalAuthentication: false,
335
339
  configuration: {
336
340
  externalIdentifier,
337
341
  email,
@@ -393,6 +397,7 @@ test('a malformed stored claimMapping is skipped (fail-open) — sign-in succeed
393
397
  initReturnsConfig: false,
394
398
  requireVerifiedEmail: false,
395
399
  assumeEmailVerified: false,
400
+ disableLocalAuthentication: false,
396
401
  configuration: {
397
402
  externalIdentifier,
398
403
  email,
@@ -456,6 +461,7 @@ test('a claim mapping referencing a non-existent project is skipped, sign-in sti
456
461
  initReturnsConfig: false,
457
462
  requireVerifiedEmail: false,
458
463
  assumeEmailVerified: false,
464
+ disableLocalAuthentication: false,
459
465
  configuration: {
460
466
  externalIdentifier,
461
467
  email,
@@ -521,6 +527,7 @@ test('unmatched: remove strips a no-longer-granted membership in a mapped projec
521
527
  initReturnsConfig: false,
522
528
  requireVerifiedEmail: false,
523
529
  assumeEmailVerified: false,
530
+ disableLocalAuthentication: false,
524
531
  configuration: {
525
532
  externalIdentifier,
526
533
  email,
@@ -623,6 +630,7 @@ test('unmatched: remove revokes the whole membership when a declared variable re
623
630
  initReturnsConfig: false,
624
631
  requireVerifiedEmail: false,
625
632
  assumeEmailVerified: false,
633
+ disableLocalAuthentication: false,
626
634
  configuration: {
627
635
  externalIdentifier,
628
636
  email,
@@ -737,6 +745,7 @@ test('sticky sync policy APPLIES on a brand-new auto-signed-up person', async ()
737
745
  initReturnsConfig: false,
738
746
  requireVerifiedEmail: false,
739
747
  assumeEmailVerified: false,
748
+ disableLocalAuthentication: false,
740
749
  configuration: {
741
750
  externalIdentifier,
742
751
  email,
@@ -822,6 +831,7 @@ test('unmatched: remove revokes a vocabulary membership even when no rule matche
822
831
  initReturnsConfig: false,
823
832
  requireVerifiedEmail: false,
824
833
  assumeEmailVerified: false,
834
+ disableLocalAuthentication: false,
825
835
  configuration: {
826
836
  externalIdentifier,
827
837
  email,
@@ -919,6 +929,7 @@ test('unmatched: remove never strips a membership outside the mapping vocabulary
919
929
  initReturnsConfig: false,
920
930
  requireVerifiedEmail: false,
921
931
  assumeEmailVerified: false,
932
+ disableLocalAuthentication: false,
922
933
  configuration: {
923
934
  externalIdentifier,
924
935
  email,
@@ -1023,6 +1034,7 @@ test('a passthrough claim-derived variable writes only allow-listed values (the
1023
1034
  initReturnsConfig: false,
1024
1035
  requireVerifiedEmail: false,
1025
1036
  assumeEmailVerified: false,
1037
+ disableLocalAuthentication: false,
1026
1038
  configuration: {
1027
1039
  externalIdentifier,
1028
1040
  email,
@@ -1121,6 +1133,7 @@ test('TEST-4: a claimMapping carrying only the OIDC identity-field remap (no rul
1121
1133
  initReturnsConfig: false,
1122
1134
  requireVerifiedEmail: false,
1123
1135
  assumeEmailVerified: false,
1136
+ disableLocalAuthentication: false,
1124
1137
  configuration: {
1125
1138
  externalIdentifier,
1126
1139
  email,
@@ -1177,6 +1190,7 @@ test('a passthrough claim-derived variable with allow:[] writes no variable, kee
1177
1190
  initReturnsConfig: false,
1178
1191
  requireVerifiedEmail: false,
1179
1192
  assumeEmailVerified: false,
1193
+ disableLocalAuthentication: false,
1180
1194
  configuration: {
1181
1195
  externalIdentifier,
1182
1196
  email,
@@ -60,6 +60,7 @@ test('SEC-1: a grant for a project that does not exist yet is KEPT but inert —
60
60
  initReturnsConfig: false,
61
61
  requireVerifiedEmail: false,
62
62
  assumeEmailVerified: false,
63
+ disableLocalAuthentication: false,
63
64
  configuration: { externalIdentifier, email, claims: { department: 'Editorial' }, claimMapping: { rules: [editorRule] } },
64
65
  disabledAt: null,
65
66
  slug: 'mock',
@@ -111,6 +112,7 @@ test('SEC-1: a grant for an EXISTING project whose schema is unresolvable is DRO
111
112
  initReturnsConfig: false,
112
113
  requireVerifiedEmail: false,
113
114
  assumeEmailVerified: false,
115
+ disableLocalAuthentication: false,
114
116
  configuration: { externalIdentifier, email, claims: { department: 'Editorial' }, claimMapping: { rules: [editorRule] } },
115
117
  disabledAt: null,
116
118
  slug: 'mock',
@@ -170,6 +172,7 @@ test('TEST-3: a partial drop applies the safe grant AND still emits the fail-ope
170
172
  initReturnsConfig: false,
171
173
  requireVerifiedEmail: false,
172
174
  assumeEmailVerified: false,
175
+ disableLocalAuthentication: false,
173
176
  configuration: {
174
177
  externalIdentifier,
175
178
  email,
@@ -264,6 +267,7 @@ test('TEST-4: an allow-bounded claim-derived `condition` variable writes ONLY th
264
267
  initReturnsConfig: false,
265
268
  requireVerifiedEmail: false,
266
269
  assumeEmailVerified: false,
270
+ disableLocalAuthentication: false,
267
271
  configuration: {
268
272
  externalIdentifier,
269
273
  email,
@@ -81,6 +81,7 @@ test('a DB error during the membership apply rolls back the whole sign-in (no se
81
81
  initReturnsConfig: false,
82
82
  requireVerifiedEmail: false,
83
83
  assumeEmailVerified: false,
84
+ disableLocalAuthentication: false,
84
85
  configuration: {
85
86
  externalIdentifier,
86
87
  email,
@@ -151,6 +152,7 @@ test('sticky leaves an existing local account being LINKED to the IdP untouched
151
152
  initReturnsConfig: false,
152
153
  requireVerifiedEmail: false,
153
154
  assumeEmailVerified: false,
155
+ disableLocalAuthentication: false,
154
156
  configuration: {
155
157
  externalIdentifier,
156
158
  email,
@@ -219,6 +221,7 @@ test('apply-time guard drops a grant that would inject a claim into a condition
219
221
  initReturnsConfig: false,
220
222
  requireVerifiedEmail: false,
221
223
  assumeEmailVerified: false,
224
+ disableLocalAuthentication: false,
222
225
  configuration: {
223
226
  externalIdentifier,
224
227
  email,
@@ -299,6 +302,7 @@ test('TEST-1: under unmatched:remove, a rule dropped by the apply-time guard doe
299
302
  initReturnsConfig: false,
300
303
  requireVerifiedEmail: false,
301
304
  assumeEmailVerified: false,
305
+ disableLocalAuthentication: false,
302
306
  configuration: {
303
307
  externalIdentifier,
304
308
  email,
@@ -382,6 +386,7 @@ test('SEC: apply-time guard drops an `admin` grant whose claim-derived variable
382
386
  initReturnsConfig: false,
383
387
  requireVerifiedEmail: false,
384
388
  assumeEmailVerified: false,
389
+ disableLocalAuthentication: false,
385
390
  configuration: {
386
391
  externalIdentifier,
387
392
  email,
@@ -465,6 +470,7 @@ test('SEC: a legitimate `admin` grant (role has no required variable) is still a
465
470
  initReturnsConfig: false,
466
471
  requireVerifiedEmail: false,
467
472
  assumeEmailVerified: false,
473
+ disableLocalAuthentication: false,
468
474
  configuration: {
469
475
  externalIdentifier,
470
476
  email,
@@ -49,6 +49,7 @@ test('signs in idp with existing identity', async () => {
49
49
  initReturnsConfig: false,
50
50
  requireVerifiedEmail: false,
51
51
  assumeEmailVerified: false,
52
+ disableLocalAuthentication: false,
52
53
  slug: 'mock',
53
54
  type: 'mock',
54
55
  },
@@ -125,6 +126,7 @@ test('does NOT link by e-mail when provider requires a verified e-mail and the c
125
126
  initReturnsConfig: false,
126
127
  requireVerifiedEmail: true,
127
128
  assumeEmailVerified: false,
129
+ disableLocalAuthentication: false,
128
130
  configuration: {
129
131
  externalIdentifier,
130
132
  email,
@@ -204,6 +206,7 @@ test('links by e-mail when provider requires a verified e-mail and the claim is
204
206
  initReturnsConfig: false,
205
207
  requireVerifiedEmail: true,
206
208
  assumeEmailVerified: false,
209
+ disableLocalAuthentication: false,
207
210
  configuration: {
208
211
  externalIdentifier,
209
212
  email,
@@ -291,6 +294,7 @@ test('links by e-mail when require + assumeEmailVerified are set, even though th
291
294
  requireVerifiedEmail: true,
292
295
  // trusted IdP: e-mails are treated as verified even though the claim says false
293
296
  assumeEmailVerified: true,
297
+ disableLocalAuthentication: false,
294
298
  configuration: {
295
299
  externalIdentifier,
296
300
  email,
@@ -378,6 +382,7 @@ test('signs in exclusive idp', async () => {
378
382
  initReturnsConfig: false,
379
383
  requireVerifiedEmail: false,
380
384
  assumeEmailVerified: false,
385
+ disableLocalAuthentication: false,
381
386
  configuration: {
382
387
  externalIdentifier: externalIdentifier,
383
388
  email,
@@ -3,6 +3,7 @@ import { SQL } from '../../../src/tags.js'
3
3
  import { testUuid } from '../../../src/testUuid.js'
4
4
  import { signInMutation } from './gql/signIn.js'
5
5
  import { getPersonByEmailSql } from './sql/getPersonByEmailSql.js'
6
+ import { localAuthDisablingIdpsSql } from './sql/localAuthDisablingIdpsSql.js'
6
7
  import { getConfigSql } from './sql/getConfigSql.js'
7
8
  import { getNextLoginAttemptSql } from './sql/getNextLoginAttemptSql.js'
8
9
  import { createSessionKeySql } from './sql/createSessionKeySql.js'
@@ -48,6 +49,7 @@ test('MFA required + no factor + no code → MFA_ENROLLMENT_REQUIRED with pendin
48
49
  getConfigSql(),
49
50
  getNextLoginAttemptSql(email),
50
51
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: ['editor'] } }),
52
+ localAuthDisablingIdpsSql({ personId }),
51
53
  requiringPolicy(),
52
54
  getAllProjectRolesByIdentitySql({ identityId }),
53
55
  // Enforcement resolves the grace duration: the matched policy has no
@@ -113,6 +115,7 @@ test('MFA required + valid pending code → enrolls (pending→active), returns
113
115
  getConfigSql(),
114
116
  getNextLoginAttemptSql(email),
115
117
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: ['editor'], otpPendingUri: otp.uri } }),
118
+ localAuthDisablingIdpsSql({ personId }),
116
119
  requiringPolicy(),
117
120
  getAllProjectRolesByIdentitySql({ identityId }),
118
121
  // Enforcement resolves the grace duration from the global config default.
@@ -173,6 +176,7 @@ test('MFA required + invalid pending code → INVALID_OTP_TOKEN', async () => {
173
176
  getConfigSql(),
174
177
  getNextLoginAttemptSql(email),
175
178
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: ['editor'], otpPendingUri: pendingUri } }),
179
+ localAuthDisablingIdpsSql({ personId }),
176
180
  requiringPolicy(),
177
181
  getAllProjectRolesByIdentitySql({ identityId }),
178
182
  // Enforcement resolves the grace duration from the global config default.
@@ -206,6 +210,7 @@ test('active TOTP + requiring policy → existing OTP_REQUIRED path (policy not
206
210
  getConfigSql(),
207
211
  getNextLoginAttemptSql(email),
208
212
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: ['editor'], otpUri } }),
213
+ localAuthDisablingIdpsSql({ personId }),
209
214
  ],
210
215
  return: {
211
216
  data: {
@@ -241,6 +246,7 @@ test('MFA required + per-policy grace → opens grace window, sets mfa_grace_unt
241
246
  getConfigSql(),
242
247
  getNextLoginAttemptSql(email),
243
248
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: ['editor'] } }),
249
+ localAuthDisablingIdpsSql({ personId }),
244
250
  gracePolicy(),
245
251
  getAllProjectRolesByIdentitySql({ identityId }),
246
252
  // Per-policy grace override is present → the global config is NOT read here.
@@ -288,6 +294,7 @@ test('MFA required + no policy grace override → global config default opens th
288
294
  getConfigSql(),
289
295
  getNextLoginAttemptSql(email),
290
296
  getPersonByEmailSql({ email, response: { personId, identityId, password, roles: ['editor'] } }),
297
+ localAuthDisablingIdpsSql({ personId }),
291
298
  requiringPolicy(),
292
299
  getAllProjectRolesByIdentitySql({ identityId }),
293
300
  // No per-policy override → the global config default is read; here it is