@vertesia/ui 1.5.0 → 1.5.1

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 (210) hide show
  1. package/lib/core/components/SelectList.d.ts.map +1 -1
  2. package/lib/core/components/SelectList.js +3 -3
  3. package/lib/core/components/SelectList.js.map +1 -1
  4. package/lib/core/components/SidePanel.d.ts.map +1 -1
  5. package/lib/core/components/SidePanel.js +2 -1
  6. package/lib/core/components/SidePanel.js.map +1 -1
  7. package/lib/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  8. package/lib/features/agent/chat/ModernAgentConversation.js +51 -17
  9. package/lib/features/agent/chat/ModernAgentConversation.js.map +1 -1
  10. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts +5 -1
  11. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.d.ts.map +1 -1
  12. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js +9 -2
  13. package/lib/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  14. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts +1 -1
  15. package/lib/features/agent/chat/hooks/useAgentPlans.d.ts.map +1 -1
  16. package/lib/features/agent/chat/hooks/useAgentPlans.js +5 -5
  17. package/lib/features/agent/chat/hooks/useAgentPlans.js.map +1 -1
  18. package/lib/features/magic-pdf/GroundedExtractionView.js +3 -3
  19. package/lib/features/magic-pdf/GroundedExtractionView.js.map +1 -1
  20. package/lib/features/oauth/McpConnectionsButton.d.ts +2 -2
  21. package/lib/features/oauth/McpConnectionsButton.d.ts.map +1 -1
  22. package/lib/features/oauth/McpConnectionsButton.js +9 -7
  23. package/lib/features/oauth/McpConnectionsButton.js.map +1 -1
  24. package/lib/features/oauth/McpConnectionsDialog.d.ts.map +1 -1
  25. package/lib/features/oauth/McpConnectionsDialog.js +7 -3
  26. package/lib/features/oauth/McpConnectionsDialog.js.map +1 -1
  27. package/lib/features/oauth/useMcpConnections.d.ts +4 -2
  28. package/lib/features/oauth/useMcpConnections.d.ts.map +1 -1
  29. package/lib/features/oauth/useMcpConnections.js +15 -9
  30. package/lib/features/oauth/useMcpConnections.js.map +1 -1
  31. package/lib/features/store/objects/selection/ObjectsActionContext.d.ts.map +1 -1
  32. package/lib/features/store/objects/selection/ObjectsActionContext.js +2 -0
  33. package/lib/features/store/objects/selection/ObjectsActionContext.js.map +1 -1
  34. package/lib/features/store/objects/selection/SelectionActions.d.ts +4 -2
  35. package/lib/features/store/objects/selection/SelectionActions.d.ts.map +1 -1
  36. package/lib/features/store/objects/selection/SelectionActions.js +6 -3
  37. package/lib/features/store/objects/selection/SelectionActions.js.map +1 -1
  38. package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts +8 -0
  39. package/lib/features/store/objects/selection/actions/RunIntakeAction.d.ts.map +1 -0
  40. package/lib/features/store/objects/selection/actions/RunIntakeAction.js +65 -0
  41. package/lib/features/store/objects/selection/actions/RunIntakeAction.js.map +1 -0
  42. package/lib/features/store/objects/selection/actions/index.d.ts +1 -0
  43. package/lib/features/store/objects/selection/actions/index.d.ts.map +1 -1
  44. package/lib/features/store/objects/selection/actions/index.js +1 -0
  45. package/lib/features/store/objects/selection/actions/index.js.map +1 -1
  46. package/lib/features/store/types/ContentObjectTypesTable.d.ts.map +1 -1
  47. package/lib/features/store/types/ContentObjectTypesTable.js +11 -3
  48. package/lib/features/store/types/ContentObjectTypesTable.js.map +1 -1
  49. package/lib/features/store/types/IntakePolicyEditor.d.ts.map +1 -1
  50. package/lib/features/store/types/IntakePolicyEditor.js +14 -14
  51. package/lib/features/store/types/IntakePolicyEditor.js.map +1 -1
  52. package/lib/features/store/types/TypeRegistry.d.ts +275 -150
  53. package/lib/features/store/types/TypeRegistry.d.ts.map +1 -1
  54. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts +275 -150
  55. package/lib/features/store/types/search/ObjectTypeSearchContext.d.ts.map +1 -1
  56. package/lib/i18n/locales/ar.json +28 -7
  57. package/lib/i18n/locales/de.json +24 -7
  58. package/lib/i18n/locales/en.json +24 -7
  59. package/lib/i18n/locales/es.json +24 -7
  60. package/lib/i18n/locales/fr.json +25 -7
  61. package/lib/i18n/locales/it.json +24 -7
  62. package/lib/i18n/locales/ja.json +24 -7
  63. package/lib/i18n/locales/ko.json +24 -7
  64. package/lib/i18n/locales/pt.json +24 -7
  65. package/lib/i18n/locales/ru.json +26 -7
  66. package/lib/i18n/locales/tr.json +24 -7
  67. package/lib/i18n/locales/zh-TW.json +24 -7
  68. package/lib/i18n/locales/zh.json +24 -7
  69. package/lib/shell/apps/AppInstallationProvider.d.ts +4 -4
  70. package/lib/shell/login/SignInIcons.d.ts.map +1 -1
  71. package/lib/shell/login/SignInIcons.js +2 -1
  72. package/lib/shell/login/SignInIcons.js.map +1 -1
  73. package/lib/shell/login/SignInPasswordStep.d.ts +9 -0
  74. package/lib/shell/login/SignInPasswordStep.d.ts.map +1 -0
  75. package/lib/shell/login/SignInPasswordStep.js +58 -0
  76. package/lib/shell/login/SignInPasswordStep.js.map +1 -0
  77. package/lib/shell/login/SignInPrimitives.d.ts +13 -0
  78. package/lib/shell/login/SignInPrimitives.d.ts.map +1 -1
  79. package/lib/shell/login/SignInPrimitives.js +11 -1
  80. package/lib/shell/login/SignInPrimitives.js.map +1 -1
  81. package/lib/shell/login/SignInProvidersStep.d.ts +3 -1
  82. package/lib/shell/login/SignInProvidersStep.d.ts.map +1 -1
  83. package/lib/shell/login/SignInProvidersStep.js +4 -2
  84. package/lib/shell/login/SignInProvidersStep.js.map +1 -1
  85. package/lib/shell/login/SignInReturningStep.d.ts +3 -1
  86. package/lib/shell/login/SignInReturningStep.d.ts.map +1 -1
  87. package/lib/shell/login/SignInReturningStep.js +18 -8
  88. package/lib/shell/login/SignInReturningStep.js.map +1 -1
  89. package/lib/shell/login/SignInTenantStep.d.ts +3 -1
  90. package/lib/shell/login/SignInTenantStep.d.ts.map +1 -1
  91. package/lib/shell/login/SignInTenantStep.js +2 -2
  92. package/lib/shell/login/SignInTenantStep.js.map +1 -1
  93. package/lib/shell/login/SignInWithProviderButton.d.ts +5 -3
  94. package/lib/shell/login/SignInWithProviderButton.d.ts.map +1 -1
  95. package/lib/shell/login/SignInWithProviderButton.js +8 -7
  96. package/lib/shell/login/SignInWithProviderButton.js.map +1 -1
  97. package/lib/shell/login/SigninScreen.d.ts.map +1 -1
  98. package/lib/shell/login/SigninScreen.js +13 -4
  99. package/lib/shell/login/SigninScreen.js.map +1 -1
  100. package/lib/shell/login/signInUtils.d.ts +20 -4
  101. package/lib/shell/login/signInUtils.d.ts.map +1 -1
  102. package/lib/shell/login/signInUtils.js +47 -1
  103. package/lib/shell/login/signInUtils.js.map +1 -1
  104. package/lib/vertesia-ui-core.js +1 -1
  105. package/lib/vertesia-ui-core.js.map +1 -1
  106. package/lib/vertesia-ui-features.js +14 -14
  107. package/lib/vertesia-ui-features.js.map +1 -1
  108. package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js +5 -0
  109. package/lib/vertesia-ui-i18n.BoNE1APG.chunk.js.map +1 -0
  110. package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js +5 -0
  111. package/lib/vertesia-ui-i18n.BoPtiSPb.chunk.js.map +1 -0
  112. package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js +5 -0
  113. package/lib/vertesia-ui-i18n.C4BBW5v5.chunk.js.map +1 -0
  114. package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js +5 -0
  115. package/lib/vertesia-ui-i18n.C81wuNkY.chunk.js.map +1 -0
  116. package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js +5 -0
  117. package/lib/vertesia-ui-i18n.C9GipKIH.chunk.js.map +1 -0
  118. package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js +5 -0
  119. package/lib/vertesia-ui-i18n.CWsoS0W9.chunk.js.map +1 -0
  120. package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js +5 -0
  121. package/lib/vertesia-ui-i18n.Cr9NtzZB.chunk.js.map +1 -0
  122. package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js +5 -0
  123. package/lib/vertesia-ui-i18n.DEpkHOD5.chunk.js.map +1 -0
  124. package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js +5 -0
  125. package/lib/vertesia-ui-i18n.DPXaRKFO.chunk.js.map +1 -0
  126. package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js +5 -0
  127. package/lib/vertesia-ui-i18n.D_QNm1Xg.chunk.js.map +1 -0
  128. package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js +5 -0
  129. package/lib/vertesia-ui-i18n.F26Cc0f7.chunk.js.map +1 -0
  130. package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js +5 -0
  131. package/lib/vertesia-ui-i18n.YkXIESV_.chunk.js.map +1 -0
  132. package/lib/vertesia-ui-i18n.js +3 -3
  133. package/lib/vertesia-ui-i18n.js.map +1 -1
  134. package/lib/vertesia-ui-shell.js +15 -9
  135. package/lib/vertesia-ui-shell.js.map +1 -1
  136. package/lib/widgets/xml-viewer/hooks/useCollapsible.d.ts +1 -1
  137. package/package.json +7 -7
  138. package/src/core/components/SelectList.tsx +5 -2
  139. package/src/core/components/SidePanel.tsx +65 -60
  140. package/src/features/agent/chat/ModernAgentConversation.test.tsx +150 -1
  141. package/src/features/agent/chat/ModernAgentConversation.tsx +59 -21
  142. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +17 -1
  143. package/src/features/agent/chat/hooks/useAgentPlans.test.ts +52 -0
  144. package/src/features/agent/chat/hooks/useAgentPlans.ts +5 -5
  145. package/src/features/magic-pdf/GroundedExtractionView.tsx +3 -3
  146. package/src/features/oauth/McpConnectionsButton.test.tsx +56 -0
  147. package/src/features/oauth/McpConnectionsButton.tsx +9 -6
  148. package/src/features/oauth/McpConnectionsDialog.tsx +14 -5
  149. package/src/features/oauth/useMcpConnections.test.ts +10 -0
  150. package/src/features/oauth/useMcpConnections.ts +18 -10
  151. package/src/features/store/objects/selection/ObjectsActionContext.tsx +2 -0
  152. package/src/features/store/objects/selection/SelectionActions.tsx +13 -6
  153. package/src/features/store/objects/selection/actions/RunIntakeAction.tsx +69 -0
  154. package/src/features/store/objects/selection/actions/index.ts +1 -0
  155. package/src/features/store/types/ContentObjectTypesTable.tsx +20 -3
  156. package/src/features/store/types/IntakePolicyEditor.tsx +33 -28
  157. package/src/i18n/locales/ar.json +28 -7
  158. package/src/i18n/locales/de.json +24 -7
  159. package/src/i18n/locales/en.json +24 -7
  160. package/src/i18n/locales/es.json +24 -7
  161. package/src/i18n/locales/fr.json +25 -7
  162. package/src/i18n/locales/it.json +24 -7
  163. package/src/i18n/locales/ja.json +24 -7
  164. package/src/i18n/locales/ko.json +24 -7
  165. package/src/i18n/locales/pt.json +24 -7
  166. package/src/i18n/locales/ru.json +26 -7
  167. package/src/i18n/locales/tr.json +24 -7
  168. package/src/i18n/locales/zh-TW.json +24 -7
  169. package/src/i18n/locales/zh.json +24 -7
  170. package/src/shell/login/SignInIcons.tsx +2 -1
  171. package/src/shell/login/SignInPasswordRequired.test.tsx +82 -0
  172. package/src/shell/login/SignInPasswordStep.test.tsx +71 -0
  173. package/src/shell/login/SignInPasswordStep.tsx +103 -0
  174. package/src/shell/login/SignInPrimitives.tsx +53 -1
  175. package/src/shell/login/SignInProvidersStep.tsx +8 -4
  176. package/src/shell/login/SignInReturningStep.tsx +20 -8
  177. package/src/shell/login/SignInTenantStep.tsx +6 -2
  178. package/src/shell/login/SignInWithProviderButton.tsx +11 -7
  179. package/src/shell/login/SigninScreen.tsx +32 -3
  180. package/src/shell/login/signInPassword.test.ts +133 -0
  181. package/src/shell/login/signInUtils.ts +60 -7
  182. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts +0 -7
  183. package/lib/features/store/objects/selection/actions/StartWorkflowButton.d.ts.map +0 -1
  184. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js +0 -51
  185. package/lib/features/store/objects/selection/actions/StartWorkflowButton.js.map +0 -1
  186. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js +0 -5
  187. package/lib/vertesia-ui-i18n.BfN7iOHb.chunk.js.map +0 -1
  188. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js +0 -5
  189. package/lib/vertesia-ui-i18n.BjH2zEZw.chunk.js.map +0 -1
  190. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js +0 -5
  191. package/lib/vertesia-ui-i18n.BjSwAZXl.chunk.js.map +0 -1
  192. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js +0 -5
  193. package/lib/vertesia-ui-i18n.C7PUm5Au.chunk.js.map +0 -1
  194. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js +0 -5
  195. package/lib/vertesia-ui-i18n.CF_1mUS5.chunk.js.map +0 -1
  196. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js +0 -5
  197. package/lib/vertesia-ui-i18n.ChfUl3_-.chunk.js.map +0 -1
  198. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js +0 -5
  199. package/lib/vertesia-ui-i18n.D-Sk-5d1.chunk.js.map +0 -1
  200. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js +0 -5
  201. package/lib/vertesia-ui-i18n.D4rnu1_S.chunk.js.map +0 -1
  202. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js +0 -5
  203. package/lib/vertesia-ui-i18n.DDf0-laK.chunk.js.map +0 -1
  204. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js +0 -5
  205. package/lib/vertesia-ui-i18n.DN79y06J.chunk.js.map +0 -1
  206. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js +0 -5
  207. package/lib/vertesia-ui-i18n.DWLIciYQ.chunk.js.map +0 -1
  208. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js +0 -5
  209. package/lib/vertesia-ui-i18n.DvF_YaAJ.chunk.js.map +0 -1
  210. package/src/features/store/objects/selection/actions/StartWorkflowButton.tsx +0 -104
