@vertesia/ui 0.81.0 → 0.81.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 (111) hide show
  1. package/lib/esm/core/components/shadcn/dialog.js +7 -3
  2. package/lib/esm/core/components/shadcn/dialog.js.map +1 -1
  3. package/lib/esm/core/components/shadcn/popover.js +3 -1
  4. package/lib/esm/core/components/shadcn/popover.js.map +1 -1
  5. package/lib/esm/core/components/shadcn/selectBox.js +1 -1
  6. package/lib/esm/core/components/shadcn/selectBox.js.map +1 -1
  7. package/lib/esm/core/components/shadcn/tooltip.js +5 -1
  8. package/lib/esm/core/components/shadcn/tooltip.js.map +1 -1
  9. package/lib/esm/features/agent/PayloadBuilder.js +22 -0
  10. package/lib/esm/features/agent/PayloadBuilder.js.map +1 -1
  11. package/lib/esm/features/agent/chat/ModernAgentConversation.js +6 -62
  12. package/lib/esm/features/agent/chat/ModernAgentConversation.js.map +1 -1
  13. package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js +1 -1
  14. package/lib/esm/features/agent/chat/ModernAgentOutput/Header.js.map +1 -1
  15. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageInput.js +1 -1
  16. package/lib/esm/features/agent/chat/ModernAgentOutput/MessageInput.js.map +1 -1
  17. package/lib/esm/features/facets/AgentRunnerFacetsNav.js +86 -0
  18. package/lib/esm/features/facets/AgentRunnerFacetsNav.js.map +1 -0
  19. package/lib/esm/features/facets/index.js +1 -0
  20. package/lib/esm/features/facets/index.js.map +1 -1
  21. package/lib/esm/features/store/collections/CreateCollection.js +1 -1
  22. package/lib/esm/features/store/collections/CreateCollection.js.map +1 -1
  23. package/lib/esm/features/store/collections/EditCollectionView.js +4 -6
  24. package/lib/esm/features/store/collections/EditCollectionView.js.map +1 -1
  25. package/lib/esm/features/store/objects/components/PropertiesEditorModal.js +25 -39
  26. package/lib/esm/features/store/objects/components/PropertiesEditorModal.js.map +1 -1
  27. package/lib/esm/features/store/objects/components/useContentPanelHooks.js +3 -3
  28. package/lib/esm/features/store/objects/components/useContentPanelHooks.js.map +1 -1
  29. package/lib/esm/features/store/types/ObjectSchemaEditor.js +4 -6
  30. package/lib/esm/features/store/types/ObjectSchemaEditor.js.map +1 -1
  31. package/lib/esm/features/store/types/TableLayoutEditor.js +4 -6
  32. package/lib/esm/features/store/types/TableLayoutEditor.js.map +1 -1
  33. package/lib/esm/widgets/index.js +1 -1
  34. package/lib/esm/widgets/index.js.map +1 -1
  35. package/lib/esm/widgets/json-view/JSONCode.js +16 -151
  36. package/lib/esm/widgets/json-view/JSONCode.js.map +1 -1
  37. package/lib/esm/widgets/{codemirror → monacoEditor}/MonacoEditor.js +18 -6
  38. package/lib/esm/widgets/monacoEditor/MonacoEditor.js.map +1 -0
  39. package/lib/esm/widgets/monacoEditor/index.js +2 -0
  40. package/lib/esm/widgets/monacoEditor/index.js.map +1 -0
  41. package/lib/tsconfig.tsbuildinfo +1 -1
  42. package/lib/types/core/components/shadcn/dialog.d.ts.map +1 -1
  43. package/lib/types/core/components/shadcn/popover.d.ts.map +1 -1
  44. package/lib/types/core/components/shadcn/tooltip.d.ts.map +1 -1
  45. package/lib/types/features/agent/PayloadBuilder.d.ts +1 -0
  46. package/lib/types/features/agent/PayloadBuilder.d.ts.map +1 -1
  47. package/lib/types/features/agent/chat/ModernAgentConversation.d.ts.map +1 -1
  48. package/lib/types/features/facets/AgentRunnerFacetsNav.d.ts +14 -0
  49. package/lib/types/features/facets/AgentRunnerFacetsNav.d.ts.map +1 -0
  50. package/lib/types/features/facets/index.d.ts +1 -0
  51. package/lib/types/features/facets/index.d.ts.map +1 -1
  52. package/lib/types/features/store/collections/EditCollectionView.d.ts.map +1 -1
  53. package/lib/types/features/store/objects/components/PropertiesEditorModal.d.ts.map +1 -1
  54. package/lib/types/features/store/objects/components/useContentPanelHooks.d.ts.map +1 -1
  55. package/lib/types/features/store/types/ObjectSchemaEditor.d.ts.map +1 -1
  56. package/lib/types/features/store/types/TableLayoutEditor.d.ts.map +1 -1
  57. package/lib/types/widgets/index.d.ts +1 -1
  58. package/lib/types/widgets/index.d.ts.map +1 -1
  59. package/lib/types/widgets/json-view/JSONCode.d.ts +2 -19
  60. package/lib/types/widgets/json-view/JSONCode.d.ts.map +1 -1
  61. package/lib/types/widgets/{codemirror → monacoEditor}/MonacoEditor.d.ts +6 -3
  62. package/lib/types/widgets/monacoEditor/MonacoEditor.d.ts.map +1 -0
  63. package/lib/types/widgets/monacoEditor/index.d.ts +3 -0
  64. package/lib/types/widgets/monacoEditor/index.d.ts.map +1 -0
  65. package/lib/vertesia-ui-core.js +1 -1
  66. package/lib/vertesia-ui-core.js.map +1 -1
  67. package/lib/vertesia-ui-features.js +1 -1
  68. package/lib/vertesia-ui-features.js.map +1 -1
  69. package/lib/vertesia-ui-widgets.js +1 -1
  70. package/lib/vertesia-ui-widgets.js.map +1 -1
  71. package/package.json +6 -10
  72. package/src/core/components/shadcn/calendar.tsx +2 -2
  73. package/src/core/components/shadcn/dialog.tsx +23 -19
  74. package/src/core/components/shadcn/filters/index.ts +1 -1
  75. package/src/core/components/shadcn/popover.tsx +3 -1
  76. package/src/core/components/shadcn/selectBox.tsx +2 -2
  77. package/src/core/components/shadcn/tooltip.tsx +20 -16
  78. package/src/features/agent/PayloadBuilder.tsx +28 -0
  79. package/src/features/agent/chat/ModernAgentConversation.tsx +6 -104
  80. package/src/features/agent/chat/ModernAgentOutput/Header.tsx +1 -1
  81. package/src/features/agent/chat/ModernAgentOutput/MessageInput.tsx +1 -1
  82. package/src/features/facets/AgentRunnerFacetsNav.tsx +125 -0
  83. package/src/features/facets/index.ts +1 -0
  84. package/src/features/store/collections/CreateCollection.tsx +1 -1
  85. package/src/features/store/collections/EditCollectionView.tsx +10 -8
  86. package/src/features/store/objects/components/PropertiesEditorModal.tsx +36 -51
  87. package/src/features/store/objects/components/useContentPanelHooks.ts +3 -3
  88. package/src/features/store/types/ObjectSchemaEditor.tsx +9 -7
  89. package/src/features/store/types/TableLayoutEditor.tsx +9 -7
  90. package/src/session/auth/auth-flow.md +1094 -0
  91. package/src/widgets/index.ts +1 -1
  92. package/src/widgets/json-view/JSONCode.tsx +30 -172
  93. package/src/widgets/{codemirror → monacoEditor}/MonacoEditor.tsx +30 -10
  94. package/src/widgets/monacoEditor/index.ts +3 -0
  95. package/lib/esm/widgets/codemirror/CodeMirrorEditor.js +0 -103
  96. package/lib/esm/widgets/codemirror/CodeMirrorEditor.js.map +0 -1
  97. package/lib/esm/widgets/codemirror/CodemirrorStateSingleton.js +0 -33
  98. package/lib/esm/widgets/codemirror/CodemirrorStateSingleton.js.map +0 -1
  99. package/lib/esm/widgets/codemirror/MonacoEditor.js.map +0 -1
  100. package/lib/esm/widgets/codemirror/index.js +0 -3
  101. package/lib/esm/widgets/codemirror/index.js.map +0 -1
  102. package/lib/types/widgets/codemirror/CodeMirrorEditor.d.ts +0 -24
  103. package/lib/types/widgets/codemirror/CodeMirrorEditor.d.ts.map +0 -1
  104. package/lib/types/widgets/codemirror/CodemirrorStateSingleton.d.ts +0 -15
  105. package/lib/types/widgets/codemirror/CodemirrorStateSingleton.d.ts.map +0 -1
  106. package/lib/types/widgets/codemirror/MonacoEditor.d.ts.map +0 -1
  107. package/lib/types/widgets/codemirror/index.d.ts +0 -5
  108. package/lib/types/widgets/codemirror/index.d.ts.map +0 -1
  109. package/src/widgets/codemirror/CodeMirrorEditor.tsx +0 -122
  110. package/src/widgets/codemirror/CodemirrorStateSingleton.tsx +0 -39
  111. package/src/widgets/codemirror/index.ts +0 -6
