@rapidmx/web-client 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (400) hide show
  1. package/apps/admin/audit-log/index.tsx +3 -3
  2. package/apps/admin/branding/index.tsx +3 -3
  3. package/apps/admin/data-requests/index.tsx +18 -7
  4. package/apps/admin/distribution-lists/[uid].tsx +3 -3
  5. package/apps/admin/distribution-lists/index.tsx +3 -3
  6. package/apps/admin/distribution-lists/new/index.tsx +3 -3
  7. package/apps/admin/domains/[uid].tsx +6 -5
  8. package/apps/admin/domains/index.tsx +3 -3
  9. package/apps/admin/domains/new/index.tsx +3 -3
  10. package/apps/admin/escrow-scopes/[uid].tsx +11 -5
  11. package/apps/admin/escrow-scopes/index.tsx +3 -3
  12. package/apps/admin/escrow-scopes/new/index.tsx +9 -3
  13. package/apps/admin/index.tsx +4 -4
  14. package/apps/admin/ingest-queue/index.tsx +3 -3
  15. package/apps/admin/mailboxes/[uid].tsx +15 -7
  16. package/apps/admin/quarantine/index.tsx +4 -4
  17. package/apps/admin/retention-policy/index.tsx +3 -3
  18. package/apps/admin/signing-certificates/index.tsx +3 -3
  19. package/apps/admin/transport-rules/[uid].tsx +4 -4
  20. package/apps/admin/transport-rules/index.tsx +3 -3
  21. package/apps/admin/transport-rules/new/index.tsx +3 -3
  22. package/apps/escrow/matters/[uid].tsx +26 -2
  23. package/apps/escrow/matters/new/index.tsx +4 -0
  24. package/apps/shared/calendar/calendarLiveUpdates.ts +11 -10
  25. package/apps/shared/calendar/useCalendarReminders.ts +2 -0
  26. package/apps/shared/components/admin/ActionAlert.tsx +32 -0
  27. package/apps/shared/components/admin/diagnostics/InformationPanel.tsx +1 -1
  28. package/apps/shared/components/admin/diagnostics/InstalledPlugins.tsx +1 -1
  29. package/apps/shared/components/admin/diagnostics/LogsPanel.tsx +1 -1
  30. package/apps/shared/components/admin/diagnostics/RuntimePanel.tsx +1 -1
  31. package/apps/shared/components/admin/diagnostics/SystemPanel.tsx +1 -1
  32. package/apps/shared/components/admin/distributionLists/MemberListCard.tsx +4 -4
  33. package/apps/shared/components/admin/elevation.ts +19 -0
  34. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +58 -3
  35. package/apps/shared/components/admin/layout/AdminShell.tsx +6 -1
  36. package/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.tsx +3 -3
  37. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +4 -4
  38. package/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.tsx +4 -4
  39. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  40. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +6 -6
  41. package/apps/shared/components/admin/settings/BrandingForm.tsx +22 -9
  42. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -4
  43. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +33 -4
  44. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +3 -3
  45. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +8 -3
  46. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +3 -3
  47. package/apps/shared/components/admin/settings/PluginsManager.tsx +39 -17
  48. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +25 -12
  49. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +40 -7
  50. package/apps/shared/components/admin/setup/SetupWizard.tsx +8 -6
  51. package/apps/shared/components/admin/signOut.ts +3 -0
  52. package/apps/shared/components/admin/usePagedList.tsx +2 -2
  53. package/apps/shared/components/calendar/DescriptionEditor.tsx +2 -1
  54. package/apps/shared/components/calendar/EventDetails.tsx +34 -8
  55. package/apps/shared/components/calendar/EventEditor.tsx +90 -40
  56. package/apps/shared/components/calendar/EventFormParts.tsx +5 -2
  57. package/apps/shared/components/calendar/EventMatchNav.tsx +14 -2
  58. package/apps/shared/components/calendar/EventModal.tsx +25 -1
  59. package/apps/shared/components/calendar/FindATime.tsx +10 -2
  60. package/apps/shared/components/calendar/ListView.tsx +99 -25
  61. package/apps/shared/components/calendar/RecurrenceEditor.tsx +19 -8
  62. package/apps/shared/components/calendar/SplitDayView.tsx +14 -18
  63. package/apps/shared/components/calendar/TaskQuickForm.tsx +3 -2
  64. package/apps/shared/components/calendar/TimeGridView.tsx +35 -19
  65. package/apps/shared/components/calendar/gridLayout.ts +94 -0
  66. package/apps/shared/components/calendar/layout/CalendarShell.tsx +1 -1
  67. package/apps/shared/components/calendar/timePicker.ts +5 -2
  68. package/apps/shared/components/contacts/ContactCard.tsx +15 -4
  69. package/apps/shared/components/contacts/ContactDetailPane.tsx +54 -15
  70. package/apps/shared/components/contacts/ContactForm.tsx +61 -17
  71. package/apps/shared/components/contacts/ContactPhotoEditor.tsx +88 -0
  72. package/apps/shared/components/contacts/ContactPhotoField.tsx +36 -39
  73. package/apps/shared/components/contacts/layout/ContactsShell.tsx +1 -1
  74. package/apps/shared/components/contacts/participantDetails.ts +18 -1
  75. package/apps/shared/components/layout/AppShell.tsx +9 -0
  76. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +9 -4
  77. package/apps/shared/components/layout/MailboxProvisioning.tsx +31 -3
  78. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +4 -2
  79. package/apps/shared/components/mail/ConversationList.tsx +49 -6
  80. package/apps/shared/components/mail/ConversationThreadPane.tsx +1 -1
  81. package/apps/shared/components/mail/MenuButton.tsx +3 -400
  82. package/apps/shared/components/mail/MessageDetailPane.tsx +16 -13
  83. package/apps/shared/components/mail/compose/ComposeToolbar.tsx +11 -1
  84. package/apps/shared/components/mail/compose/ComposeWindow.tsx +57 -11
  85. package/apps/shared/components/mail/compose/GifPicker.tsx +10 -4
  86. package/apps/shared/components/mail/compose/RecipientInput.tsx +9 -7
  87. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +14 -1
  88. package/apps/shared/components/mail/compose/encryptionRequirement.ts +5 -0
  89. package/apps/shared/components/mail/compose/quotedBody.ts +31 -6
  90. package/apps/shared/components/mail/layout/MailShell.tsx +1 -1
  91. package/apps/shared/components/mail/reading/MessageBody.tsx +13 -7
  92. package/apps/shared/components/mail/reading/bodyContent.ts +2 -0
  93. package/apps/shared/components/mail/reading/bodyHtml.ts +25 -2
  94. package/apps/shared/components/mail/writableMailboxes.ts +2 -0
  95. package/apps/shared/components/rules/RuleBuilder.tsx +11 -2
  96. package/apps/shared/components/settings/GravatarSetting.tsx +27 -0
  97. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -1
  98. package/apps/shared/components/tasks/dueDate.ts +29 -0
  99. package/apps/shared/components/tasks/layout/TasksShell.tsx +1 -1
  100. package/apps/shared/mail/decryptedMessages.ts +51 -2
  101. package/apps/shared/mail/listSnapshots.ts +2 -0
  102. package/apps/shared/mail/outbox/sendDecision.ts +14 -3
  103. package/apps/shared/mail/outbox/sendJob.ts +5 -0
  104. package/apps/shared/mail/useMailLiveUpdates.ts +7 -1
  105. package/apps/shared/mail/usePermanentDelete.tsx +3 -1
  106. package/apps/shared/notifications/apiErrors.ts +2 -0
  107. package/apps/shared/notifications/preferences.ts +1 -1
  108. package/apps/shared/notifications/store.ts +3 -1
  109. package/apps/shared/plugins/pluginNav.ts +4 -2
  110. package/apps/www/calendar/index.tsx +24 -12
  111. package/apps/www/contacts/[uid].tsx +6 -1
  112. package/apps/www/contacts/index.tsx +66 -12
  113. package/apps/www/index.tsx +91 -28
  114. package/apps/www/messages/[uid].tsx +8 -3
  115. package/apps/www/settings/auto-reply/index.tsx +13 -0
  116. package/apps/www/settings/blocked-senders/index.tsx +4 -0
  117. package/apps/www/settings/encryption/index.tsx +66 -4
  118. package/apps/www/settings/privacy/index.tsx +4 -1
  119. package/apps/www/settings/profile/index.tsx +12 -1
  120. package/apps/www/settings/sharing/index.tsx +11 -1
  121. package/apps/www/settings/signatures/[uid].tsx +6 -4
  122. package/apps/www/tasks/index.tsx +97 -29
  123. package/dist/apps/admin/audit-log/index.js +3 -3
  124. package/dist/apps/admin/branding/index.js +3 -3
  125. package/dist/apps/admin/data-requests/index.js +17 -7
  126. package/dist/apps/admin/distribution-lists/[uid].js +3 -3
  127. package/dist/apps/admin/distribution-lists/index.js +3 -3
  128. package/dist/apps/admin/distribution-lists/new/index.js +3 -3
  129. package/dist/apps/admin/domains/[uid].js +6 -5
  130. package/dist/apps/admin/domains/index.js +3 -3
  131. package/dist/apps/admin/domains/new/index.js +3 -3
  132. package/dist/apps/admin/escrow-scopes/[uid].js +11 -6
  133. package/dist/apps/admin/escrow-scopes/index.js +3 -3
  134. package/dist/apps/admin/escrow-scopes/new/index.js +9 -4
  135. package/dist/apps/admin/index.js +4 -4
  136. package/dist/apps/admin/ingest-queue/index.js +3 -3
  137. package/dist/apps/admin/mailboxes/[uid].js +15 -7
  138. package/dist/apps/admin/quarantine/index.js +4 -4
  139. package/dist/apps/admin/retention-policy/index.js +3 -3
  140. package/dist/apps/admin/signing-certificates/index.js +3 -3
  141. package/dist/apps/admin/transport-rules/[uid].js +4 -4
  142. package/dist/apps/admin/transport-rules/index.js +3 -3
  143. package/dist/apps/admin/transport-rules/new/index.js +3 -3
  144. package/dist/apps/escrow/matters/[uid].js +8 -3
  145. package/dist/apps/escrow/matters/new/index.js +4 -0
  146. package/dist/apps/shared/calendar/calendarLiveUpdates.d.ts +11 -7
  147. package/dist/apps/shared/calendar/calendarLiveUpdates.js +10 -9
  148. package/dist/apps/shared/calendar/useCalendarReminders.js +2 -0
  149. package/dist/apps/shared/components/admin/ActionAlert.d.ts +13 -0
  150. package/dist/apps/shared/components/admin/ActionAlert.js +24 -0
  151. package/dist/apps/shared/components/admin/diagnostics/InformationPanel.js +1 -1
  152. package/dist/apps/shared/components/admin/diagnostics/InstalledPlugins.js +1 -1
  153. package/dist/apps/shared/components/admin/diagnostics/LogsPanel.js +1 -1
  154. package/dist/apps/shared/components/admin/diagnostics/RuntimePanel.js +1 -1
  155. package/dist/apps/shared/components/admin/diagnostics/SystemPanel.js +1 -1
  156. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.js +4 -4
  157. package/dist/apps/shared/components/admin/elevation.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/elevation.js +16 -0
  159. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +2 -0
  160. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +41 -1
  161. package/dist/apps/shared/components/admin/layout/AdminShell.js +2 -1
  162. package/dist/apps/shared/components/admin/mailboxes/EraseLeftoverDataDialog.js +3 -3
  163. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +4 -4
  164. package/dist/apps/shared/components/admin/mailboxes/LeftoverMailboxesSection.js +4 -4
  165. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  166. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +6 -6
  167. package/dist/apps/shared/components/admin/settings/BrandingForm.js +17 -7
  168. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +4 -4
  169. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +17 -5
  170. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +3 -3
  171. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +8 -3
  172. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +3 -3
  173. package/dist/apps/shared/components/admin/settings/PluginsManager.js +26 -12
  174. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +17 -10
  175. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +18 -8
  176. package/dist/apps/shared/components/admin/setup/SetupWizard.js +8 -6
  177. package/dist/apps/shared/components/admin/signOut.js +3 -0
  178. package/dist/apps/shared/components/admin/usePagedList.js +2 -2
  179. package/dist/apps/shared/components/calendar/DescriptionEditor.js +2 -1
  180. package/dist/apps/shared/components/calendar/EventDetails.d.ts +3 -1
  181. package/dist/apps/shared/components/calendar/EventDetails.js +7 -4
  182. package/dist/apps/shared/components/calendar/EventEditor.js +81 -33
  183. package/dist/apps/shared/components/calendar/EventFormParts.d.ts +3 -1
  184. package/dist/apps/shared/components/calendar/EventFormParts.js +3 -3
  185. package/dist/apps/shared/components/calendar/EventMatchNav.d.ts +1 -1
  186. package/dist/apps/shared/components/calendar/EventMatchNav.js +12 -2
  187. package/dist/apps/shared/components/calendar/EventModal.js +10 -2
  188. package/dist/apps/shared/components/calendar/FindATime.js +4 -3
  189. package/dist/apps/shared/components/calendar/ListView.d.ts +10 -4
  190. package/dist/apps/shared/components/calendar/ListView.js +92 -27
  191. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +4 -1
  192. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +15 -10
  193. package/dist/apps/shared/components/calendar/SplitDayView.js +11 -17
  194. package/dist/apps/shared/components/calendar/TaskQuickForm.js +3 -2
  195. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -21
  196. package/dist/apps/shared/components/calendar/gridLayout.d.ts +41 -0
  197. package/dist/apps/shared/components/calendar/gridLayout.js +83 -0
  198. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +1 -1
  199. package/dist/apps/shared/components/calendar/timePicker.js +5 -2
  200. package/dist/apps/shared/components/contacts/ContactCard.js +9 -3
  201. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +5 -3
  202. package/dist/apps/shared/components/contacts/ContactDetailPane.js +11 -5
  203. package/dist/apps/shared/components/contacts/ContactForm.js +54 -16
  204. package/dist/apps/shared/components/contacts/ContactPhotoEditor.d.ts +21 -0
  205. package/dist/apps/shared/components/contacts/ContactPhotoEditor.js +56 -0
  206. package/dist/apps/shared/components/contacts/ContactPhotoField.d.ts +7 -4
  207. package/dist/apps/shared/components/contacts/ContactPhotoField.js +19 -21
  208. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +1 -1
  209. package/dist/apps/shared/components/contacts/participantDetails.d.ts +4 -1
  210. package/dist/apps/shared/components/contacts/participantDetails.js +17 -1
  211. package/dist/apps/shared/components/layout/AppShell.js +9 -0
  212. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +2 -3
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +4 -1
  214. package/dist/apps/shared/components/layout/MailboxProvisioning.js +24 -2
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +4 -2
  216. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +4 -2
  217. package/dist/apps/shared/components/mail/ConversationList.js +49 -6
  218. package/dist/apps/shared/components/mail/ConversationThreadPane.js +1 -1
  219. package/dist/apps/shared/components/mail/MenuButton.d.ts +2 -78
  220. package/dist/apps/shared/components/mail/MenuButton.js +2 -211
  221. package/dist/apps/shared/components/mail/MessageDetailPane.js +10 -6
  222. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +3 -0
  223. package/dist/apps/shared/components/mail/compose/ComposeToolbar.js +10 -1
  224. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +55 -10
  225. package/dist/apps/shared/components/mail/compose/GifPicker.js +10 -4
  226. package/dist/apps/shared/components/mail/compose/RecipientInput.js +9 -7
  227. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +7 -1
  228. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +13 -1
  229. package/dist/apps/shared/components/mail/compose/encryptionRequirement.d.ts +5 -0
  230. package/dist/apps/shared/components/mail/compose/quotedBody.js +28 -5
  231. package/dist/apps/shared/components/mail/layout/MailShell.js +1 -1
  232. package/dist/apps/shared/components/mail/reading/MessageBody.js +11 -6
  233. package/dist/apps/shared/components/mail/reading/bodyContent.js +2 -0
  234. package/dist/apps/shared/components/mail/reading/bodyHtml.d.ts +5 -1
  235. package/dist/apps/shared/components/mail/reading/bodyHtml.js +23 -2
  236. package/dist/apps/shared/components/mail/writableMailboxes.js +2 -0
  237. package/dist/apps/shared/components/rules/RuleBuilder.js +9 -1
  238. package/dist/apps/shared/components/settings/GravatarSetting.d.ts +6 -0
  239. package/dist/apps/shared/components/settings/GravatarSetting.js +15 -0
  240. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -1
  241. package/dist/apps/shared/components/tasks/dueDate.d.ts +12 -0
  242. package/dist/apps/shared/components/tasks/dueDate.js +26 -0
  243. package/dist/apps/shared/components/tasks/layout/TasksShell.js +1 -1
  244. package/dist/apps/shared/mail/decryptedMessages.d.ts +8 -0
  245. package/dist/apps/shared/mail/decryptedMessages.js +46 -2
  246. package/dist/apps/shared/mail/listSnapshots.js +2 -0
  247. package/dist/apps/shared/mail/outbox/sendDecision.d.ts +6 -3
  248. package/dist/apps/shared/mail/outbox/sendDecision.js +13 -3
  249. package/dist/apps/shared/mail/outbox/sendJob.d.ts +1 -0
  250. package/dist/apps/shared/mail/outbox/sendJob.js +5 -1
  251. package/dist/apps/shared/mail/useMailLiveUpdates.d.ts +3 -0
  252. package/dist/apps/shared/mail/useMailLiveUpdates.js +6 -1
  253. package/dist/apps/shared/mail/usePermanentDelete.d.ts +2 -0
  254. package/dist/apps/shared/mail/usePermanentDelete.js +1 -1
  255. package/dist/apps/shared/notifications/apiErrors.js +2 -0
  256. package/dist/apps/shared/notifications/preferences.d.ts +1 -1
  257. package/dist/apps/shared/notifications/preferences.js +1 -1
  258. package/dist/apps/shared/notifications/store.js +3 -1
  259. package/dist/apps/shared/plugins/pluginNav.d.ts +2 -1
  260. package/dist/apps/shared/plugins/pluginNav.js +4 -2
  261. package/dist/apps/www/calendar/index.js +24 -12
  262. package/dist/apps/www/contacts/[uid].js +6 -2
  263. package/dist/apps/www/contacts/index.js +62 -11
  264. package/dist/apps/www/index.js +85 -30
  265. package/dist/apps/www/messages/[uid].js +8 -3
  266. package/dist/apps/www/settings/auto-reply/index.js +13 -1
  267. package/dist/apps/www/settings/blocked-senders/index.js +4 -0
  268. package/dist/apps/www/settings/encryption/index.js +29 -5
  269. package/dist/apps/www/settings/privacy/index.js +3 -2
  270. package/dist/apps/www/settings/profile/index.js +4 -1
  271. package/dist/apps/www/settings/sharing/index.js +10 -1
  272. package/dist/apps/www/settings/signatures/[uid].js +6 -4
  273. package/dist/apps/www/tasks/index.js +94 -32
  274. package/dist/lib/admin/auditLogApi.js +2 -2
  275. package/dist/lib/admin/distributionListsApi.js +2 -2
  276. package/dist/lib/admin/domainsApi.js +2 -2
  277. package/dist/lib/admin/escrowAuditLogApi.js +2 -2
  278. package/dist/lib/admin/escrowScopesApi.js +2 -2
  279. package/dist/lib/admin/pluginsApi.d.ts +13 -2
  280. package/dist/lib/admin/pluginsApi.js +8 -0
  281. package/dist/lib/admin/retentionPolicyApi.d.ts +3 -0
  282. package/dist/lib/admin/retentionPolicyApi.js +3 -0
  283. package/dist/lib/admin/transportRulesApi.js +2 -2
  284. package/dist/lib/appearance/preferencesApi.d.ts +1 -1
  285. package/dist/lib/appearance/preferencesApi.js +3 -14
  286. package/dist/lib/auth/session.d.ts +51 -12
  287. package/dist/lib/auth/session.js +214 -38
  288. package/dist/lib/branding/brandingApi.d.ts +3 -3
  289. package/dist/lib/branding/brandingApi.js +9 -21
  290. package/dist/lib/calendar/calendarApi.js +3 -1
  291. package/dist/lib/calendar/eventSearch.js +43 -6
  292. package/dist/lib/calendar/freeBusyApi.d.ts +2 -1
  293. package/dist/lib/calendar/freeBusyApi.js +13 -5
  294. package/dist/lib/calendar/recurrence.js +51 -3
  295. package/dist/lib/calendar/useDayKey.d.ts +7 -0
  296. package/dist/lib/calendar/useDayKey.js +43 -0
  297. package/dist/lib/components/avatar/ContactAvatar.d.ts +6 -2
  298. package/dist/lib/components/avatar/ContactAvatar.js +37 -8
  299. package/dist/lib/components/avatar/ContactPhotoAvatar.d.ts +11 -0
  300. package/dist/lib/components/avatar/ContactPhotoAvatar.js +18 -0
  301. package/dist/lib/components/forms/CameraCaptureModal.d.ts +17 -0
  302. package/dist/lib/components/forms/CameraCaptureModal.js +88 -0
  303. package/dist/lib/components/forms/ImageEditBadge.d.ts +28 -0
  304. package/dist/lib/components/forms/ImageEditBadge.js +62 -0
  305. package/dist/lib/components/menus/MenuButton.d.ts +78 -0
  306. package/dist/lib/components/menus/MenuButton.js +219 -0
  307. package/dist/lib/components/navigation/BottomTabBar.d.ts +3 -1
  308. package/dist/lib/components/navigation/BottomTabBar.js +22 -3
  309. package/dist/lib/components/overlays/PopoverPortal.d.ts +2 -1
  310. package/dist/lib/components/overlays/PopoverPortal.js +3 -2
  311. package/dist/lib/components/pickers/TimeZonePicker.js +13 -1
  312. package/dist/lib/contacts/contactsApi.d.ts +5 -2
  313. package/dist/lib/contacts/contactsApi.js +16 -19
  314. package/dist/lib/contacts/gravatar.d.ts +14 -3
  315. package/dist/lib/contacts/gravatar.js +42 -5
  316. package/dist/lib/contacts/preparePhoto.d.ts +24 -0
  317. package/dist/lib/contacts/preparePhoto.js +250 -0
  318. package/dist/lib/contacts/useContactPhotoSrc.d.ts +19 -0
  319. package/dist/lib/contacts/useContactPhotoSrc.js +153 -0
  320. package/dist/lib/contacts/vcard.d.ts +3 -0
  321. package/dist/lib/contacts/vcard.js +59 -7
  322. package/dist/lib/crypto/escrowKeys.d.ts +8 -0
  323. package/dist/lib/crypto/escrowKeys.js +20 -1
  324. package/dist/lib/crypto/messageSecurity.d.ts +7 -4
  325. package/dist/lib/crypto/messageSecurity.js +23 -5
  326. package/dist/lib/crypto/passwordUnlock.d.ts +6 -1
  327. package/dist/lib/crypto/passwordUnlock.js +13 -2
  328. package/dist/lib/crypto/smime.js +12 -2
  329. package/dist/lib/mail/compose/composeQuoting.js +3 -2
  330. package/dist/lib/mail/compose/quotedImages.d.ts +6 -3
  331. package/dist/lib/mail/compose/quotedImages.js +35 -9
  332. package/dist/lib/mail/labelsApi.js +2 -2
  333. package/dist/lib/mail/mailApi.d.ts +8 -5
  334. package/dist/lib/mail/mailApi.js +73 -39
  335. package/dist/lib/mail/mailDetailHooks.js +11 -3
  336. package/dist/lib/mail/mailFilterRulesApi.js +2 -2
  337. package/dist/lib/mail/mailSignaturesApi.js +2 -2
  338. package/dist/lib/mail/mailboxImportApi.d.ts +1 -1
  339. package/dist/lib/mail/mailboxImportApi.js +3 -15
  340. package/dist/lib/mail/messageBodySanitizer.d.ts +6 -0
  341. package/dist/lib/mail/messageBodySanitizer.js +44 -1
  342. package/dist/lib/search/queryGrammar.js +12 -4
  343. package/dist/lib/search/searchTier3.d.ts +6 -1
  344. package/dist/lib/search/searchTier3.js +54 -25
  345. package/dist/lib/tasks/tasksApi.js +3 -3
  346. package/dist/lib/util/api.d.ts +26 -4
  347. package/dist/lib/util/api.js +73 -16
  348. package/dist/lib/util/apiQuery.d.ts +11 -0
  349. package/dist/lib/util/apiQuery.js +11 -0
  350. package/dist/lib/util/settleWithLimit.d.ts +5 -0
  351. package/dist/lib/util/settleWithLimit.js +25 -0
  352. package/lib/admin/auditLogApi.ts +2 -2
  353. package/lib/admin/distributionListsApi.ts +2 -2
  354. package/lib/admin/domainsApi.ts +2 -2
  355. package/lib/admin/escrowAuditLogApi.ts +2 -2
  356. package/lib/admin/escrowScopesApi.ts +2 -2
  357. package/lib/admin/pluginsApi.ts +21 -2
  358. package/lib/admin/retentionPolicyApi.ts +4 -0
  359. package/lib/admin/transportRulesApi.ts +2 -2
  360. package/lib/appearance/preferencesApi.ts +3 -14
  361. package/lib/auth/session.ts +239 -39
  362. package/lib/branding/brandingApi.ts +9 -21
  363. package/lib/calendar/calendarApi.ts +3 -1
  364. package/lib/calendar/eventSearch.ts +46 -6
  365. package/lib/calendar/freeBusyApi.ts +14 -6
  366. package/lib/calendar/recurrence.ts +59 -8
  367. package/lib/calendar/useDayKey.ts +45 -0
  368. package/lib/components/avatar/ContactAvatar.tsx +46 -8
  369. package/lib/components/avatar/ContactPhotoAvatar.tsx +26 -0
  370. package/lib/components/forms/CameraCaptureModal.tsx +133 -0
  371. package/lib/components/forms/ImageEditBadge.tsx +121 -0
  372. package/lib/components/menus/MenuButton.tsx +408 -0
  373. package/lib/components/navigation/BottomTabBar.tsx +30 -8
  374. package/lib/components/overlays/PopoverPortal.tsx +3 -2
  375. package/lib/components/pickers/TimeZonePicker.tsx +17 -1
  376. package/lib/contacts/contactsApi.ts +18 -19
  377. package/lib/contacts/gravatar.ts +47 -5
  378. package/lib/contacts/preparePhoto.ts +273 -0
  379. package/lib/contacts/useContactPhotoSrc.ts +177 -0
  380. package/lib/contacts/vcard.ts +60 -7
  381. package/lib/crypto/escrowKeys.ts +21 -1
  382. package/lib/crypto/messageSecurity.ts +33 -9
  383. package/lib/crypto/passwordUnlock.ts +14 -2
  384. package/lib/crypto/smime.ts +13 -2
  385. package/lib/mail/compose/composeQuoting.ts +3 -2
  386. package/lib/mail/compose/quotedImages.ts +39 -9
  387. package/lib/mail/labelsApi.ts +2 -2
  388. package/lib/mail/mailApi.ts +96 -39
  389. package/lib/mail/mailDetailHooks.ts +11 -3
  390. package/lib/mail/mailFilterRulesApi.ts +2 -2
  391. package/lib/mail/mailSignaturesApi.ts +2 -2
  392. package/lib/mail/mailboxImportApi.ts +9 -15
  393. package/lib/mail/messageBodySanitizer.ts +47 -1
  394. package/lib/search/queryGrammar.ts +13 -4
  395. package/lib/search/searchTier3.ts +65 -32
  396. package/lib/tasks/tasksApi.ts +3 -3
  397. package/lib/util/api.ts +99 -16
  398. package/lib/util/apiQuery.ts +13 -0
  399. package/lib/util/settleWithLimit.ts +25 -0
  400. package/package.json +1 -1