@@ -15,6 +15,7 @@ import {
15
15
  import { useCallback, useEffect, useState } from 'react';
16
16
  import SignInAuthPending from './SignInAuthPending';
17
17
  import SignInEmailStep, { type TenantInfo } from './SignInEmailStep';
18
+ import SignInPasswordStep from './SignInPasswordStep';
18
19
  import SignInProvidersStep from './SignInProvidersStep';
19
20
  import SignInRecoveryStep, { type SignInRecoveryKind } from './SignInRecoveryStep';
20
21
  import SignInRestrictedEnvStep from './SignInRestrictedEnvStep';
@@ -79,6 +80,7 @@ type Mode =
79
80
  | 'email'
80
81
  | 'providers'
81
82
  | 'tenant'
83
+ | 'password'
82
84
  | 'blocked'
83
85
  | 'returning'
84
86
  | 'pending'
@@ -185,7 +187,13 @@ function SigninScreenImpl({
185
187
  const onProceedFromEmail = useCallback((e: string, t: TenantInfo | undefined) => {
186
188
  setEmail(e);
187
189
  setTenant(t);
188
- setMode(t ? 'tenant' : 'providers');
190
+ // A password tenant has no identity provider to pick; ask for its password directly.
191
+ setMode(t?.provider === 'password' ? 'password' : t ? 'tenant' : 'providers');
192
+ }, []);
193
+
194
+ const onPasswordRequired = useCallback((e: string) => {
195
+ setEmail(e);
196
+ setMode('password');
189
197
  }, []);
190
198
 
191
199
  const onBack = useCallback(() => {
@@ -336,6 +344,14 @@ function SigninScreenImpl({
336
344
  );
337
345
  } else if (mode === 'signup' && !localStorage.getItem('tenantName')) {
338
346
  content = <SignupForm onSignup={onSignup} goBack={startOver} />;
347
+ } else if (mode === 'password' && email) {
348
+ content = (
349
+ <SignInPasswordStep
350
+ email={email}
351
+ tenantName={tenant?.label || tenant?.name || storedSession?.tenantName || undefined}
352
+ onBack={onBack}
353
+ />
354
+ );
339
355
  } else if (mode === 'tenant' && tenant) {
340
356
  content = (
341
357
  <SignInTenantStep
@@ -343,13 +359,26 @@ function SigninScreenImpl({
343
359
  tenant={tenant}
344
360
  onBack={onBack}
345
361
  onProviderClicked={() => onProviderClicked((tenant.provider ?? 'oidc') as ProviderId)}
362
+ onPasswordRequired={onPasswordRequired}
346
363
  />
347
364
  );
348
365
  } else if (mode === 'providers') {
349
- content = <SignInProvidersStep email={email} onBack={onBack} onProviderClicked={onProviderClicked} />;
366
+ content = (
367
+ <SignInProvidersStep
368
+ email={email}
369
+ onBack={onBack}
370
+ onProviderClicked={onProviderClicked}
371
+ onPasswordRequired={onPasswordRequired}
372
+ />
373
+ );
350
374
  } else if (mode === 'returning' && storedSession) {
351
375
  content = (
352
- <SignInReturningStep session={storedSession} onNotYou={onNotYou} onProviderClicked={onProviderClicked} />
376
+ <SignInReturningStep
377
+ session={storedSession}
378
+ onNotYou={onNotYou}
379
+ onProviderClicked={onProviderClicked}
380
+ onPasswordRequired={onPasswordRequired}
381
+ />
353
382
  );
354
383
  } else {
355
384
  content = <SignInEmailStep initialEmail={email} onProceed={onProceedFromEmail} />;
@@ -0,0 +1,133 @@
1
+ import { FirebaseError } from 'firebase/app';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ const mocks = vi.hoisted(() => ({
5
+ auth: { tenantId: null as string | null },
6
+ setFirebaseTenant: vi.fn(),
7
+ signInWithEmailAndPassword: vi.fn(),
8
+ signInWithRedirect: vi.fn(),
9
+ }));
10
+
11
+ vi.mock('@vertesia/ui/session', () => ({
12
+ getFirebaseAuth: () => mocks.auth,
13
+ setFirebaseTenant: mocks.setFirebaseTenant,
14
+ }));
15
+
16
+ vi.mock('firebase/auth', async (importOriginal) => ({
17
+ ...(await importOriginal<typeof import('firebase/auth')>()),
18
+ signInWithEmailAndPassword: mocks.signInWithEmailAndPassword,
19
+ signInWithRedirect: mocks.signInWithRedirect,
20
+ }));
21
+
22
+ const { passwordFailureReason, readPendingSignin, signInWithPassword, startSignIn } = await import('./signInUtils');
23
+
24
+ const PASSWORD_TENANT = {
25
+ label: 'Vertesia Reviewers',
26
+ name: 'reviewers',
27
+ firebaseTenantId: 'reviewers-abc12',
28
+ provider: 'password',
29
+ domain: ['reviewers.vertesia.io'],
30
+ };
31
+ const OIDC_TENANT = { ...PASSWORD_TENANT, label: 'Acme', name: 'acme', provider: 'oidc', domain: ['acme.com'] };
32
+ const EMAIL = 'claude@reviewers.vertesia.io';
33
+
34
+ describe('passwordFailureReason', () => {
35
+ // One answer for every credential failure, so the form cannot be used to probe which
36
+ // addresses exist or are disabled.
37
+ it.each([
38
+ 'auth/invalid-credential',
39
+ 'auth/wrong-password',
40
+ 'auth/user-not-found',
41
+ 'auth/user-disabled',
42
+ 'auth/invalid-email',
43
+ ])('folds %s into invalid-credentials', (code) => {
44
+ expect(passwordFailureReason(new FirebaseError(code, code))).toBe('invalid-credentials');
45
+ });
46
+
47
+ it('reports throttling separately', () => {
48
+ expect(passwordFailureReason(new FirebaseError('auth/too-many-requests', 'throttled'))).toBe(
49
+ 'too-many-attempts',
50
+ );
51
+ });
52
+
53
+ it('treats anything else as a generic failure', () => {
54
+ expect(passwordFailureReason(new FirebaseError('auth/network-request-failed', 'offline'))).toBe('failed');
55
+ expect(passwordFailureReason(new Error('boom'))).toBe('failed');
56
+ expect(passwordFailureReason(undefined)).toBe('failed');
57
+ });
58
+ });
59
+
60
+ describe('signInWithPassword', () => {
61
+ beforeEach(() => {
62
+ mocks.setFirebaseTenant.mockReset();
63
+ mocks.signInWithEmailAndPassword.mockReset();
64
+ mocks.signInWithRedirect.mockReset();
65
+ });
66
+
67
+ afterEach(() => {
68
+ sessionStorage.clear();
69
+ localStorage.clear();
70
+ });
71
+
72
+ it('signs a password-tenant user in place and records the pending sign-in', async () => {
73
+ mocks.setFirebaseTenant.mockResolvedValue(PASSWORD_TENANT);
74
+ mocks.signInWithEmailAndPassword.mockResolvedValue({});
75
+
76
+ await expect(signInWithPassword(EMAIL, 'secret')).resolves.toEqual({ ok: true });
77
+
78
+ expect(mocks.setFirebaseTenant).toHaveBeenCalledWith(EMAIL);
79
+ expect(mocks.signInWithEmailAndPassword).toHaveBeenCalledWith(mocks.auth, EMAIL, 'secret');
80
+ expect(readPendingSignin()).toEqual({
81
+ email: EMAIL,
82
+ provider: 'password',
83
+ tenantName: 'Vertesia Reviewers',
84
+ });
85
+ expect(mocks.signInWithRedirect).not.toHaveBeenCalled();
86
+ });
87
+
88
+ // The password form is reachable only through tenant resolution, but the helper re-checks: an
89
+ // address that resolves to a federated tenant must never be offered a password.
90
+ it('refuses an address whose tenant is not a password tenant', async () => {
91
+ mocks.setFirebaseTenant.mockResolvedValue(OIDC_TENANT);
92
+
93
+ await expect(signInWithPassword('someone@acme.com', 'secret')).resolves.toEqual({
94
+ ok: false,
95
+ reason: 'not-password-tenant',
96
+ });
97
+ expect(mocks.signInWithEmailAndPassword).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('fails without signing in when the tenant cannot be resolved', async () => {
101
+ mocks.setFirebaseTenant.mockResolvedValue(undefined);
102
+
103
+ await expect(signInWithPassword(EMAIL, 'secret')).resolves.toEqual({ ok: false, reason: 'failed' });
104
+ expect(mocks.signInWithEmailAndPassword).not.toHaveBeenCalled();
105
+ });
106
+
107
+ it('clears the pending sign-in when Firebase rejects the credential', async () => {
108
+ mocks.setFirebaseTenant.mockResolvedValue(PASSWORD_TENANT);
109
+ mocks.signInWithEmailAndPassword.mockRejectedValue(new FirebaseError('auth/invalid-credential', 'bad'));
110
+
111
+ await expect(signInWithPassword(EMAIL, 'wrong')).resolves.toEqual({
112
+ ok: false,
113
+ reason: 'invalid-credentials',
114
+ });
115
+ expect(readPendingSignin()).toBeNull();
116
+ });
117
+ });
118
+
119
+ describe('startSignIn', () => {
120
+ beforeEach(() => {
121
+ mocks.setFirebaseTenant.mockReset();
122
+ mocks.signInWithRedirect.mockReset();
123
+ });
124
+
125
+ // A returning session's stored provider can go stale; a password tenant has nowhere to redirect.
126
+ it('does not redirect when the address resolves to a password tenant', async () => {
127
+ mocks.setFirebaseTenant.mockResolvedValue(PASSWORD_TENANT);
128
+
129
+ await expect(startSignIn('google', EMAIL)).resolves.toEqual({ ok: false, reason: 'password-required' });
130
+ expect(mocks.signInWithRedirect).not.toHaveBeenCalled();
131
+ expect(readPendingSignin()).toBeNull();
132
+ });
133
+ });
@@ -1,16 +1,21 @@
1
1
  import { getFirebaseAuth, setFirebaseTenant } from '@vertesia/ui/session';
2
+ import { FirebaseError } from 'firebase/app';
2
3
  import {
3
4
  type AuthProvider,
4
5
  signOut as firebaseSignOut,
5
6
  GithubAuthProvider,
6
7
  GoogleAuthProvider,
7
8
  OAuthProvider,
9
+ signInWithEmailAndPassword,
8
10
  signInWithRedirect,
9
11
  } from 'firebase/auth';
10
12
 
11
13
  // Matches auth-tenants.json `provider` values. Tenant context is derived from
12
14
  // email resolution at sign-in time, not encoded here.
13
- export type ProviderId = 'google' | 'github' | 'microsoft' | 'oidc';
15
+ export type ProviderId = 'google' | 'github' | 'microsoft' | 'oidc' | 'password';
16
+
17
+ /** Providers that sign in by redirecting to an identity provider. `password` signs in in place. */
18
+ export type RedirectProviderId = Exclude<ProviderId, 'password'>;
14
19
 
15
20
  export interface LastSuccessfulLogin {
16
21
  email: string;
@@ -140,28 +145,28 @@ const buildOidcProvider: ProviderBuilder = (email) => {
140
145
  return p;
141
146
  };
142
147
 
143
- const PROVIDER_BUILDERS: Record<ProviderId, ProviderBuilder> = {
148
+ const PROVIDER_BUILDERS: Record<RedirectProviderId, ProviderBuilder> = {
144
149
  google: buildGoogleProvider,
145
150
  github: buildGithubProvider,
146
151
  microsoft: buildMicrosoftProvider,
147
152
  oidc: buildOidcProvider,
148
153
  };
149
154
 
150
- function buildFirebaseProvider(idp: ProviderId, email?: string, redirectTo?: string): AuthProvider {
155
+ function buildFirebaseProvider(idp: RedirectProviderId, email?: string, redirectTo?: string): AuthProvider {
151
156
  return PROVIDER_BUILDERS[idp](email, redirectTo);
152
157
  }
153
158
 
154
159
  export async function startSignIn(
155
- provider: ProviderId,
160
+ provider: RedirectProviderId,
156
161
  email: string,
157
162
  redirectTo?: string,
158
- ): Promise<{ ok: true } | { ok: false; reason: 'no-email' }> {
163
+ ): Promise<{ ok: true } | { ok: false; reason: 'no-email' | 'password-required' }> {
159
164
  if (!email) return { ok: false, reason: 'no-email' };
160
165
 
161
166
  // A tenant-mapped email uses the tenant's provider (overriding `provider`); otherwise use `provider`.
162
167
  const tenant = await setFirebaseTenant(email);
163
168
  const auth = getFirebaseAuth();
164
- let effectiveIdp = provider;
169
+ let effectiveIdp: ProviderId = provider;
165
170
  let tenantName: string | undefined;
166
171
 
167
172
  if (tenant) {
@@ -175,13 +180,61 @@ export async function startSignIn(
175
180
  if (auth.tenantId) auth.tenantId = null;
176
181
  }
177
182
 
183
+ // A password tenant has no identity provider to redirect to; its step collects the password.
184
+ if (effectiveIdp === 'password') return { ok: false, reason: 'password-required' };
185
+
178
186
  writePendingSignin({ email, provider: effectiveIdp, tenantName });
179
187
  void signInWithRedirect(auth, buildFirebaseProvider(effectiveIdp, email, redirectTo));
180
188
  return { ok: true };
181
189
  }
182
190
 
191
+ export type PasswordSignInFailure = 'not-password-tenant' | 'invalid-credentials' | 'too-many-attempts' | 'failed';
192
+
193
+ // Firebase reports an unknown user, a wrong password and a disabled user with distinct codes on
194
+ // some SDK versions; they are folded into one answer so the form does not reveal which it was.
195
+ const INVALID_CREDENTIAL_CODES = new Set([
196
+ 'auth/invalid-credential',
197
+ 'auth/invalid-email',
198
+ 'auth/user-disabled',
199
+ 'auth/user-not-found',
200
+ 'auth/wrong-password',
201
+ ]);
202
+
203
+ export function passwordFailureReason(err: unknown): Exclude<PasswordSignInFailure, 'not-password-tenant'> {
204
+ if (!(err instanceof FirebaseError)) return 'failed';
205
+ if (INVALID_CREDENTIAL_CODES.has(err.code)) return 'invalid-credentials';
206
+ if (err.code === 'auth/too-many-requests') return 'too-many-attempts';
207
+ return 'failed';
208
+ }
209
+
210
+ /**
211
+ * Signs in a user of a password tenant, in place — there is no redirect. Only an address whose
212
+ * domain resolves to a tenant configured for password sign-in may use it; the tenant is resolved
213
+ * again here rather than trusted from the caller, so a returning session cannot carry a stale one.
214
+ * On success the session's auth-state listener completes the login.
215
+ */
216
+ export async function signInWithPassword(
217
+ email: string,
218
+ password: string,
219
+ ): Promise<{ ok: true } | { ok: false; reason: PasswordSignInFailure }> {
220
+ const tenant = await setFirebaseTenant(email);
221
+ if (!tenant) return { ok: false, reason: 'failed' };
222
+ if (tenant.provider !== 'password') return { ok: false, reason: 'not-password-tenant' };
223
+
224
+ const tenantName = tenant.label || tenant.name || undefined;
225
+ localStorage.setItem('tenantName', tenantName ?? '');
226
+ writePendingSignin({ email, provider: 'password', tenantName });
227
+ try {
228
+ await signInWithEmailAndPassword(getFirebaseAuth(), email, password);
229
+ return { ok: true };
230
+ } catch (err: unknown) {
231
+ clearPendingSignin();
232
+ return { ok: false, reason: passwordFailureReason(err) };
233
+ }
234
+ }
235
+
183
236
  /** Starts a provider sign-in directly, skipping email/tenant resolution and clearing any tenant routing. */
184
- export function startSignInWithoutTenant(provider: ProviderId, redirectTo?: string): void {
237
+ export function startSignInWithoutTenant(provider: RedirectProviderId, redirectTo?: string): void {
185
238
  const auth = getFirebaseAuth();
186
239
  localStorage.removeItem('tenantName');
187
240
  if (auth.tenantId) auth.tenantId = null;
@@ -1,7 +0,0 @@
1
- /**
2
- * Standalone "Start Workflow" button for the content objects toolbar. Renders only when objects are
3
- * selected; opens a modal to pick a workflow rule and runs it on the selected object ids. Gated by
4
- * the caller via the `allowWorkflowRun` prop on {@link SelectionActions}.
5
- */
6
- export declare function StartWorkflowButton(): import("react").JSX.Element | null;
7
- //# sourceMappingURL=StartWorkflowButton.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"StartWorkflowButton.d.ts","sourceRoot":"","sources":["../../../../../../src/features/store/objects/selection/actions/StartWorkflowButton.tsx"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,wBAAgB,mBAAmB,uCAelC"}
@@ -1,51 +0,0 @@
1
- import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Button, Modal, ModalBody, ModalFooter, ModalTitle, SelectList, Spinner, useFetch, useToast, } from '@vertesia/ui/core';
3
- import { useUITranslation } from '@vertesia/ui/i18n';
4
- import { useUserSession } from '@vertesia/ui/session';
5
- import { useState } from 'react';
6
- import { useDocumentSelection } from '../../DocumentSelectionProvider.js';
7
- /**
8
- * Standalone "Start Workflow" button for the content objects toolbar. Renders only when objects are
9
- * selected; opens a modal to pick a workflow rule and runs it on the selected object ids. Gated by
10
- * the caller via the `allowWorkflowRun` prop on {@link SelectionActions}.
11
- */
12
- export function StartWorkflowButton() {
13
- const { t } = useUITranslation();
14
- const selection = useDocumentSelection();
15
- const [isOpen, setOpen] = useState(false);
16
- if (!selection?.hasSelection()) {
17
- return null;
18
- }
19
- return (_jsxs(_Fragment, { children: [_jsx(Button, { onClick: () => setOpen(true), children: t('store.actions.startWorkflow') }), isOpen && _jsx(StartWorkflowModal, { objectIds: selection.getObjectIds(), onClose: () => setOpen(false) })] }));
20
- }
21
- function StartWorkflowModal({ objectIds, onClose }) {
22
- const { t } = useUITranslation();
23
- const { client } = useUserSession();
24
- const toast = useToast();
25
- const [selected, setSelected] = useState(undefined);
26
- const [isStarting, setIsStarting] = useState(false);
27
- const { data: rules, isLoading } = useFetch(() => client.store.workflows.rules.list(), []);
28
- const onStart = async () => {
29
- if (!selected || isStarting)
30
- return;
31
- setIsStarting(true);
32
- try {
33
- await client.store.workflows.rules.execute(selected.id, objectIds);
34
- toast({ title: t('store.actions.workflowStarted'), status: 'success', duration: 3000 });
35
- onClose();
36
- }
37
- catch (err) {
38
- toast({
39
- title: t('store.actions.errorStartingWorkflow'),
40
- description: err instanceof Error ? err.message : undefined,
41
- status: 'error',
42
- duration: 9000,
43
- });
44
- }
45
- finally {
46
- setIsStarting(false);
47
- }
48
- };
49
- return (_jsxs(Modal, { isOpen: true, onClose: onClose, size: "lg", children: [_jsx(ModalTitle, { children: t('store.actions.startWorkflowByRule') }), _jsxs(ModalBody, { children: [_jsx("div", { className: "pb-2", children: t('store.actions.chooseWorkflowRule') }), isLoading ? (_jsx("div", { className: "flex justify-center py-4", children: _jsx(Spinner, {}) })) : (_jsx("div", { className: "max-h-[420px] overflow-y-auto border-border border rounded-md", children: _jsx(SelectList, { className: "text-start w-full", options: rules ?? [], value: selected, onChange: setSelected, optionLayout: (rule) => ({ label: rule.name }) }) }))] }), _jsxs(ModalFooter, { align: "right", children: [_jsx(Button, { variant: "outline", onClick: onClose, disabled: isStarting, children: "Cancel" }), _jsx(Button, { onClick: onStart, isDisabled: !selected || isStarting, isLoading: isStarting, children: "Start" })] })] }));
50
- }
51
- //# sourceMappingURL=StartWorkflowButton.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"StartWorkflowButton.js","sourceRoot":"","sources":["../../../../../../src/features/store/objects/selection/actions/StartWorkflowButton.tsx"],"names":[],"mappings":"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"}