@@ -0,0 +1,1094 @@
1
+ # User Authentication Flow
2
+
3
+ This document describes the authentication flow for the Vertesia user-facing applications.
4
+
5
+ ## Overview
6
+
7
+ The authentication system uses:
8
+ - **Firebase** for identity management and OAuth provider integration
9
+ - **Central Auth** (internal-auth.vertesia.app) for centralized authentication flow
10
+ - **STS (Security Token Service)** (sts.vertesia.io) to validate Firebase tokens and generate Vertesia JWT tokens
11
+ - **Session state management** to validate redirects and prevent CSRF attacks
12
+
13
+ ### STS Endpoints
14
+ - Production/Preview: `https://sts.vertesia.io`
15
+ - Staging: `https://sts-staging.vertesia.io`
16
+
17
+ **Important**: STS is the single source of truth for generating Vertesia JWT tokens. Both authentication paths (Central Auth and Direct Firebase) ultimately call STS to get the JWT.
18
+
19
+ ## Key Components
20
+
21
+ - **UserSession** (UserSession.ts): Core session class managing auth state, client configuration, and user data
22
+ - **UserSessionProvider** (UserSessionProvider.tsx): React context provider orchestrating the auth flow
23
+ - **SigninScreen** (SigninScreen.tsx): UI component showing login options and handling signup
24
+
25
+ ---
26
+
27
+ ## Complete Authentication Flow
28
+
29
+ ```
30
+ ┌─────────────────────────────────────────────────────────────────────────────┐
31
+ │ APP LOADS (NO AUTHENTICATION) │
32
+ └────────────────────────────────────────────┬────────────────────────────────┘
33
+
34
+
35
+ ┌─────────────────────────────┐
36
+ │ UserSessionProvider mounts │
37
+ │ │
38
+ │ 1. Parse URL hash: │
39
+ │ token=..., state=... │
40
+ │ 2. Get account/project from │
41
+ │ URL (?a=...&p=...) or │
42
+ │ localStorage │
43
+ │ 3. Clear URL hash │
44
+ │ 4. Setup idempotency guard │
45
+ │ 5. Create new UserSession │
46
+ │ (authToken = undefined) │
47
+ └──────────┬──────────────────┘
48
+
49
+
50
+ ┌─────────────────────────────┐
51
+ │ Has token & state in URL? │
52
+ └─────┬───────────────┬───────┘
53
+ YES NO
54
+ │ │
55
+ ┌───────────────────────────┘ └───────────────────────────┐
56
+ ▼ ▼
57
+ ┌────────────────────┐ ┌────────────────────┐
58
+ │ ═══════════════════│ │ ═══════════════════│
59
+ │ PATH A: TOKEN FLOW │ │ PATH B: NO TOKEN │
60
+ │ ═══════════════════│ │ ═══════════════════│
61
+ │ │ │ │
62
+ │ (Returning from │ │ (First load or │
63
+ │ central auth) │ │ no URL token) │
64
+ └────────┬───────────┘ └────────┬───────────┘
65
+ │ │
66
+ ▼ ▼
67
+ ┌────────────────────┐ ┌────────────────────┐
68
+ │ verifyState() │ │ Setup Firebase │
69
+ └────┬───────────┬───┘ │ onAuthStateChanged │
70
+ YES NO │ listener │
71
+ │ │ └────────┬───────────┘
72
+ │ ▼ │
73
+ │ ┌────────────────┐ │
74
+ │ │ Invalid state! │ │
75
+ │ │ Log error │ │
76
+ │ │ Show │ │
77
+ │ │ SigninScreen │ │
78
+ │ └────────────────┘ │
79
+ │ │
80
+ ▼ ▼
81
+ ┌────────────────────┐ ┌────────────────────────┐
82
+ │ clearState() │ │ Show SigninScreen │
83
+ └────────┬───────────┘ │ │
84
+ │ │ shouldRedirectTo │
85
+ ▼ │ CentralAuth()? │
86
+ ┌────────────────────┐ └──────┬─────────────────┘
87
+ │ getComposableToken │ YES
88
+ │ (with URL token) │ │
89
+ │ │ ▼
90
+ │ IMPORTANT: │ ┌──────────────────────────┐
91
+ │ This token is │ │ Show "Continue with │
92
+ │ already a │ │ Central Auth" button │
93
+ │ VERTESIA JWT! │ │ │
94
+ └────────┬───────────┘ │ OR │
95
+ │ │ │
96
+ ▼ │ Show login options: │
97
+ ┌────────────────────┐ │ - GoogleSignInButton │
98
+ │ SUCCESS? │ │ - GitHubSignInButton │
99
+ └────┬───────────┬───┘ │ - MicrosoftSignInButton │
100
+ YES NO │ - EnterpriseSigninButton │
101
+ │ │ └──────┬───────────────────┘
102
+ │ ▼ │
103
+ │ ┌──────────────────┐ │
104
+ │ │ Error Type? │ ┌───────────────┴───────────────┐
105
+ │ └──┬───────────┬───┘ │ │
106
+ │ │ │ ┌───────▼────────┐ ┌──────────────┐ │
107
+ │ UserNot Other │ OPTION 1: │ │ OPTION 2: │ │
108
+ │ Found Error │ CENTRAL AUTH │ │ STANDARD │ │
109
+ │ │ │ │ (OAuth-like) │ │ SIGNIN │ │
110
+ │ ▼ ▼ └───────┬────────┘ └──────┬───────┘ │
111
+ │ ┌───────┐ ┌──────────────────┐ │ │ │
112
+ │ │ Show │ │ Show SigninScreen│ │ │ │
113
+ │ │signup │ │ with error msg │ │ │ │
114
+ │ │ flow │ │ (authError set) │ │ ┌────▼──────────▼────┐
115
+ │ └───────┘ └──────────────────┘ │ │ OPTION 3: │
116
+ │ │ │ SSO SIGNIN (SAML) │
117
+ │ │ └────┬───────────────┘
118
+ ▼ │ │
119
+ ┌────────────────────┐ │ │
120
+ │ session.login(JWT) │ ◄──────────────────┼───────────────────┘
121
+ │ │ │
122
+ │ Inside login(): │ │
123
+ │ 1. authError=undef │ │
124
+ │ 2. isLoading=false │ │
125
+ │ 3. Decode JWT │ │
126
+ │ 4. Set auth │ │
127
+ │ callback │ │
128
+ │ 5. Save to │ │
129
+ │ localStorage │ │
130
+ │ 6. Notify │ │
131
+ │ Env.onLogin() │ │
132
+ │ 7. Promise.all([ │ │
133
+ │ _loadTypes(), │ │
134
+ │ fetchOnboard │ │
135
+ │ ingStatus() │ │
136
+ │ ]) │ │
137
+ └────────┬───────────┘ │
138
+ │ │
139
+ ▼ │
140
+ ┌────────────────────┐ │
141
+ │ setSession() │ │
142
+ │ Update state │ │
143
+ │ │ │
144
+ │ USER LOGGED IN! │ │
145
+ │ Show main app │ │
146
+ └────────────────────┘ │
147
+
148
+
149
+ ┌──────────────────────────────────────────┼──────────────────────────────────┐
150
+ │ │ │
151
+ │ DETAILED: SIGNIN OPTIONS │ │
152
+ │ │ │
153
+ └──────────────────────────────────────────┼──────────────────────────────────┘
154
+
155
+ ┌───────────────────────────────┴────────────────────────┐
156
+ │ │
157
+ ▼ │
158
+ ┌────────────────────────┐ ┌────────────────────────┐ ┌─────▼─────────────┐
159
+ │ OPTION 1: │ │ OPTION 2: │ │ OPTION 3: │
160
+ │ CENTRAL AUTH │ │ STANDARD SIGNIN │ │ SSO SIGNIN │
161
+ │ ═════════════ │ │ ═══════════════ │ │ ══════════ │
162
+ └────────┬───────────────┘ └────────┬───────────────┘ └─────┬─────────────┘
163
+ │ │ │
164
+ ▼ ▼ ▼
165
+ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
166
+ │ Generate state │ │ User clicks: │ │ User enters: │
167
+ │ Save to session │ │ - Google │ │ - Company email │
168
+ │ Storage (5 min) │ │ - GitHub │ │ or domain │
169
+ └──────────┬───────────┘ │ - Microsoft │ └──────────┬───────────┘
170
+ │ └──────────┬───────────┘ │
171
+ ▼ │ ▼
172
+ ┌──────────────────────┐ │ ┌──────────────────────┐
173
+ │ Get account/ │ ▼ │ Lookup SSO │
174
+ │ project from URL or │ ┌──────────────────────┐ │ provider config │
175
+ │ localStorage │ │ Call Firebase: │ │ for domain │
176
+ └──────────┬───────────┘ │ signInWith...() │ └──────────┬───────────┘
177
+ │ │ - Google │ │
178
+ ▼ │ - GitHub │ ▼
179
+ ┌──────────────────────┐ │ - Microsoft │ ┌──────────────────────┐
180
+ │ Build redirect URL: │ └──────────┬───────────┘ │ Redirect to SSO │
181
+ │ │ │ │ provider (Okta, │
182
+ │ internal-auth │ ▼ │ Azure AD, etc.) │
183
+ │ .vertesia.app │ ┌──────────────────────┐ └──────────┬───────────┘
184
+ │ ?sts=... │ │ Firebase OAuth │ │
185
+ │ &redirect_uri=... │ │ redirect & return │ ▼
186
+ │ &state=... │ └──────────┬───────────┘ ┌──────────────────────┐
187
+ └──────────┬───────────┘ │ │ User auth at SSO │
188
+ │ ▼ └──────────┬───────────┘
189
+ ▼ ┌──────────────────────┐ │
190
+ ┌──────────────────────┐ │ Firebase auth │ ▼
191
+ │ window.location │ │ success! │ ┌──────────────────────┐
192
+ │ .replace() to │ │ │ │ SAML response │
193
+ │ central auth │ │ Firebase creates │ │ back to app │
194
+ └──────────┬───────────┘ │ session │ └──────────┬───────────┘
195
+ │ └──────────┬───────────┘ │
196
+ ▼ │ ▼
197
+ ┌──────────────────────┐ │ ┌──────────────────────┐
198
+ │ Central Auth Page │ │ │ Exchange SAML for │
199
+ │ │ │ │ Firebase custom │
200
+ │ User authenticates │ │ │ token │
201
+ │ Gets Firebase token │ │ └──────────┬───────────┘
202
+ └──────────┬───────────┘ │ │
203
+ │ │ ▼
204
+ ▼ │ ┌──────────────────────┐
205
+ ┌──────────────────────┐ │ │ Sign in to Firebase │
206
+ │ Central auth calls │ │ │ with custom token │
207
+ │ STS server: │ │ └──────────┬───────────┘
208
+ │ │ │ │
209
+ │ POST │ │ │
210
+ │ sts.vertesia.io │ │ │
211
+ │ /token/issue │ │ │
212
+ │ │ │ │
213
+ │ Authorization: │ │ │
214
+ │ Bearer <Firebase> │ │ │
215
+ │ │ │ │
216
+ │ Body: { │ │ │
217
+ │ type: 'user', │ │ │
218
+ │ account_id, │ │ │
219
+ │ project_id │ │ │
220
+ │ } │ │ │
221
+ └──────────┬───────────┘ │ │
222
+ │ │ │
223
+ ▼ │ │
224
+ ┌──────────────────────┐ │ │
225
+ │ STS validates │ │ │
226
+ │ Firebase token and │ │ │
227
+ │ generates Vertesia │ │ │
228
+ │ JWT │ │ │
229
+ └──────────┬───────────┘ │ │
230
+ │ │ │
231
+ ▼ │ │
232
+ ┌──────────────────────┐ │ │
233
+ │ Redirect back to app │ │ │
234
+ │ with: │ │ │
235
+ │ │ │ │
236
+ │ #token=<VERTESIA_JWT>│ │ │
237
+ │ &state=<state> │ │ │
238
+ └──────────┬───────────┘ │ │
239
+ │ │ │
240
+ │ ▼ ▼
241
+ │ ┌────────────────────────────────────────────┐
242
+ │ │ ★ TRIGGERS onAuthStateChanged listener ★ │
243
+ │ │ │
244
+ │ │ This Firebase listener is triggered by: │
245
+ │ │ │
246
+ │ │ 1. Page load/mount (always) │
247
+ │ │ │
248
+ │ │ 2. Firebase sign-in from: │
249
+ │ │ - Google/GitHub/MS OAuth (Option 2) │
250
+ │ │ - Enterprise SSO/SAML (Option 3) │
251
+ │ │ │
252
+ │ │ 3. Firebase sign-out (session.logout()) │
253
+ │ │ │
254
+ │ │ 4. Token refresh/expiry (automatic) │
255
+ │ └────────────┬───────────────────────────────┘
256
+ │ │
257
+ │ ▼
258
+ │ ┌────────────────────────────────┐
259
+ │ │ Firebase user exists? │
260
+ │ └─────┬──────────────────┬───────┘
261
+ │ YES NO
262
+ │ │ │
263
+ │ ▼ ▼
264
+ │ ┌────────────────────┐ ┌────────────────────┐
265
+ │ │ RETURNING USER │ │ NEW/LOGGED OUT │
266
+ │ │ │ │ USER │
267
+ │ │ getComposableToken │ │ │
268
+ │ │ calls STS: │ │ session.authToken │
269
+ │ │ │ │ stays undefined │
270
+ │ │ POST │ │ │
271
+ │ │ sts.vertesia.io │ │ SigninScreen shows │
272
+ │ │ /token/issue │ │ (!session||!user) │
273
+ │ │ │ └────────────────────┘
274
+ │ │ Authorization: │
275
+ │ │ Bearer <Firebase> │
276
+ │ │ │
277
+ │ │ Body: { │
278
+ │ │ type: 'user', │
279
+ │ │ account_id, │
280
+ │ │ project_id │
281
+ │ │ } │
282
+ │ │ │
283
+ │ │ STS returns │
284
+ │ │ Vertesia JWT │
285
+ │ └────────┬───────────┘
286
+ │ │
287
+ │ ▼
288
+ │ ┌────────────────────┐
289
+ │ │ session.login(JWT) │
290
+ │ └────────┬───────────┘
291
+ │ │
292
+ │ ▼
293
+ │ ┌────────────────────┐
294
+ │ │ SUCCESS? │
295
+ │ └────┬───────────┬───┘
296
+ │ YES NO
297
+ │ │ │
298
+ │ │ ▼
299
+ │ │ ┌──────────────────┐
300
+ │ │ │ UserNotFoundError│
301
+ │ │ │ → Show signup │
302
+ │ │ │ │
303
+ │ │ │ Other Error │
304
+ │ │ │ → logout() │
305
+ │ │ │ → Show error │
306
+ │ │ └──────────────────┘
307
+ │ │
308
+ │ ▼
309
+ │ ┌────────────────────┐
310
+ │ │ Update state │
311
+ │ │ User logged in │
312
+ │ │ Show main app │
313
+ │ └────────────────────┘
314
+ │ │
315
+ └────────────────┘
316
+
317
+
318
+ ┌────────────────────┐
319
+ │ ═══════════════════│
320
+ │ ALL PATHS END HERE │
321
+ │ ═══════════════════│
322
+ │ │
323
+ │ User authenticated │
324
+ │ Main app shown │
325
+ └────────────────────┘
326
+ ```
327
+
328
+ ## Key Differences Between Authentication Paths
329
+
330
+ ### Path A: Central Auth (Option 1) - Returns with JWT
331
+ 1. User clicks "Continue with Central Auth"
332
+ 2. Redirects to internal-auth.vertesia.app
333
+ 3. Central auth authenticates user and gets Firebase token
334
+ 4. **Central auth calls STS** (`POST sts.vertesia.io/token/issue` with Firebase token)
335
+ 5. **STS validates Firebase token and generates Vertesia JWT**
336
+ 6. Returns with **Vertesia JWT** in URL hash
337
+ 7. UserSessionProvider processes JWT directly → `session.login(JWT)`
338
+ 8. **onAuthStateChanged may fire separately but login already happened**
339
+
340
+ ### Path B: Standard/SSO (Options 2 & 3) - Firebase First
341
+ 1. User clicks provider button (Google/GitHub/Microsoft/Enterprise)
342
+ 2. Firebase authentication flow (OAuth or SAML)
343
+ 3. **Firebase creates session (NO token in URL)**
344
+ 4. **onAuthStateChanged fires FIRST**
345
+ 5. Listener calls `getComposableToken()` which **calls STS** (`POST sts.vertesia.io/token/issue` with Firebase token)
346
+ 6. **STS validates Firebase token and generates Vertesia JWT**
347
+ 7. Then calls `session.login(JWT)`
348
+
349
+ ### Summary Table
350
+
351
+ | Aspect | Central Auth (Option 1) | Standard/SSO (Options 2 & 3) |
352
+ |--------|------------------------|------------------------------|
353
+ | **Token in URL?** | YES - Vertesia JWT | NO |
354
+ | **Initial trigger** | URL token processing | onAuthStateChanged listener |
355
+ | **Who calls STS?** | Central auth server | Your app (in onAuthStateChanged) |
356
+ | **STS call location** | Server-side (central auth) | Client-side (browser) |
357
+ | **Firebase session** | Created by central auth | Created directly by Firebase |
358
+ | **Login order** | 1. session.login()<br>2. onAuthStateChanged fires | 1. onAuthStateChanged fires<br>2. session.login() |
359
+
360
+ ---
361
+
362
+ ## Signup Flow (UserNotFoundError)
363
+
364
+ ```
365
+ ┌─────────────────────────────────────────────────────────────────────────────┐
366
+ │ UserNotFoundError Handling │
367
+ └────────────────────────────────────────────┬────────────────────────────────┘
368
+
369
+
370
+ ┌─────────────────────────────┐
371
+ │ getComposableToken() │
372
+ │ returns UserNotFoundError │
373
+ │ │
374
+ │ This means: Firebase user │
375
+ │ exists but no Vertesia user │
376
+ └──────────┬──────────────────┘
377
+
378
+
379
+ ┌─────────────────────────────┐
380
+ │ Set state: │
381
+ │ - isLoading = false │
382
+ │ - authError = err │
383
+ │ - Do NOT logout() │
384
+ │ - Do NOT redirect │
385
+ └──────────┬──────────────────┘
386
+
387
+
388
+ ┌─────────────────────────────┐
389
+ │ SigninScreen detects │
390
+ │ UserNotFoundError via │
391
+ │ useEffect │
392
+ └──────────┬──────────────────┘
393
+
394
+
395
+ ┌─────────────────────────────┐
396
+ │ setCollectSignupData(true) │
397
+ │ │
398
+ │ Shows SignupForm component │
399
+ └──────────┬──────────────────┘
400
+
401
+
402
+ ┌─────────────────────────────┐
403
+ │ SignupForm Component │
404
+ │ │
405
+ │ User fills in: │
406
+ │ - Name │
407
+ │ - Organization │
408
+ │ - Other signup data │
409
+ └──────────┬──────────────────┘
410
+
411
+
412
+ ┌─────────────────────────────┐
413
+ │ onSignup() called │
414
+ │ │
415
+ │ Build SignupPayload: │
416
+ │ - signupData │
417
+ │ - firebaseToken │
418
+ └──────────┬──────────────────┘
419
+
420
+
421
+ ┌─────────────────────────────┐
422
+ │ POST /auth/signup │
423
+ │ │
424
+ │ Server creates: │
425
+ │ - Vertesia user │
426
+ │ - Default account │
427
+ │ - Default project │
428
+ └──────────┬──────────────────┘
429
+
430
+
431
+ ┌─────────────────────────────┐
432
+ │ trackEvent("sign_up") │
433
+ │ │
434
+ │ window.location.href = "/" │
435
+ │ │
436
+ │ (Full page reload triggers │
437
+ │ normal auth flow) │
438
+ └─────────────────────────────┘
439
+ ```
440
+
441
+ ---
442
+
443
+ ## Logout Flow
444
+
445
+ ```
446
+ ┌─────────────────────────────────────────────────────────────────────────────┐
447
+ │ User Logout │
448
+ └────────────────────────────────────────────┬────────────────────────────────┘
449
+
450
+
451
+ ┌─────────────────────────────┐
452
+ │ session.logout() or │
453
+ │ session.signOut() │
454
+ └──────────┬──────────────────┘
455
+
456
+
457
+ ┌─────────────────────────────┐
458
+ │ Check: authToken exists? │
459
+ └──────────┬──────────────────┘
460
+ YES
461
+
462
+
463
+ ┌─────────────────────────────┐
464
+ │ shouldRedirectToCentralAuth │
465
+ │ ()? │
466
+ └──────┬────────────┬─────────┘
467
+ YES NO
468
+ │ │
469
+ ┌───────────┘ └──────────┐
470
+ ▼ ▼
471
+ ┌──────────────────────┐ ┌──────────────────────┐
472
+ │ Redirect to Central │ │ getFirebaseAuth() │
473
+ │ Auth for logout: │ │ .signOut() │
474
+ │ │ │ │
475
+ │ internal-auth │ │ (Triggers │
476
+ │ .vertesia.app │ │ onAuthStateChanged │
477
+ │ /logout │ │ with anonymous user)│
478
+ │ │ └──────────┬───────────┘
479
+ │ (Central auth │ │
480
+ │ handles Firebase │ │
481
+ │ logout) │ │
482
+ └──────────┬───────────┘ │
483
+ │ │
484
+ └────────────┬───────────────────┘
485
+
486
+
487
+ ┌─────────────────────────────┐
488
+ │ Clear session data: │
489
+ │ - authError = undefined │
490
+ │ - isLoading = false │
491
+ │ - authToken = undefined │
492
+ │ - typeRegistry = undefined │
493
+ └──────────┬──────────────────┘
494
+
495
+
496
+ ┌─────────────────────────────┐
497
+ │ client.withAuthCallback │
498
+ │ (undefined) │
499
+ │ │
500
+ │ (Clear client auth) │
501
+ └──────────┬──────────────────┘
502
+
503
+
504
+ ┌─────────────────────────────┐
505
+ │ setSession(this.clone()) │
506
+ │ │
507
+ │ React re-render triggers │
508
+ │ SigninScreen display │
509
+ └─────────────────────────────┘
510
+ ```
511
+
512
+ ---
513
+
514
+ ## Account/Project Switching
515
+
516
+ ```
517
+ ┌─────────────────────────────────────────────────────────────────────────────┐
518
+ │ Account/Project Switching │
519
+ └────────────────────────────────────────┬────────────────────────────────────┘
520
+
521
+ ┌────────────────┴────────────────┐
522
+ │ │
523
+ ▼ ▼
524
+ ┌────────────────────┐ ┌────────────────────┐
525
+ │ switchAccount(id) │ │ switchProject(id) │
526
+ └──────────┬─────────┘ └──────────┬─────────┘
527
+ │ │
528
+ ▼ ▼
529
+ ┌────────────────────┐ ┌────────────────────┐
530
+ │ Save to │ │ Save to │
531
+ │ localStorage: │ │ localStorage: │
532
+ │ - LastSelected │ │ - LastSelected │
533
+ │ AccountId │ │ ProjectId │
534
+ │ - LastSelected │ │ (per account) │
535
+ │ ProjectId │ └──────────┬─────────┘
536
+ │ (for current) │ │
537
+ └──────────┬─────────┘ │
538
+ │ │
539
+ ▼ ▼
540
+ ┌────────────────────┐ ┌────────────────────┐
541
+ │ window.location │ │ window.location │
542
+ │ .replace( │ │ .replace( │
543
+ │ '/?a=' + id) │ │ '/?a=' + acct │
544
+ │ │ │ '&p=' + proj) │
545
+ │ Full page reload │ │ │
546
+ └──────────┬─────────┘ │ Full page reload │
547
+ │ └──────────┬─────────┘
548
+ │ │
549
+ └───────────┬───────────────────┘
550
+
551
+
552
+ ┌─────────────────────────────┐
553
+ │ UserSessionProvider mounts │
554
+ │ with new account/project │
555
+ │ from URL params │
556
+ │ │
557
+ │ Firebase listener triggers │
558
+ │ getComposableToken with │
559
+ │ new selection │
560
+ └─────────────────────────────┘
561
+ ```
562
+
563
+ ---
564
+
565
+ ## Key Implementation Details
566
+
567
+ ### State Validation (CSRF Protection)
568
+
569
+ ```typescript
570
+ // generateState() creates random string, stores in sessionStorage with 5 min TTL
571
+ const state = crypto.randomUUID();
572
+ sessionStorage.setItem('auth_state', JSON.stringify({
573
+ state,
574
+ timestamp: Date.now()
575
+ }));
576
+
577
+ // verifyState() checks:
578
+ // 1. State exists in sessionStorage
579
+ // 2. Matches URL parameter
580
+ // 3. Not expired (< 5 minutes old)
581
+ ```
582
+
583
+ ### Account/Project Selection Priority
584
+
585
+ ```typescript
586
+ // 1. URL params (highest priority)
587
+ const accountId = searchParams.get('a')
588
+ // 2. localStorage (fallback)
589
+ ?? localStorage.getItem(LastSelectedAccountId_KEY)
590
+ // 3. undefined (will use default)
591
+ ?? undefined;
592
+
593
+ const projectId = searchParams.get('p')
594
+ ?? localStorage.getItem(LastSelectedProjectId_KEY + '-' + accountId)
595
+ ?? undefined;
596
+ ```
597
+
598
+ ### Session.login() Internal Flow
599
+
600
+ ```typescript
601
+ async login(token: string) {
602
+ // 1. Clear any previous errors
603
+ this.authError = undefined;
604
+ this.isLoading = false;
605
+
606
+ // 2. Setup auth callback for API client
607
+ this.client.withAuthCallback(() => this.authCallback);
608
+
609
+ // 3. Decode JWT to get user info
610
+ this.authToken = jwtDecode(token);
611
+
612
+ // 4. Save selections to localStorage
613
+ localStorage.setItem(LastSelectedAccountId_KEY, this.authToken.account.id);
614
+ localStorage.setItem(
615
+ LastSelectedProjectId_KEY + '-' + this.authToken.account.id,
616
+ this.authToken.project?.id ?? ''
617
+ );
618
+
619
+ // 5. Notify host app
620
+ Env.onLogin?.(this.authToken);
621
+
622
+ // 6. Load types and onboarding in parallel
623
+ await Promise.all([
624
+ this._loadTypes(),
625
+ this.fetchOnboardingStatus(),
626
+ ]);
627
+ }
628
+ ```
629
+
630
+ ### Idempotency Guard
631
+
632
+ ```typescript
633
+ // UserSessionProvider uses ref to prevent duplicate auth flow
634
+ const hasInitiatedAuthRef = useRef(false);
635
+
636
+ useEffect(() => {
637
+ if (hasInitiatedAuthRef.current) {
638
+ console.log("Auth: skipping duplicate auth flow initiation");
639
+ return;
640
+ }
641
+ hasInitiatedAuthRef.current = true;
642
+ // ... rest of auth flow
643
+ }, []);
644
+ ```
645
+
646
+ ---
647
+
648
+ ## Environment Differences
649
+
650
+ The `shouldRedirectToCentralAuth()` function determines which signin UI to show:
651
+
652
+ ```typescript
653
+ // Currently hardcoded to return true
654
+ export function shouldRedirectToCentralAuth() {
655
+ return true;
656
+
657
+ // Has logic for:
658
+ // if (Env.isDocker) return true;
659
+ // return devDomains.some(domain =>
660
+ // window.location.hostname.endsWith(domain)
661
+ // );
662
+ }
663
+ ```
664
+
665
+ When `true`: Shows "Continue with Central Auth" button
666
+ When `false`: Shows Google/GitHub/Microsoft/Enterprise sign-in buttons
667
+
668
+ ---
669
+
670
+ ## Error Handling Summary
671
+
672
+ | Error Type | Flow | Action |
673
+ |------------|------|--------|
674
+ | **UserNotFoundError** | Token exchange | Show SignupForm, don't logout, don't redirect |
675
+ | **Invalid state** | State validation | Show SigninScreen with error |
676
+ | **Network/API error** | Token exchange | Show SigninScreen with error message |
677
+ | **Type loading error** | session.login() | Set authError, partial login (may show app with limitations) |
678
+ | **Onboarding fetch error** | session.login() | Log error, continue (onboardingComplete=false) |
679
+
680
+ ---
681
+
682
+ ## Storage Keys
683
+
684
+ - `LastSelectedAccountId_KEY`: Last selected account ID
685
+ - `LastSelectedProjectId_KEY-{accountId}`: Last selected project per account
686
+ - `auth_state`: State parameter for CSRF protection (sessionStorage, 5 min TTL)
687
+
688
+ ---
689
+
690
+ ## Complete Flow Diagram
691
+
692
+ ### Full Authentication Flow (Mermaid)
693
+
694
+ ```mermaid
695
+ flowchart TB
696
+ Start([APP LOADS NO AUTHENTICATION]) --> Mount[UserSessionProvider mounts<br/>1. Parse URL hash<br/>2. Get account/project<br/>3. Clear URL hash<br/>4. Setup idempotency<br/>5. Create UserSession]
697
+ Mount --> HasToken{Has token &<br/>state in URL?}
698
+
699
+ %% PATH B: NO TOKEN FLOW
700
+ HasToken -->|NO| PathB[PATH B: NO TOKEN FLOW<br/>First load or no URL token]
701
+ PathB --> SetupFirebase[Setup Firebase<br/>onAuthStateChanged<br/>listener]
702
+ SetupFirebase --> ShowSignin[Show SigninScreen]
703
+ ShowSignin --> ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
704
+ ShouldRedirect -->|YES| ShowCentral[Show Continue with<br/>Central Auth button]
705
+ ShouldRedirect -->|NO| ShowOptions[Show login options:<br/>- Google<br/>- GitHub<br/>- Microsoft<br/>- Enterprise]
706
+
707
+ %% Three authentication options
708
+ ShowCentral --> Option1
709
+ ShowOptions --> Option2
710
+ ShowOptions --> Option3
711
+
712
+ subgraph Option1[OPTION 1: CENTRAL AUTH]
713
+ C1[Generate state<br/>Save to sessionStorage]
714
+ C2[Get account/project<br/>from URL or localStorage]
715
+ C3[Build redirect URL:<br/>internal-auth.vertesia.app<br/>?sts=...&redirect_uri=...&state=...]
716
+ C4[window.location.replace<br/>to central auth]
717
+ C5[Central Auth Page<br/>User authenticates<br/>Gets Firebase token]
718
+ C6[Central auth calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id]
719
+ C7[STS validates Firebase token<br/>and generates Vertesia JWT]
720
+ C8[Redirect back to app with:<br/>#token=VERTESIA_JWT&state=state]
721
+
722
+ C1 --> C2 --> C3 --> C4 --> C5 --> C6 --> C7 --> C8
723
+ end
724
+
725
+ subgraph Option2[OPTION 2: STANDARD SIGNIN]
726
+ S1[User clicks:<br/>Google/GitHub/Microsoft]
727
+ S2[Call Firebase:<br/>signInWith... OAuth]
728
+ S3[Firebase OAuth<br/>redirect & return]
729
+ S4[Firebase auth success!<br/>Firebase creates session]
730
+
731
+ S1 --> S2 --> S3 --> S4
732
+ end
733
+
734
+ subgraph Option3[OPTION 3: SSO SIGNIN SAML]
735
+ E1[User enters:<br/>Company email or domain]
736
+ E2[Lookup SSO<br/>provider config]
737
+ E3[Redirect to SSO provider<br/>Okta, Azure AD, etc.]
738
+ E4[User auth at SSO provider]
739
+ E5[SAML response back to app]
740
+ E6[Exchange SAML for<br/>Firebase custom token]
741
+ E7[Sign in to Firebase<br/>with custom token]
742
+
743
+ E1 --> E2 --> E3 --> E4 --> E5 --> E6 --> E7
744
+ end
745
+
746
+ %% PATH A: TOKEN FLOW - After redirect back
747
+ C8 --> PathA[PATH A: TOKEN FLOW<br/>Returning from central auth]
748
+ HasToken -->|YES| PathA
749
+ PathA --> VerifyState{verifyState?}
750
+ VerifyState -->|NO| InvalidState[Invalid state!<br/>Log error<br/>Show SigninScreen]
751
+ VerifyState -->|YES| ReturningUser[RETURNING USER<br/>getComposableToken calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id<br/><br/>STS returns Vertesia JWT]
752
+ ClearState --> GetToken[getComposableToken<br/>with URL token<br/>This is already a<br/>VERTESIA JWT from STS]
753
+ GetToken --> TokenSuccess{SUCCESS?}
754
+ TokenSuccess -->|YES| SessionLogin[session.login JWT]
755
+ TokenSuccess -->|NO| TokenError{Error Type?}
756
+ TokenError -->|UserNotFound| SignupFlow
757
+ TokenError -->|Other| ShowError[Show SigninScreen<br/>with error msg]
758
+
759
+ subgraph SignupFlow[SIGNUP FLOW - UserNotFoundError]
760
+ SU1[Set state:<br/>- isLoading = false<br/>- authError = UserNotFoundError<br/>- Do NOT logout]
761
+ SU2[SigninScreen detects<br/>UserNotFoundError via useEffect]
762
+ SU3[setCollectSignupData true<br/>Shows SignupForm component]
763
+ SU4[User fills in:<br/>- Name<br/>- Organization<br/>- Other signup data]
764
+ SU5[onSignup called<br/>Build SignupPayload]
765
+ SU6[POST /auth/signup<br/><br/>Server creates:<br/>- Vertesia user<br/>- Default account<br/>- Default project]
766
+ SU7[trackEvent sign_up<br/>window.location.href = /<br/>Full page reload]
767
+
768
+ SU1 --> SU2 --> SU3 --> SU4 --> SU5 --> SU6 --> SU7
769
+ end
770
+
771
+ Start([User Logout]) --> Trigger[session.logout or<br/>session.signOut]
772
+
773
+ Trigger --> Check{authToken<br/>exists?}
774
+
775
+ Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
776
+
777
+ ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
778
+
779
+ ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
780
+
781
+ RedirectLogout --> ClearData[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
782
+
783
+ FirebaseSignout --> ClearData
784
+
785
+ ClearData --> ClearClient[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
786
+
787
+ ClearClient --> UpdateState[setSession this.clone<br/><br/>React re-render triggers<br/>SigninScreen display]
788
+
789
+
790
+ SU7 --> Converge
791
+
792
+ %% Convergence point
793
+ S4 --> FirebaseListener
794
+ E7 --> FirebaseListener
795
+
796
+ FirebaseListener[★ TRIGGERS onAuthStateChanged ★<br/>Triggered by:<br/>1. Page load/mount always<br/>2. Firebase sign-in Options 2 & 3<br/>3. Firebase sign-out<br/>4. Token refresh/expiry]
797
+
798
+ FirebaseListener --> FirebaseUser{Firebase user exists?}
799
+
800
+ FirebaseUser -->|NO| LoggedOut[NEW/LOGGED OUT USER<br/>session.authToken stays undefined<br/>SigninScreen shows]
801
+
802
+ LoggedOut --> ShowSignin
803
+
804
+ FirebaseUser -->|YES| ReturningUser[RETURNING USER<br/>getComposableToken calls STS:<br/>POST sts.vertesia.io/token/issue<br/>Authorization: Bearer Firebase<br/>Body: type, account_id, project_id<br/><br/>STS returns Vertesia JWT]
805
+
806
+ ReturningUser --> ReturningLogin[session.login JWT]
807
+ ReturningLogin --> ReturningSuccess{SUCCESS?}
808
+ ReturningSuccess -->|YES| UpdateState[Update state<br/>User logged in<br/>Show main app]
809
+ ReturningSuccess -->|NO| ReturningError{Error Type?}
810
+ ReturningError -->|UserNotFound| SignupFlow
811
+ ReturningError -->|Other| LogoutError[logout<br/>Show error]
812
+
813
+ %% Session login flow
814
+ SessionLogin --> LoginSteps[Inside login:<br/>1. authError=undefined<br/>2. isLoading=false<br/>3. Decode JWT<br/>4. Set auth callback<br/>5. Save to localStorage<br/>6. Notify Env.onLogin<br/>7. Promise.all loadTypes, fetchOnboarding]
815
+ LoginSteps --> SetSession[setSession<br/>Update state]
816
+ SetSession --> LoggedIn[USER LOGGED IN!<br/>Show main app]
817
+
818
+ UpdateState --> Converge[ALL PATHS END HERE<br/>User authenticated<br/>Main app shown]
819
+ LoggedIn --> Converge
820
+
821
+ style PathA fill:#e1f5ff
822
+ style PathB fill:#fff4e1
823
+ style Option1 fill:#f0f0f0
824
+ style Option2 fill:#f0f0f0
825
+ style Option3 fill:#f0f0f0
826
+ style Converge fill:#d4edda
827
+ style FirebaseListener fill:#fff3cd
828
+ ```
829
+
830
+ ---
831
+
832
+ ## Sequence Diagrams
833
+
834
+ ### Option 1: Central Auth Flow
835
+
836
+ ```mermaid
837
+ sequenceDiagram
838
+ actor User
839
+ participant App as Composable UI
840
+ participant CentralAuth as Central Auth<br/>(internal-auth.vertesia.app)
841
+ participant Firebase as Firebase Auth
842
+ participant STS as STS<br/>(sts.vertesia.io)
843
+
844
+ User->>App: Navigate to app (no auth)
845
+ activate App
846
+ App->>App: Show SigninScreen
847
+ User->>App: Click 'Continue with Central Auth'
848
+
849
+ App->>App: generateState()<br/>(save to sessionStorage)
850
+ App->>CentralAuth: Redirect with state<br/>?sts=...&redirect_uri=...&state=...
851
+ activate CentralAuth
852
+
853
+ CentralAuth->>Firebase: Authenticate user
854
+ activate Firebase
855
+ Firebase-->>CentralAuth: Firebase ID token
856
+ deactivate Firebase
857
+
858
+ CentralAuth->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
859
+ activate STS
860
+ STS->>STS: Validate Firebase token
861
+ STS->>STS: Generate Vertesia JWT
862
+ STS-->>CentralAuth: Vertesia JWT
863
+ deactivate STS
864
+
865
+ CentralAuth->>App: Redirect back<br/>#token={JWT}&state={state}
866
+ deactivate CentralAuth
867
+
868
+ App->>App: verifyState()
869
+ App->>App: clearState()
870
+ App->>App: getComposableToken(JWT from URL)
871
+ App->>App: session.login(JWT)
872
+ App->>App: Show main app
873
+ deactivate App
874
+ ```
875
+
876
+ ### Option 2: Standard Sign-in (Google/GitHub/Microsoft)
877
+
878
+ ```mermaid
879
+ sequenceDiagram
880
+ actor User
881
+ participant App as Composable UI
882
+ participant Firebase as Firebase Auth
883
+ participant Provider as OAuth Provider<br/>(Google/GitHub/Microsoft)
884
+ participant STS as STS<br/>(sts.vertesia.io)
885
+
886
+ User->>App: Navigate to app (no auth)
887
+ activate App
888
+ App->>App: Show SigninScreen
889
+ App->>App: Setup onAuthStateChanged listener
890
+ User->>App: Click provider button (e.g., Google)
891
+
892
+ App->>Firebase: signInWithPopup(GoogleAuthProvider)
893
+ activate Firebase
894
+ Firebase->>Provider: OAuth redirect
895
+ activate Provider
896
+ Provider->>User: Show provider login page
897
+ User->>Provider: Authenticate & consent
898
+ Provider-->>Firebase: OAuth credentials
899
+ deactivate Provider
900
+ Firebase->>Firebase: Create Firebase session
901
+ Firebase-->>App: Firebase auth success
902
+
903
+ Note over App,Firebase: onAuthStateChanged fires
904
+ Firebase->>App: User object (with ID token)
905
+ deactivate Firebase
906
+
907
+ App->>App: getComposableToken()
908
+ App->>Firebase: Get ID token from current user
909
+ activate Firebase
910
+ Firebase-->>App: Firebase ID token
911
+ deactivate Firebase
912
+
913
+ App->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
914
+ activate STS
915
+ STS->>STS: Validate Firebase token
916
+ STS->>STS: Generate Vertesia JWT
917
+ STS-->>App: Vertesia JWT
918
+ deactivate STS
919
+
920
+ App->>App: session.login(JWT)
921
+ App->>App: Show main app
922
+ deactivate App
923
+ ```
924
+
925
+ ### Option 3: Enterprise SSO/SAML Sign-in
926
+
927
+ ```mermaid
928
+ sequenceDiagram
929
+ actor User
930
+ participant App as Composable UI
931
+ participant Firebase as Firebase Auth
932
+ participant SSO as SSO Provider<br/>(Okta/Azure AD/etc.)
933
+ participant STS as STS<br/>(sts.vertesia.io)
934
+
935
+ User->>App: Navigate to app (no auth)
936
+ activate App
937
+ App->>App: Show SigninScreen
938
+ App->>App: Setup onAuthStateChanged listener
939
+ User->>App: Click 'Enterprise Sign-in'
940
+ User->>App: Enter company email/domain
941
+
942
+ App->>App: Lookup SSO provider config
943
+ App->>SSO: Redirect to SSO provider
944
+ activate SSO
945
+ SSO->>User: Show SSO login page
946
+ User->>SSO: Authenticate
947
+ SSO-->>App: SAML response
948
+ deactivate SSO
949
+
950
+ App->>Firebase: Exchange SAML for Firebase custom token
951
+ activate Firebase
952
+ Firebase->>Firebase: Validate SAML & create session
953
+ Firebase-->>App: Firebase auth success
954
+
955
+ Note over App,Firebase: onAuthStateChanged fires
956
+ Firebase->>App: User object (with ID token)
957
+ deactivate Firebase
958
+
959
+ App->>App: getComposableToken()
960
+ App->>Firebase: Get ID token from current user
961
+ activate Firebase
962
+ Firebase-->>App: Firebase ID token
963
+ deactivate Firebase
964
+
965
+ App->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
966
+ activate STS
967
+ STS->>STS: Validate Firebase token
968
+ STS->>STS: Generate Vertesia JWT
969
+ STS-->>App: Vertesia JWT
970
+ deactivate STS
971
+
972
+ App->>App: session.login(JWT)
973
+ App->>App: Show main app
974
+ deactivate App
975
+ ```
976
+
977
+ ### Session Restoration (Page Reload)
978
+
979
+ ```mermaid
980
+ sequenceDiagram
981
+ actor User
982
+ participant App as Composable UI
983
+ participant Firebase as Firebase Auth
984
+ participant STS as STS<br/>(sts.vertesia.io)
985
+
986
+ User->>App: Reload page / Navigate back
987
+ activate App
988
+ App->>App: UserSessionProvider mounts
989
+ App->>App: Setup onAuthStateChanged listener
990
+
991
+ Note over App,Firebase: onAuthStateChanged fires immediately
992
+
993
+ alt Firebase session exists
994
+ Firebase->>App: User object (with ID token)
995
+
996
+ App->>App: getComposableToken()
997
+ App->>Firebase: Get ID token from current user
998
+ activate Firebase
999
+ Firebase-->>App: Firebase ID token
1000
+ deactivate Firebase
1001
+
1002
+ App->>STS: POST /token/issue<br/>Authorization: Bearer {Firebase token}<br/>Body: {type:'user', account_id, project_id}
1003
+ activate STS
1004
+ STS->>STS: Validate Firebase token
1005
+ STS->>STS: Generate Vertesia JWT
1006
+ STS-->>App: Vertesia JWT
1007
+ deactivate STS
1008
+
1009
+ App->>App: session.login(JWT)
1010
+ App->>App: Show main app (user logged in)
1011
+ else No Firebase session
1012
+ Firebase->>App: Anonymous user
1013
+ App->>App: Show SigninScreen (not logged in)
1014
+ end
1015
+
1016
+ deactivate App
1017
+ ```
1018
+
1019
+ ---
1020
+
1021
+ ## Additional Flow Diagrams (Mermaid)
1022
+
1023
+ ### Logout Flow
1024
+
1025
+ ```mermaid
1026
+ flowchart TB
1027
+ Start([User Logout]) --> Trigger[session.logout or<br/>session.signOut]
1028
+
1029
+ Trigger --> Check{authToken<br/>exists?}
1030
+
1031
+ Check -->|YES| ShouldRedirect{shouldRedirectTo<br/>CentralAuth?}
1032
+
1033
+ ShouldRedirect -->|YES| RedirectLogout[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
1034
+
1035
+ ShouldRedirect -->|NO| FirebaseSignout[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
1036
+
1037
+ RedirectLogout --> ClearData[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
1038
+
1039
+ FirebaseSignout --> ClearData
1040
+
1041
+ ClearData --> ClearClient[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
1042
+
1043
+ ClearClient --> UpdateState[setSession this.clone<br/><br/>React re-render triggers<br/>SigninScreen display]
1044
+
1045
+ style Start fill:#fff4e1
1046
+ style UpdateState fill:#d4edda
1047
+ ```
1048
+
1049
+ ### Account/Project Switching Flow
1050
+
1051
+ ```mermaid
1052
+ flowchart TB
1053
+ Start([Account/Project Switching]) --> Branch{Which action?}
1054
+
1055
+ Branch -->|Account| SwitchAccount[switchAccount id]
1056
+ Branch -->|Project| SwitchProject[switchProject id]
1057
+
1058
+ SwitchAccount --> SaveAccount[Save to localStorage:<br/>- LastSelectedAccountId<br/>- LastSelectedProjectId<br/> for current account]
1059
+
1060
+ SwitchProject --> SaveProject[Save to localStorage:<br/>- LastSelectedProjectId<br/> per account]
1061
+
1062
+ SaveAccount --> ReloadAccount[window.location.replace<br/>/?a= + id<br/><br/>Full page reload]
1063
+
1064
+ SaveProject --> ReloadProject[window.location.replace<br/>/?a= + acct & p= + proj<br/><br/>Full page reload]
1065
+
1066
+ ReloadAccount --> Converge[UserSessionProvider mounts<br/>with new account/project<br/>from URL params<br/><br/>Firebase listener triggers<br/>getComposableToken with<br/>new selection]
1067
+
1068
+ ReloadProject --> Converge
1069
+
1070
+ style Start fill:#fff4e1
1071
+ style Converge fill:#d4edda
1072
+ ```
1073
+
1074
+
1075
+
1076
+ subgraph LogoutFlow[LOGOUT FLOW]
1077
+ LO1([User Logout])
1078
+ LO2[session.logout or<br/>session.signOut]
1079
+ LO3{authToken<br/>exists?}
1080
+ LO4{shouldRedirectTo<br/>CentralAuth?}
1081
+ LO5[Redirect to Central Auth<br/>for logout<br/><br/>internal-auth.vertesia.app<br/>/logout<br/><br/>Central auth handles<br/>Firebase logout]
1082
+ LO6[getFirebaseAuth.signOut<br/><br/>Triggers onAuthStateChanged<br/>with anonymous user]
1083
+ LO7[Clear session data:<br/>- authError = undefined<br/>- isLoading = false<br/>- authToken = undefined<br/>- typeRegistry = undefined]
1084
+ LO8[client.withAuthCallback<br/>undefined<br/><br/>Clear client auth]
1085
+ LO9[setSession this.clone<br/><br/>React re-render triggers<br/>SigninScreen display]
1086
+
1087
+ LO1 --> LO2 --> LO3
1088
+ LO3 -->|YES| LO4
1089
+ LO4 -->|YES| LO5
1090
+ LO4 -->|NO| LO6
1091
+ LO5 --> LO7
1092
+ LO6 --> LO7
1093
+ LO7 --> LO8 --> LO9
1094
+ end