@@ -13,7 +13,7 @@
13
13
  */
14
14
 
15
15
  import { ApiClient, withClient } from "../util/api.js";
16
- import { ListParams, buildQuery } from "../util/apiQuery.js";
16
+ import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
17
17
 
18
18
  export type { ListParams };
19
19
 
@@ -57,7 +57,7 @@ export interface EscrowAuditLogEntry {
57
57
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
58
58
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
59
59
  export function listAuditLogEntries(params: ListParams = {}, client?: ApiClient): Promise<EscrowAuditLogEntry[]> {
60
- return withClient(client, `/escrow/audit-log?${buildQuery(params)}`);
60
+ return withClient(client, `/escrow/audit-log?${buildQuery(params, sortQuery({ sequence: "DESC" }))}`);
61
61
  }
62
62
 
63
63
  export function getAuditLogEntry(uid: string, client?: ApiClient): Promise<EscrowAuditLogEntry> {
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { ApiClient, withClient } from "../util/api.js";
15
- import { ListParams, buildQuery } from "../util/apiQuery.js";
15
+ import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
16
16
  import type { ResolvedPrincipal } from "../mail/mailboxAccessApi.js";
17
17
 
18
18
  export type { ListParams };
@@ -53,7 +53,7 @@ export interface EscrowScope {
53
53
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
54
54
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
55
55
  export function listEscrowScopes(params: ListParams = {}, client?: ApiClient): Promise<EscrowScope[]> {
56
- return withClient(client, `/escrow/scopes?${buildQuery(params)}`);
56
+ return withClient(client, `/escrow/scopes?${buildQuery(params, sortQuery({ uid: "ASC" }))}`);
57
57
  }
58
58
 
59
59
  /**
@@ -37,6 +37,25 @@ export interface PluginManifest {
37
37
 
38
38
  export type PluginSettingValue = string | number | boolean;
39
39
 
40
+ /** What `@rapidmx/restapi` answers in place of the saved value of a setting whose key names a secret (secret, password, credential, token, api key), so a
41
+ * stored API key or shared secret never reaches the console. Sent back as it was given, it means "left alone": the saved value stays. */
42
+ export interface PluginSecretSetting {
43
+ secret: true;
44
+ }
45
+
46
+ /** A saved setting as a response shows it. */
47
+ export type PluginSavedSetting = PluginSettingValue | PluginSecretSetting;
48
+
49
+ /** Whether `value` is the placeholder a saved secret setting is shown as. */
50
+ export function isSecretSetting(value: unknown): value is PluginSecretSetting {
51
+ return typeof value === "object" && value !== null && (value as { secret?: unknown }).secret === true;
52
+ }
53
+
54
+ /** Whether a setting's key names a secret - mirrors `@rapidmx/restapi`'s `SECRET_SETTING_KEY`, which decides what it withholds. */
55
+ export function isSecretSettingKey(key: string): boolean {
56
+ return /secret|password|credential|token|api_?key/i.test(key);
57
+ }
58
+
40
59
  /** Mirrors `@rapidmx/restapi`'s `PluginConfiguredSetting`: what the deployment's own configuration (the command line, the
41
60
  * environment, the server's defaults) says about one setting. It applies until a value is saved, which wins over it. */
42
61
  export interface PluginConfiguredSetting {
@@ -54,7 +73,7 @@ export interface Plugin {
54
73
  packageVersion: string;
55
74
  integrity?: string;
56
75
  enabled: boolean;
57
- settings: Record<string, PluginSettingValue>;
76
+ settings: Record<string, PluginSavedSetting>;
58
77
  /** For each setting the deployment's configuration provides a value for, what it says. Absent from a server that
59
78
  * predates it, which reads as nothing being configured. */
60
79
  configured?: Record<string, PluginConfiguredSetting>;
@@ -216,7 +235,7 @@ export interface UpdatePluginInput {
216
235
  packageVersion?: string;
217
236
  enabled?: boolean;
218
237
  /** A `null` value clears a setting back to the plugin's own default. */
219
- settings?: Record<string, PluginSettingValue | null>;
238
+ settings?: Record<string, PluginSavedSetting | null>;
220
239
  /** What a previewed version change or enable was confirmed to also install and enable. */
221
240
  expectedPlan?: PluginExpectedPlan;
222
241
  }
@@ -27,6 +27,10 @@ export interface RetentionPolicy {
27
27
  * the server enforces this regardless. */
28
28
  export const MIN_AUDIT_LOG_RETENTION_DAYS = 2190;
29
29
 
30
+ /** The server-enforced floor for `messageRetentionDays` (30 days) - `MIN_MESSAGE_RETENTION_DAYS` in `@rapidmx/restapi`, mirrored here for the same reason as
31
+ * `MIN_AUDIT_LOG_RETENTION_DAYS`. A shorter period is refused with a 400. */
32
+ export const MIN_MESSAGE_RETENTION_DAYS = 30;
33
+
30
34
  /** `client`, given by every function below, is an explicit `ApiClient` from `createApiClient()` (e.g. one
31
35
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
32
36
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { ApiClient, withClient } from "../util/api.js";
13
- import { ListParams, buildQuery } from "../util/apiQuery.js";
13
+ import { ListParams, buildQuery, sortQuery } from "../util/apiQuery.js";
14
14
 
15
15
  export type { ListParams };
16
16
 
@@ -53,7 +53,7 @@ export interface TransportRule {
53
53
  * account of a multi-account app) to call instead of the default global `apiFetch()` - see `withClient()`'s
54
54
  * own doc comment in `util/api.ts`. Omitted (the default), every function here behaves exactly as before. */
55
55
  export function listTransportRules(params: ListParams = {}, client?: ApiClient): Promise<TransportRule[]> {
56
- return withClient(client, `/mail/transport-rules?${buildQuery(params)}`);
56
+ return withClient(client, `/mail/transport-rules?${buildQuery(params, sortQuery({ sequence: "ASC", uid: "ASC" }))}`);
57
57
  }
58
58
 
59
59
  export function getTransportRule(uid: string, client?: ApiClient): Promise<TransportRule> {
@@ -19,7 +19,7 @@
19
19
  * Everything that comes from outside - the response, a push event, the page's server-rendered props, `localStorage` - goes through
20
20
  * `normalizeAppearance()`, which keeps only well-formed values (so a stale or hostile value can never reach a CSS declaration).
21
21
  */
22
- import { ApiClient, ApiRequestError, apiUrl, withClient, withCsrfHeader } from "../util/api.js";
22
+ import { ApiClient, ApiRequestError, apiUrl, withClient, withClientRaw } from "../util/api.js";
23
23
 
24
24
  export type AppearanceMode = "system" | "light" | "dark";
25
25
  export type BackgroundKind = "none" | "color" | "image";
@@ -283,19 +283,8 @@ export async function saveAppearance(update: AppearanceUpdate, client?: ApiClien
283
283
 
284
284
  /** Stores `file` (already checked with `validateBackgroundFile()`) as the background: its raw bytes, with its own content type - which
285
285
  * bypasses `apiFetch()`, as it always sends JSON. Answers with the updated preferences. */
286
- export async function uploadAppearanceBackground(file: Blob): Promise<AppearancePreferences> {
287
- const res = await fetch(apiUrl(`${PATH}/background`), {
288
- method: "POST",
289
- credentials: "include",
290
- headers: withCsrfHeader({ "Content-Type": file.type || "application/octet-stream" }),
291
- body: file,
292
- });
293
- const contentType = res.headers.get("content-type") ?? "";
294
- const body = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
295
- if (!res.ok) {
296
- const message = (body && (body.message || body.error)) || res.statusText || "Upload failed.";
297
- throw new ApiRequestError(message, res.status, body?.code);
298
- }
286
+ export async function uploadAppearanceBackground(file: Blob, client?: ApiClient): Promise<AppearancePreferences> {
287
+ const body = await withClientRaw(client, `${PATH}/background`, "POST", file, file.type || "application/octet-stream");
299
288
  const stored = normalizeAppearance(body);
300
289
  if (!stored) {
301
290
  throw new ApiRequestError("The server did not return the saved appearance.", 502);
@@ -25,9 +25,36 @@ export const SESSION_REFRESH_CHECK_MS = 60 * 1000;
25
25
  /** A reload that follows a successful recovery refresh but still finds no session is not repeated within this window. */
26
26
  export const SESSION_RELOAD_GUARD_MS = 60 * 1000;
27
27
 
28
+ /** A refresh answered with a refusal is asked once more after this long, in case the cookies were rotated by another tab or the server's session save lagged. */
29
+ export const SESSION_REFRESH_RETRY_MS = 1500;
30
+ /** A forced refresh (a request was refused, or the page has no session) does nothing when a refresh succeeded within this long: the refusal came from the old cookies. */
31
+ export const SESSION_REFRESH_MIN_GAP_MS = 10 * 1000;
32
+ /** How many entries `rapidmx.session.log` keeps. */
33
+ export const SESSION_LOG_MAX = 40;
34
+
28
35
  const LOCK_NAME = "rapidmx-session-refresh";
29
36
  const REFRESHED_AT_KEY = "rapidmx.session.refreshedAt";
30
37
  const RELOADED_AT_KEY = "rapidmx.session.reloadedAt";
38
+ const LOG_KEY = "rapidmx.session.log";
39
+
40
+ /**
41
+ * How long a refresh request may go unanswered before it is given up on (not aborted - see `postRefresh()`). A refresh runs inside a cross-tab Web Lock, so one that hangs would hold the lock - and
42
+ * with it every tab's refresh, and every request waiting on a recovery - for good.
43
+ */
44
+ export const SESSION_REFRESH_TIMEOUT_MS = 15 * 1000;
45
+
46
+ let retryDelayMs = SESSION_REFRESH_RETRY_MS;
47
+ let refreshTimeoutMs = SESSION_REFRESH_TIMEOUT_MS;
48
+
49
+ /** Sets how long a refresh request may go unanswered (for tests); `resetSessionRecovery()` restores the default. */
50
+ export function setSessionRefreshTimeout(ms: number): void {
51
+ refreshTimeoutMs = ms;
52
+ }
53
+
54
+ /** Sets how long a refused refresh waits before its one retry (for tests); `resetSessionRecovery()` restores the default. */
55
+ export function setSessionRefreshRetryDelay(ms: number): void {
56
+ retryDelayMs = ms;
57
+ }
31
58
 
32
59
  function readTimestamp(storage: () => Storage, key: string): number {
33
60
  try {
@@ -46,34 +73,186 @@ function writeTimestamp(storage: () => Storage, key: string, value: number): voi
46
73
  }
47
74
  }
48
75
 
76
+ /** Whether a refresh succeeded within the last `ms`, in this tab or another (they share `localStorage`). A cleared or future-dated record counts as no. */
77
+ export function refreshedRecently(ms: number): boolean {
78
+ const at = readTimestamp(() => localStorage, REFRESHED_AT_KEY);
79
+ const age = Date.now() - at;
80
+ return at > 0 && age >= 0 && age < ms;
81
+ }
82
+
83
+ /** One entry of the session log (`rapidmx.session.log`). */
84
+ export interface SessionLogEntry {
85
+ /** When it happened, ISO 8601. */
86
+ t: string;
87
+ /** What happened, e.g. `refresh:start`, `refresh:rejected`, `leave`. */
88
+ e: string;
89
+ /** A short detail (a reason, a status, an age). Never a token or cookie value. */
90
+ d?: string;
91
+ }
92
+
93
+ /** The session log: the last `SESSION_LOG_MAX` things the silent refresh did, oldest first, kept in `localStorage['rapidmx.session.log']` so the cause of an unexpected sign-in can be read afterward. */
94
+ export function getSessionLog(): SessionLogEntry[] {
95
+ try {
96
+ const value = JSON.parse(localStorage.getItem(LOG_KEY) ?? "[]");
97
+ return Array.isArray(value) ? value : [];
98
+ } catch {
99
+ return [];
100
+ }
101
+ }
102
+
103
+ /** Appends to the session log, dropping the oldest entries past `SESSION_LOG_MAX`. Best effort: blocked storage loses the entry. */
104
+ export function logSessionEvent(e: string, d?: string): void {
105
+ try {
106
+ const entries = getSessionLog();
107
+ entries.push(d === undefined ? { t: new Date().toISOString(), e } : { t: new Date().toISOString(), e, d });
108
+ localStorage.setItem(LOG_KEY, JSON.stringify(entries.slice(-SESSION_LOG_MAX)));
109
+ } catch {
110
+ // The log is evidence only; it must never get in the way of the session.
111
+ }
112
+ }
113
+
114
+ /** How long ago the last successful refresh was, for log entries: `12s`, or `never` when this browser has no record. */
115
+ function sinceLastRefresh(): string {
116
+ const at = readTimestamp(() => localStorage, REFRESHED_AT_KEY);
117
+ return at > 0 ? `${Math.round((Date.now() - at) / 1000)}s` : "never";
118
+ }
119
+
49
120
  /** The URL of auth-server's sign-in page, carrying the current URL as `return_to` so it can send the browser back here. */
50
121
  function signInUrl(authServerUrl: string): string {
51
122
  return `${authServerUrl}/auth/signin?return_to=${encodeURIComponent(window.location.href)}`;
52
123
  }
53
124
 
125
+ /** Sends the browser to auth-server's sign-in page, first writing why to the session log and printing the log to the console. */
126
+ function goToSignIn(authServerUrl: string, reason: string): void {
127
+ logSessionEvent("leave", `${reason} sinceOk=${sinceLastRefresh()}`);
128
+ console.info("[rapidmx] Going to sign-in. Session log:", getSessionLog());
129
+ window.location.href = signInUrl(authServerUrl);
130
+ }
131
+
132
+ /** Why a refresh was made, for the session log. */
133
+ export type SessionRefreshReason = "check" | "recovery" | "nouser";
134
+
135
+ /** Whether `err` says auth-server rejected the refresh token itself, as opposed to a failure worth retrying. */
136
+ function isAuthRejection(err: unknown): boolean {
137
+ return err instanceof ApiRequestError && (err.status === 401 || err.status === 403);
138
+ }
139
+
140
+ function describeFailure(err: unknown): string {
141
+ if (err instanceof ApiRequestError) {
142
+ return `status=${err.status}${err.code ? ` code=${err.code}` : ""} sinceOk=${sinceLastRefresh()}`;
143
+ }
144
+ return `${err instanceof Error ? err.message : "unknown"}`.slice(0, 80);
145
+ }
146
+
147
+ /** Counts the refresh requests made, so a request that answers after it was given up on can tell whether a newer one has started since. */
148
+ let refreshSeq = 0;
149
+
150
+ /**
151
+ * One `POST /auth/refresh`, recording when it succeeded. Given up on after `SESSION_REFRESH_TIMEOUT_MS`, which is a failure worth retrying, not a rejection.
152
+ * Giving up only stops waiting: the request is NOT aborted. Auth-server rotates the single-use refresh token as it answers, and a browser that aborts before the
153
+ * response headers arrive never stores the rotated cookie, leaving the consumed token behind - the next refresh would be refused and the user signed out.
154
+ * A late answer is still logged, and a late success still recorded unless a newer refresh has started since.
155
+ */
156
+ async function postRefresh(authServerUrl: string): Promise<void> {
157
+ const seq = ++refreshSeq;
158
+ const request = authApiFetch(authServerUrl, "/auth/refresh", { method: "POST" });
159
+ let timedOut = false;
160
+ let timer: ReturnType<typeof setTimeout> | undefined;
161
+ const timeout = new Promise<never>((_resolve, reject) => {
162
+ timer = setTimeout(() => {
163
+ timedOut = true;
164
+ reject(new Error(`refresh timed out after ${refreshTimeoutMs}ms`));
165
+ }, refreshTimeoutMs);
166
+ });
167
+ // Handles the request's own outcome whenever it comes, so a rejection after the timeout is never unhandled.
168
+ request.then(
169
+ () => {
170
+ if (!timedOut) {
171
+ return;
172
+ }
173
+ if (seq === refreshSeq) {
174
+ writeTimestamp(() => localStorage, REFRESHED_AT_KEY, Date.now());
175
+ logSessionEvent("refresh:late-ok");
176
+ } else {
177
+ logSessionEvent("refresh:late-superseded");
178
+ }
179
+ },
180
+ (err) => {
181
+ if (timedOut) {
182
+ logSessionEvent("refresh:late-error", describeFailure(err));
183
+ }
184
+ },
185
+ );
186
+ try {
187
+ await Promise.race([request, timeout]);
188
+ } finally {
189
+ clearTimeout(timer);
190
+ }
191
+ writeTimestamp(() => localStorage, REFRESHED_AT_KEY, Date.now());
192
+ }
193
+
194
+ let refreshing: { promise: Promise<boolean>; forced: boolean } | undefined;
195
+
54
196
  /**
55
197
  * Refreshes the signed-in session through auth-server: exchanges the `refresh` cookie for a new access token and
56
198
  * refresh token, both set as cookies on the response. Rejects with the `ApiRequestError` auth-server answered
57
199
  * (`401`/`403` when the refresh token has expired or was revoked).
58
200
  *
59
- * A refresh token is single-use and bound to the session, so two tabs refreshing at once would make the second one look
60
- * like a dead session. Refreshes are therefore serialised across tabs (Web Locks, where the browser has them) and each
61
- * tab records when the last one happened, so a tab that gets its turn right after another tab refreshed does nothing.
201
+ * A refresh token is single-use and bound to the session, and auth-server saves the rotated token a moment after it
202
+ * answers, so refreshing twice in quick succession makes the second look like a dead session. Refreshes are therefore
203
+ * shared by every caller in the tab (one in flight at a time), serialised across tabs (Web Locks, where the browser has
204
+ * them), and each tab records when the last one succeeded, so a caller that gets its turn right after a refresh does
205
+ * nothing. A refresh that is refused is asked once more after `SESSION_REFRESH_RETRY_MS` (the cookies may have been
206
+ * rotated meanwhile) before it counts as a rejection.
62
207
  *
63
- * @param force Refresh even when the last refresh, by this tab or another, was recent.
208
+ * @param force Refresh even when the last refresh, by this tab or another, was `SESSION_REFRESH_AFTER_MS` ago or less. Even forced, a refresh made within `SESSION_REFRESH_MIN_GAP_MS` is not repeated.
209
+ * @param reason Why, for the session log.
64
210
  * @returns `true` when a refresh was made, `false` when one was not needed.
65
211
  */
66
- export async function refreshSession(authServerUrl: string, force = false): Promise<boolean> {
212
+ export function refreshSession(authServerUrl: string, force = false, reason: SessionRefreshReason = force ? "recovery" : "check"): Promise<boolean> {
213
+ if (refreshing) {
214
+ // A forced caller must not take the outcome of a refresh that may have been skipped as not due: ask again once it is over (which the minimum gap keeps from being a second POST when it did refresh).
215
+ return force && !refreshing.forced ? refreshing.promise.then(() => refreshSession(authServerUrl, true, reason)) : refreshing.promise;
216
+ }
67
217
  const run = async (): Promise<boolean> => {
68
- if (!force && Date.now() - readTimestamp(() => localStorage, REFRESHED_AT_KEY) < SESSION_REFRESH_AFTER_MS) {
218
+ if (refreshedRecently(force ? SESSION_REFRESH_MIN_GAP_MS : SESSION_REFRESH_AFTER_MS)) {
219
+ if (force) {
220
+ logSessionEvent("refresh:skip", `recent sinceOk=${sinceLastRefresh()} reason=${reason}`);
221
+ }
69
222
  return false;
70
223
  }
71
- await authApiFetch(authServerUrl, "/auth/refresh", { method: "POST" });
72
- writeTimestamp(() => localStorage, REFRESHED_AT_KEY, Date.now());
224
+ logSessionEvent("refresh:start", `reason=${reason} sinceOk=${sinceLastRefresh()}`);
225
+ try {
226
+ await postRefresh(authServerUrl);
227
+ } catch (err) {
228
+ if (!isAuthRejection(err)) {
229
+ logSessionEvent("refresh:error", describeFailure(err));
230
+ throw err;
231
+ }
232
+ logSessionEvent("refresh:rejected", `${describeFailure(err)} attempt=1`);
233
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
234
+ if (refreshedRecently(SESSION_REFRESH_MIN_GAP_MS)) {
235
+ // Another tab renewed the session while this one waited.
236
+ logSessionEvent("refresh:skip", `recent-after-retry-wait reason=${reason}`);
237
+ return false;
238
+ }
239
+ logSessionEvent("refresh:retry", `reason=${reason}`);
240
+ try {
241
+ await postRefresh(authServerUrl);
242
+ } catch (err2) {
243
+ logSessionEvent(isAuthRejection(err2) ? "refresh:rejected" : "refresh:error", `${describeFailure(err2)} attempt=2`);
244
+ throw err2;
245
+ }
246
+ }
247
+ logSessionEvent("refresh:ok", `reason=${reason}`);
73
248
  return true;
74
249
  };
75
250
  const locks = typeof navigator === "undefined" ? undefined : navigator.locks;
76
- return locks ? locks.request(LOCK_NAME, run) : run();
251
+ const promise = (locks ? locks.request(LOCK_NAME, run) : run()).finally(() => {
252
+ refreshing = undefined;
253
+ });
254
+ refreshing = { promise, forced: force };
255
+ return promise;
77
256
  }
78
257
 
79
258
  /** How long the outcome of a recovery is reused for: requests that fail together share one refresh, and a session that will not come back is not asked again at once. */
@@ -83,9 +262,10 @@ let recovery: Promise<boolean> | undefined;
83
262
  let lastRecovery: { at: number; renewed: boolean } | undefined;
84
263
 
85
264
  /**
86
- * Renews the session because a request was refused for lack of one, whatever this browser's record of the last refresh says. Every request
87
- * refused at about the same time shares one refresh, and its outcome is reused for `SESSION_RECOVERY_COOLDOWN_MS`. A refusal also clears the record of
88
- * the last refresh, so the next regular check asks again (and sends the browser to sign-in if the refresh token is truly gone).
265
+ * Renews the session because a request was refused for lack of one, whatever this browser's record of the last refresh says - except that a refresh that
266
+ * succeeded within `SESSION_REFRESH_MIN_GAP_MS` (by this tab or another) is not repeated: the request was refused with the cookies from before it, so it is
267
+ * simply sent again with the new ones. Every request refused at about the same time shares one refresh, and its outcome is reused for `SESSION_RECOVERY_COOLDOWN_MS`.
268
+ * A refusal also clears the record of the last refresh, so the next regular check asks again (and sends the browser to sign-in if the refresh token is truly gone).
89
269
  *
90
270
  * @returns Whether the session was renewed.
91
271
  */
@@ -93,7 +273,7 @@ export function recoverSession(authServerUrl: string): Promise<boolean> {
93
273
  if (lastRecovery && Date.now() - lastRecovery.at < SESSION_RECOVERY_COOLDOWN_MS) {
94
274
  return Promise.resolve(lastRecovery.renewed);
95
275
  }
96
- recovery ??= refreshSession(authServerUrl, true)
276
+ recovery ??= refreshSession(authServerUrl, true, "recovery")
97
277
  .then(
98
278
  () => true,
99
279
  () => {
@@ -109,15 +289,13 @@ export function recoverSession(authServerUrl: string): Promise<boolean> {
109
289
  return recovery;
110
290
  }
111
291
 
112
- /** Forgets what recoveries have done (for tests). */
292
+ /** Forgets what recoveries and refreshes have done, and restores the retry delay and timeout (for tests). */
113
293
  export function resetSessionRecovery(): void {
114
294
  recovery = undefined;
115
295
  lastRecovery = undefined;
116
- }
117
-
118
- /** Whether `err` says auth-server rejected the refresh token itself, as opposed to a failure worth retrying. */
119
- function isAuthRejection(err: unknown): boolean {
120
- return err instanceof ApiRequestError && (err.status === 401 || err.status === 403);
296
+ refreshing = undefined;
297
+ retryDelayMs = SESSION_REFRESH_RETRY_MS;
298
+ refreshTimeoutMs = SESSION_REFRESH_TIMEOUT_MS;
121
299
  }
122
300
 
123
301
  /**
@@ -140,7 +318,7 @@ export function useRedirectIfUnauthenticated(userUid: string | undefined, authSe
140
318
  console.error("Cannot redirect to sign-in: mail:auth_server_url is not configured.");
141
319
  return;
142
320
  }
143
- window.location.href = signInUrl(authServerUrl);
321
+ goToSignIn(authServerUrl, "no-session");
144
322
  }, [userUid, authServerUrl, explicitClient]);
145
323
  }
146
324
 
@@ -171,9 +349,11 @@ export interface SessionRefreshOptions {
171
349
  * - `userUid` set: also renews the session when a request is refused for lack of one (`apiFetch()` asks `recoverSession()` and sends the request
172
350
  * again), so a token that ran out while timers were stopped costs nothing. Refreshes once the token is `SESSION_REFRESH_AFTER_MS` old (and right away when this browser has no
173
351
  * record of a recent refresh, since the page's token age is unknown), checked every `SESSION_REFRESH_CHECK_MS` and
174
- * whenever the tab becomes visible or the network returns, because a sleeping laptop stops timers. A failure that is
175
- * not a rejection is simply retried at the next check. A rejection (`401`/`403`: the refresh token expired or was
176
- * revoked, so the session cannot be kept) redirects to sign-in with `return_to` after `options.beforeRedirect` has run.
352
+ * whenever the tab becomes visible or focused or the network returns, because a sleeping laptop stops timers (a tab that wakes after the
353
+ * token's hour checks first, and a request refused meanwhile waits for that same refresh rather than starting its own). A failure that is
354
+ * not a rejection is simply retried at the next check. A rejection (`401`/`403` twice running, `SESSION_REFRESH_RETRY_MS` apart: the refresh
355
+ * token expired or was revoked, so the session cannot be kept) redirects to sign-in with `return_to` after `options.beforeRedirect` has run.
356
+ * Everything it does is recorded in the session log (`getSessionLog()`), which is also printed to the console when the browser goes to sign-in.
177
357
  *
178
358
  * **A no-op under an `ApiClientContext.Provider`** (see `util/apiClientContext.ts`): everything above assumes a
179
359
  * `jwt`/`refresh` cookie pair this app can renew through auth-server's own `/auth/refresh` - meaningless for a
@@ -205,21 +385,26 @@ export function useSessionRefresh(userUid: string | undefined, authServerUrl: st
205
385
  let cancelled = false;
206
386
 
207
387
  if (!userUid) {
208
- if (paused || Date.now() - readTimestamp(() => sessionStorage, RELOADED_AT_KEY) < SESSION_RELOAD_GUARD_MS) {
209
- window.location.href = signInUrl(authServerUrl);
388
+ if (paused) {
389
+ goToSignIn(authServerUrl, "no-session paused");
210
390
  return;
211
391
  }
212
- // Forced: the page has no session, so what this browser remembers of the last refresh says nothing about whether one is due.
213
- refreshSession(authServerUrl, true)
392
+ if (Date.now() - readTimestamp(() => sessionStorage, RELOADED_AT_KEY) < SESSION_RELOAD_GUARD_MS) {
393
+ goToSignIn(authServerUrl, "no-session after-reload");
394
+ return;
395
+ }
396
+ // Forced: the page has no session, so what this browser remembers of the last refresh says nothing about whether one is due (but one made seconds ago is not repeated).
397
+ refreshSession(authServerUrl, true, "nouser")
214
398
  .then(() => {
215
399
  if (!cancelled) {
400
+ logSessionEvent("reload-after-refresh");
216
401
  writeTimestamp(() => sessionStorage, RELOADED_AT_KEY, Date.now());
217
402
  window.location.reload();
218
403
  }
219
404
  })
220
405
  .catch(() => {
221
406
  if (!cancelled) {
222
- window.location.href = signInUrl(authServerUrl);
407
+ goToSignIn(authServerUrl, "no-session refresh-rejected");
223
408
  }
224
409
  });
225
410
  return () => {
@@ -236,26 +421,32 @@ export function useSessionRefresh(userUid: string | undefined, authServerUrl: st
236
421
 
237
422
  let leaving = false;
238
423
  let inFlight = false;
239
- const leave = async () => {
424
+ let hiddenAt: number | undefined;
425
+ const leave = async (reason: string) => {
240
426
  leaving = true;
427
+ logSessionEvent("leave:pending", reason);
241
428
  try {
242
429
  await beforeRedirectRef.current?.();
243
430
  } catch {
244
431
  // Saving what can be saved is best effort; the session is over either way.
245
432
  }
246
433
  if (!cancelled) {
247
- window.location.href = signInUrl(authServerUrl);
434
+ goToSignIn(authServerUrl, reason);
248
435
  }
249
436
  };
250
- const check = () => {
437
+ // `trigger` names what woke the check, for the log; the periodic tick passes none, so it logs only when it actually refreshes.
438
+ const check = (trigger?: string) => {
251
439
  if (cancelled || leaving || inFlight) {
252
440
  return;
253
441
  }
442
+ if (trigger) {
443
+ logSessionEvent("check", trigger);
444
+ }
254
445
  inFlight = true;
255
- refreshSession(authServerUrl)
446
+ refreshSession(authServerUrl, false, "check")
256
447
  .catch((err) => {
257
448
  if (isAuthRejection(err)) {
258
- void leave();
449
+ void leave("refresh-rejected");
259
450
  }
260
451
  })
261
452
  .finally(() => {
@@ -263,21 +454,30 @@ export function useSessionRefresh(userUid: string | undefined, authServerUrl: st
263
454
  });
264
455
  };
265
456
  const onVisible = () => {
266
- if (document.visibilityState === "visible") {
267
- check();
457
+ if (document.visibilityState === "hidden") {
458
+ hiddenAt = Date.now();
459
+ return;
268
460
  }
461
+ logSessionEvent("visible", hiddenAt === undefined ? "hiddenFor=unknown" : `hiddenFor=${Date.now() - hiddenAt}ms`);
462
+ hiddenAt = undefined;
463
+ check("visible");
269
464
  };
465
+ const onFocus = () => check("focus");
466
+ const onOnline = () => check("online");
467
+ const onTick = () => check();
270
468
 
271
- check();
272
- const interval = window.setInterval(check, SESSION_REFRESH_CHECK_MS);
469
+ check("mount");
470
+ const interval = window.setInterval(onTick, SESSION_REFRESH_CHECK_MS);
273
471
  document.addEventListener("visibilitychange", onVisible);
274
- window.addEventListener("online", check);
472
+ window.addEventListener("focus", onFocus);
473
+ window.addEventListener("online", onOnline);
275
474
  return () => {
276
475
  cancelled = true;
277
476
  setApiSessionRecovery(undefined);
278
477
  window.clearInterval(interval);
279
478
  document.removeEventListener("visibilitychange", onVisible);
280
- window.removeEventListener("online", check);
479
+ window.removeEventListener("focus", onFocus);
480
+ window.removeEventListener("online", onOnline);
281
481
  };
282
482
  }, [userUid, authServerUrl, paused, explicitClient]);
283
483
  }
@@ -9,7 +9,7 @@
9
9
  * it); every write is `@RequiresTrustedRole()` on the backend.
10
10
  */
11
11
 
12
- import { ApiClient, ApiRequestError, apiUrl, withClient, withCsrfHeader } from "../util/api.js";
12
+ import { ApiClient, withClient, withClientRaw } from "../util/api.js";
13
13
 
14
14
  export interface Branding {
15
15
  companyName: string;
@@ -52,36 +52,24 @@ export function updateBranding(input: UpdateBrandingInput, client?: ApiClient):
52
52
  });
53
53
  }
54
54
 
55
- async function uploadBrandingAsset(path: string, file: File): Promise<Branding> {
56
- const res = await fetch(apiUrl(path), {
57
- method: "POST",
58
- credentials: "include",
59
- headers: withCsrfHeader({ "Content-Type": file.type || "application/octet-stream" }),
60
- body: file,
61
- });
62
- const contentType = res.headers.get("content-type") ?? "";
63
- const responseBody = contentType.includes("application/json") ? await res.json().catch(() => undefined) : undefined;
64
- if (!res.ok) {
65
- const message = (responseBody && (responseBody.message || responseBody.error)) || res.statusText || "Upload failed.";
66
- throw new ApiRequestError(message, res.status, responseBody?.code);
67
- }
68
- return responseBody as Branding;
55
+ function uploadBrandingAsset(path: string, file: File, client?: ApiClient): Promise<Branding> {
56
+ return withClientRaw(client, path, "POST", file, file.type || "application/octet-stream");
69
57
  }
70
58
 
71
59
  /** Uploads `file` as the logo, self-hosted via `BlobStore` — bypasses `apiFetch` (which always forces
72
60
  * `Content-Type: application/json`) the same way `mailApi.ts`'s `uploadAttachment()` does, since
73
61
  * `BaseBrandingRoute.uploadLogo()` reads the raw request body directly. */
74
- export function uploadBrandingLogo(file: File): Promise<Branding> {
75
- return uploadBrandingAsset("/system/branding/logo", file);
62
+ export function uploadBrandingLogo(file: File, client?: ApiClient): Promise<Branding> {
63
+ return uploadBrandingAsset("/system/branding/logo", file, client);
76
64
  }
77
65
 
78
66
  /** Uploads `file` as the compact nav-header icon, independently of `uploadBrandingLogo()`'s full logo. */
79
- export function uploadBrandingIcon(file: File): Promise<Branding> {
80
- return uploadBrandingAsset("/system/branding/icon", file);
67
+ export function uploadBrandingIcon(file: File, client?: ApiClient): Promise<Branding> {
68
+ return uploadBrandingAsset("/system/branding/icon", file, client);
81
69
  }
82
70
 
83
- export function uploadBrandingStylesheet(file: File): Promise<Branding> {
84
- return uploadBrandingAsset("/system/branding/stylesheet", file);
71
+ export function uploadBrandingStylesheet(file: File, client?: ApiClient): Promise<Branding> {
72
+ return uploadBrandingAsset("/system/branding/stylesheet", file, client);
85
73
  }
86
74
 
87
75
  export function deleteBrandingLogo(client?: ApiClient): Promise<void> {
@@ -151,6 +151,8 @@ export interface CalendarEvent {
151
151
 
152
152
  const LIST_PAGE_SIZE = 500;
153
153
  const LIST_CALENDAR_EVENTS_MAX_PAGES = 100;
154
+ /** Without an order, a page boundary can skip or repeat an event: the `uid` is a fixed one. */
155
+ const EVENT_SORT = JSON.stringify({ uid: "ASC" });
154
156
 
155
157
  /**
156
158
  * Lists every event in a folder (same "fetch the flat list, filter client-side" contract as
@@ -173,7 +175,7 @@ export async function listCalendarEvents(folderUid: string, client?: ApiClient):
173
175
  for (let page = 0; page < LIST_CALENDAR_EVENTS_MAX_PAGES; page++) {
174
176
  const batch = await withClient<CalendarEvent[]>(
175
177
  client,
176
- `/mail/calendar-events?${buildQuery({ limit: LIST_PAGE_SIZE, page }, { folderUid })}`,
178
+ `/mail/calendar-events?${buildQuery({ limit: LIST_PAGE_SIZE, page }, { folderUid, sort: EVENT_SORT })}`,
177
179
  );
178
180
  events.push(...batch);
179
181
  if (batch.length < LIST_PAGE_SIZE) {