@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
@@ -4,14 +4,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
- import { ApiRequestError } from "../../../lib/util/api.js";
8
7
  import { getTransportRule, updateTransportRule, } from "../../../lib/admin/transportRulesApi.js";
9
8
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
9
  import RuleBuilder from "../../shared/components/rules/RuleBuilder.js";
11
10
  import { checkRuleConditions, NO_CONDITIONS_BLOCKED_MESSAGE, NoConditionsConfirmModal, TRANSPORT_RULE_ACTION_TYPES, TRANSPORT_RULE_CONDITION_FIELDS, } from "./_transportRuleConfig.js";
12
- import Alert from "../../../lib/components/feedback/Alert.js";
11
+ import Alert from "../../shared/components/admin/ActionAlert.js";
13
12
  import Button from "../../../lib/components/buttons/Button.js";
14
13
  import FormField from "../../../lib/components/forms/FormField.js";
14
+ import { actionErrorMessage } from "../../shared/components/admin/elevation.js";
15
15
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
16
16
  export default function TransportRuleDetailPage(props) {
17
17
  return (_jsx(AdminShell, { ...props, active: "transportRules", children: _jsx(TransportRuleDetailContent, { uid: props.params.uid }) }));
@@ -43,7 +43,7 @@ function TransportRuleDetailContent({ uid }) {
43
43
  });
44
44
  }
45
45
  })
46
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this transport rule."))
46
+ .catch((err) => setError(actionErrorMessage(err, "Could not load this transport rule.")))
47
47
  .finally(() => setLoading(false));
48
48
  }, [uid]);
49
49
  // Only ever invoked from the form below, which itself only renders once `original`/`rule` are
@@ -84,7 +84,7 @@ function TransportRuleDetailContent({ uid }) {
84
84
  setSaved(true);
85
85
  }
86
86
  catch (err) {
87
- setError(err instanceof ApiRequestError ? err.message : "Could not save this transport rule.");
87
+ setError(actionErrorMessage(err, "Could not save this transport rule."));
88
88
  }
89
89
  finally {
90
90
  setSaving(false);
@@ -4,11 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
- import { ApiRequestError } from "../../../lib/util/api.js";
8
7
  import { listTransportRules } from "../../../lib/admin/transportRulesApi.js";
9
8
  import AdminShell from "../../shared/components/admin/layout/AdminShell.js";
10
- import Alert from "../../../lib/components/feedback/Alert.js";
9
+ import Alert from "../../shared/components/admin/ActionAlert.js";
11
10
  import Button from "../../../lib/components/buttons/Button.js";
11
+ import { actionErrorMessage } from "../../shared/components/admin/elevation.js";
12
12
  const PAGE_SIZE = 25;
13
13
  /** The server's largest page (`limit` is capped at 1000). */
14
14
  const FETCH_PAGE_SIZE = 1000;
@@ -39,7 +39,7 @@ function TransportRulesContent() {
39
39
  useEffect(() => {
40
40
  listAllTransportRules()
41
41
  .then(setRules)
42
- .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load transport rules."))
42
+ .catch((err) => setError(actionErrorMessage(err, "Could not load transport rules.")))
43
43
  .finally(() => setLoading(false));
44
44
  }, []);
45
45
  const hasNextPage = (page + 1) * PAGE_SIZE < rules.length;
@@ -5,14 +5,14 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
7
  import { useRouter } from "@rapidrest/react/client";
8
- import { ApiRequestError } from "../../../../lib/util/api.js";
9
8
  import { createTransportRule, } from "../../../../lib/admin/transportRulesApi.js";
10
9
  import AdminShell from "../../../shared/components/admin/layout/AdminShell.js";
11
10
  import RuleBuilder from "../../../shared/components/rules/RuleBuilder.js";
12
11
  import { checkRuleConditions, NO_CONDITIONS_BLOCKED_MESSAGE, NoConditionsConfirmModal, TRANSPORT_RULE_ACTION_TYPES, TRANSPORT_RULE_CONDITION_FIELDS, } from "../_transportRuleConfig.js";
13
- import Alert from "../../../../lib/components/feedback/Alert.js";
12
+ import Alert from "../../../shared/components/admin/ActionAlert.js";
14
13
  import Button from "../../../../lib/components/buttons/Button.js";
15
14
  import FormField from "../../../../lib/components/forms/FormField.js";
15
+ import { actionErrorMessage } from "../../../shared/components/admin/elevation.js";
16
16
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
17
17
  export default function NewTransportRulePage(props) {
18
18
  return (_jsx(AdminShell, { ...props, active: "transportRules", children: _jsx(NewTransportRuleForm, {}) }));
@@ -58,7 +58,7 @@ function NewTransportRuleForm() {
58
58
  void navigate(`/admin/transport-rules/${encodeURIComponent(created.uid)}`);
59
59
  }
60
60
  catch (err) {
61
- setError(err instanceof ApiRequestError ? err.message : "Could not create the transport rule.");
61
+ setError(actionErrorMessage(err, "Could not create the transport rule."));
62
62
  }
63
63
  finally {
64
64
  setSaving(false);
@@ -49,6 +49,8 @@ function MatterDetailContent({ uid }) {
49
49
  const [closing, setClosing] = useState(false);
50
50
  const [closeError, setCloseError] = useState(null);
51
51
  const [approveTarget, setApproveTarget] = useState(null);
52
+ // Denying cannot be undone - a denied request is never approved afterwards, only asked for again - so it is confirmed first.
53
+ const [denyTarget, setDenyTarget] = useState(null);
52
54
  const [showNewRequest, setShowNewRequest] = useState(false);
53
55
  const [newRequestMailboxUid, setNewRequestMailboxUid] = useState("");
54
56
  const [creatingRequest, setCreatingRequest] = useState(false);
@@ -151,7 +153,10 @@ function MatterDetailContent({ uid }) {
151
153
  setActingOn((prev) => ({ ...prev, [request.uid]: false }));
152
154
  }
153
155
  }
154
- async function handleDeny(request) {
156
+ // Only ever invoked from the deny-confirmation modal below, which only renders once `denyTarget` is set.
157
+ async function handleDeny() {
158
+ const request = denyTarget;
159
+ setDenyTarget(null);
155
160
  setActingOn((prev) => ({ ...prev, [request.uid]: true }));
156
161
  setActionErrors((prev) => ({ ...prev, [request.uid]: "" }));
157
162
  try {
@@ -264,6 +269,6 @@ function MatterDetailContent({ uid }) {
264
269
  : request.status === "pending"
265
270
  ? "bg-surface-alt text-text-muted"
266
271
  : "bg-success text-white",
267
- ].join(" "), children: request.status })] }), actionErrors[request.uid] && _jsx(Alert, { children: actionErrors[request.uid] }), _jsxs("div", { className: "flex gap-3 mt-3", children: [!matter.closedAt && request.status === "pending" && (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => handleDeny(request), children: "Deny" })] })), !matter.closedAt && (request.status === "approved" || request.status === "fulfilled") && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => handleGetMaterial(request), children: "Get material" }))] })] }, request.uid))) })), _jsx(LoadMoreButton, { list: requests, label: "Load more access requests" })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide", children: "Export this matter" }), !matter.closedAt && (_jsx(Button, { type: "button", className: "!w-auto", loading: creatingExport, disabled: creatingExport, onClick: handleCreateExport, children: "+ New export" }))] }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Exports messages (narrowed to this matter\u2019s own date range), plus full contacts, calendar events, tasks, and notes, from every custodian mailbox actually assigned to this matter\u2019s escrow scope, as one combined NDJSON file." }), exportError && _jsx(Alert, { children: exportError }), exportRequests.loadError && _jsx(Alert, { children: exportRequests.loadError }), matterExportRequests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: exportRequests.loading ? "Loading…" : "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: matterExportRequests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), !matter.closedAt && request.status === "ready" && (_jsx("a", { href: matterExportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-3", children: "Search this matter\u2019s custodians" }), matter.closedAt ? (_jsx("p", { className: "text-sm text-text-muted", children: "This matter is closed, so its custodians can no longer be searched or exported." })) : (_jsxs("form", { onSubmit: handleSearch, className: "flex gap-3 mb-4", children: [_jsx("input", { "aria-label": "Search this matter", className: INPUT_CLASS, value: searchText, onChange: (e) => setSearchText(e.target.value), placeholder: "Free text or an operator filter, e.g. from:alice@example.com" }), _jsx(Button, { type: "submit", className: "!w-auto shrink-0", loading: searching, disabled: searching || !searchText.trim(), children: "Search" })] })), searchError && _jsx(Alert, { children: searchError }), searchResults &&
268
- (Object.keys(searchResults).length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No custodian mailboxes could be searched." })) : (_jsx("div", { className: "flex flex-col gap-4", children: Object.entries(searchResults).map(([mailboxUid, page]) => (_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold mb-2", children: mailboxUid }), page.results.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: "No matches." })) : (_jsx("ul", { className: "flex flex-col gap-1", children: page.results.map((result) => (_jsxs("li", { className: "text-sm py-1 px-2 bg-surface-alt rounded-sm", children: [result.entityType, " \u00B7 ", result.entityUid, result.snippet && _jsxs("span", { className: "text-text-muted", children: [" \u2014 ", result.snippet] })] }, `${result.entityType}-${result.entityUid}`))) }))] }, mailboxUid))) })))] }), _jsxs(Modal, { open: confirmingClose, onClose: closeCloseModal, title: "Close matter", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Close ", _jsx("strong", { children: matter.name }), "? Once closed, no new access requests, exports, searches, or key material reads are allowed under it. This cannot be reopened here."] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Custodians" }), _jsx("dd", { className: "break-all", children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) })] }), closeError && _jsx(Alert, { children: closeError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: closing, onClick: closeCloseModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: closing, disabled: closing, onClick: handleClose, children: "Close matter" })] })] }), _jsxs(Modal, { open: approveTarget !== null, onClose: () => setApproveTarget(null), title: "Approve access request", children: [_jsx("p", { className: "text-sm mb-3", children: "Approving lets holders of this matter\u2019s escrow scope read the key material for this mailbox once enough holders have approved." }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Matter" }), _jsx("dd", { children: matter.name }), _jsx("dt", { className: "text-text-muted", children: "Mailbox" }), _jsx("dd", { className: "break-all", children: approveTarget?.mailboxUid }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) }), _jsx("dt", { className: "text-text-muted", children: "Approvals" }), _jsxs("dd", { children: [approveTarget?.approvals.length, " of ", approveTarget?.requiredHoldersAtCreation, " so far, including requester ", approveTarget?.requestedByUserUid] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setApproveTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleApprove(), children: "Approve request" })] })] }), _jsx(Modal, { open: showNewRequest, onClose: () => setShowNewRequest(false), title: "New access request", children: _jsxs("form", { onSubmit: handleCreateRequest, className: "flex flex-col gap-4", children: [newRequestError && _jsx(Alert, { children: newRequestError }), _jsx(FormField, { label: "Mailbox uid", htmlFor: "newRequestMailboxUid", children: _jsx("input", { id: "newRequestMailboxUid", type: "text", className: INPUT_CLASS, value: newRequestMailboxUid, onChange: (e) => setNewRequestMailboxUid(e.target.value) }) }), _jsx("p", { className: "text-xs text-text-muted", children: "Must be one of this matter's own custodian mailboxes, currently assigned to this matter's escrow scope. Your own creation counts as the first approval." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creatingRequest, onClick: () => setShowNewRequest(false), children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: creatingRequest, disabled: creatingRequest, children: "Create request" })] })] }) }), _jsx(Modal, { open: materialFor !== null, onClose: closeMaterialModal, title: "Escrow key material", children: loadingMaterial ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : materialError ? (_jsx(Alert, { children: materialError })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-text-muted mb-3", children: ["Still-encrypted master key wraps for ", _jsx("strong", { children: materialFor?.mailboxUid }), " \u2014 this server cannot decrypt these either. Copy the JSON below into your own offline tooling."] }), _jsx("pre", { className: "text-xs font-mono bg-surface-alt border border-border rounded-sm p-3 overflow-x-auto whitespace-pre-wrap break-all", children: materialText })] })) })] }));
272
+ ].join(" "), children: request.status })] }), actionErrors[request.uid] && _jsx(Alert, { children: actionErrors[request.uid] }), _jsxs("div", { className: "flex gap-3 mt-3", children: [!matter.closedAt && request.status === "pending" && (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => setApproveTarget(request), children: "Approve" }), _jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => setDenyTarget(request), children: "Deny" })] })), !matter.closedAt && (request.status === "approved" || request.status === "fulfilled") && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => handleGetMaterial(request), children: "Get material" }))] })] }, request.uid))) })), _jsx(LoadMoreButton, { list: requests, label: "Load more access requests" })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsxs("div", { className: "flex items-center justify-between mb-3", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide", children: "Export this matter" }), !matter.closedAt && (_jsx(Button, { type: "button", className: "!w-auto", loading: creatingExport, disabled: creatingExport, onClick: handleCreateExport, children: "+ New export" }))] }), _jsx("p", { className: "text-xs text-text-muted mb-3", children: "Exports messages (narrowed to this matter\u2019s own date range), plus full contacts, calendar events, tasks, and notes, from every custodian mailbox actually assigned to this matter\u2019s escrow scope, as one combined NDJSON file." }), exportError && _jsx(Alert, { children: exportError }), exportRequests.loadError && _jsx(Alert, { children: exportRequests.loadError }), matterExportRequests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: exportRequests.loading ? "Loading…" : "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: matterExportRequests.map((request) => (_jsxs("li", { className: "flex items-center justify-between gap-3 text-sm py-1.5 px-3 bg-surface-alt rounded-sm", children: [_jsxs("span", { children: [new Date(request.dateCreated).toLocaleString(), request.status === "failed" && request.errorMessage && (_jsxs("span", { className: "text-danger", children: [" \u2014 ", request.errorMessage] }))] }), _jsxs("span", { className: "flex items-center gap-3", children: [_jsx("span", { className: `text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill ${statusBadgeClass(request.status)}`, children: statusLabel(request.status) }), !matter.closedAt && request.status === "ready" && (_jsx("a", { href: matterExportRequestDownloadUrl(request.uid), className: "text-primary-dark hover:underline font-medium", children: "Download" }))] })] }, request.uid))) }))] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-3", children: "Search this matter\u2019s custodians" }), matter.closedAt ? (_jsx("p", { className: "text-sm text-text-muted", children: "This matter is closed, so its custodians can no longer be searched or exported." })) : (_jsxs("form", { onSubmit: handleSearch, className: "flex gap-3 mb-4", children: [_jsx("input", { "aria-label": "Search this matter", className: INPUT_CLASS, value: searchText, onChange: (e) => setSearchText(e.target.value), placeholder: "Free text or an operator filter, e.g. from:alice@example.com" }), _jsx(Button, { type: "submit", className: "!w-auto shrink-0", loading: searching, disabled: searching || !searchText.trim(), children: "Search" })] })), searchError && _jsx(Alert, { children: searchError }), searchResults &&
273
+ (Object.keys(searchResults).length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No custodian mailboxes could be searched." })) : (_jsx("div", { className: "flex flex-col gap-4", children: Object.entries(searchResults).map(([mailboxUid, page]) => (_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-semibold mb-2", children: mailboxUid }), page.results.length === 0 ? (_jsx("p", { className: "text-xs text-text-muted", children: "No matches." })) : (_jsx("ul", { className: "flex flex-col gap-1", children: page.results.map((result) => (_jsxs("li", { className: "text-sm py-1 px-2 bg-surface-alt rounded-sm", children: [result.entityType, " \u00B7 ", result.entityUid, result.snippet && _jsxs("span", { className: "text-text-muted", children: [" \u2014 ", result.snippet] })] }, `${result.entityType}-${result.entityUid}`))) }))] }, mailboxUid))) })))] }), _jsxs(Modal, { open: confirmingClose, onClose: closeCloseModal, title: "Close matter", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Close ", _jsx("strong", { children: matter.name }), "? Once closed, no new access requests, exports, searches, or key material reads are allowed under it. This cannot be reopened here."] }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Custodians" }), _jsx("dd", { className: "break-all", children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) })] }), closeError && _jsx(Alert, { children: closeError }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: closing, onClick: closeCloseModal, children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", loading: closing, disabled: closing, onClick: handleClose, children: "Close matter" })] })] }), _jsxs(Modal, { open: approveTarget !== null, onClose: () => setApproveTarget(null), title: "Approve access request", children: [_jsx("p", { className: "text-sm mb-3", children: "Approving lets holders of this matter\u2019s escrow scope read the key material for this mailbox once enough holders have approved." }), _jsxs("dl", { className: "grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 text-sm mb-4", children: [_jsx("dt", { className: "text-text-muted", children: "Matter" }), _jsx("dd", { children: matter.name }), _jsx("dt", { className: "text-text-muted", children: "Mailbox" }), _jsx("dd", { className: "break-all", children: approveTarget?.mailboxUid }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) }), _jsx("dt", { className: "text-text-muted", children: "Approvals" }), _jsxs("dd", { children: [approveTarget?.approvals.length, " of ", approveTarget?.requiredHoldersAtCreation, " so far, including requester ", approveTarget?.requestedByUserUid] })] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setApproveTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto", onClick: () => void handleApprove(), children: "Approve request" })] })] }), _jsxs(Modal, { open: denyTarget !== null, onClose: () => setDenyTarget(null), title: "Deny access request", children: [_jsxs("p", { className: "text-sm mb-3", children: ["Denying refuses this request for good: it can\u2019t be approved afterwards, and holders can\u2019t read the key material for mailbox ", _jsx("span", { className: "break-all font-semibold", children: denyTarget?.mailboxUid }), " through it. A new request has to be made to ask again."] }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => setDenyTarget(null), children: "Cancel" }), _jsx(Button, { type: "button", className: "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger", onClick: () => void handleDeny(), children: "Deny request" })] })] }), _jsx(Modal, { open: showNewRequest, onClose: () => setShowNewRequest(false), title: "New access request", children: _jsxs("form", { onSubmit: handleCreateRequest, className: "flex flex-col gap-4", children: [newRequestError && _jsx(Alert, { children: newRequestError }), _jsx(FormField, { label: "Mailbox uid", htmlFor: "newRequestMailboxUid", children: _jsx("input", { id: "newRequestMailboxUid", type: "text", className: INPUT_CLASS, value: newRequestMailboxUid, onChange: (e) => setNewRequestMailboxUid(e.target.value) }) }), _jsx("p", { className: "text-xs text-text-muted", children: "Must be one of this matter's own custodian mailboxes, currently assigned to this matter's escrow scope. Your own creation counts as the first approval." }), _jsxs("div", { className: "flex gap-3 justify-end", children: [_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", disabled: creatingRequest, onClick: () => setShowNewRequest(false), children: "Cancel" }), _jsx(Button, { type: "submit", className: "!w-auto", loading: creatingRequest, disabled: creatingRequest, children: "Create request" })] })] }) }), _jsx(Modal, { open: materialFor !== null, onClose: closeMaterialModal, title: "Escrow key material", children: loadingMaterial ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : materialError ? (_jsx(Alert, { children: materialError })) : (_jsxs(_Fragment, { children: [_jsxs("p", { className: "text-sm text-text-muted mb-3", children: ["Still-encrypted master key wraps for ", _jsx("strong", { children: materialFor?.mailboxUid }), " \u2014 this server cannot decrypt these either. Copy the JSON below into your own offline tooling."] }), _jsx("pre", { className: "text-xs font-mono bg-surface-alt border border-border rounded-sm p-3 overflow-x-auto whitespace-pre-wrap break-all", children: materialText })] })) })] }));
269
274
  }
@@ -50,6 +50,10 @@ function NewMatterForm() {
50
50
  setError("At least one custodian mailbox is required.");
51
51
  return;
52
52
  }
53
+ if (Number.isNaN(new Date(dateRangeStart).getTime()) || Number.isNaN(new Date(dateRangeEnd).getTime())) {
54
+ setError("The date range start and end are both required.");
55
+ return;
56
+ }
53
57
  if (new Date(dateRangeStart).getTime() >= new Date(dateRangeEnd).getTime()) {
54
58
  setError("The date range start must be before its end.");
55
59
  return;
@@ -1,11 +1,15 @@
1
- /** Pure helper behind the calendar page's live updates: which push events say "an event changed, and the payload is only a busy block". */
1
+ /** Pure helper behind the calendar page's live updates: which push events say "an event changed". */
2
2
  import type { PushEvent } from "../../../lib/mail/pushClient.js";
3
3
  /**
4
- * The uid of the event a push event announces as a redacted busy block, or `undefined` for anything else the shared push connection delivers.
4
+ * The event a push event says was created, changed or deleted - its uid, and whether it is gone - or `undefined` for anything else the shared push connection
5
+ * delivers.
5
6
  *
6
- * When somebody creates or changes a private or confidential event, restapi publishes it on the calendar's channel once, for every subscriber - and that one
7
- * payload has to be safe for a read-only reader of a shared calendar, so it is the busy block (`redacted: true`, title "Busy", no details). It must not be
8
- * stored as the event by the calendar's owner (or anybody else who may see it in full): they fetch the event again by its uid, which answers with as
9
- * much as they may see.
7
+ * The payload is never stored as the event: when somebody creates or changes a private or confidential event, restapi publishes it on the calendar's channel
8
+ * once, for every subscriber, and that one payload has to be safe for a read-only reader of a shared calendar, so it is the busy block (`redacted: true`,
9
+ * title "Busy", no details). Whoever may see the event in full fetches it again by its uid, which answers with as much as they may see - so every created or
10
+ * changed event is handled that way, an ordinary one (an invitation arriving, an edit on another device) included.
10
11
  */
11
- export declare function redactedEventUidOf(event: PushEvent): string | undefined;
12
+ export declare function changedEventOf(event: PushEvent): {
13
+ uid: string;
14
+ deleted: boolean;
15
+ } | undefined;
@@ -2,22 +2,23 @@
2
2
  // Copyright (C) 2026 Jean-Philippe Steinmetz
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
- /** Pure helper behind the calendar page's live updates: which push events say "an event changed, and the payload is only a busy block". */
5
+ /** Pure helper behind the calendar page's live updates: which push events say "an event changed". */
6
6
  /** The published model name of a calendar event on either database (`CalendarEventMongo`, `CalendarEventSQL`) - and not the reminder's `"CalendarEvent"`,
7
7
  * which has its own action. */
8
8
  const EVENT_MODEL = /^CalendarEvent(Mongo|SQL)$/;
9
9
  /**
10
- * The uid of the event a push event announces as a redacted busy block, or `undefined` for anything else the shared push connection delivers.
10
+ * The event a push event says was created, changed or deleted - its uid, and whether it is gone - or `undefined` for anything else the shared push connection
11
+ * delivers.
11
12
  *
12
- * When somebody creates or changes a private or confidential event, restapi publishes it on the calendar's channel once, for every subscriber - and that one
13
- * payload has to be safe for a read-only reader of a shared calendar, so it is the busy block (`redacted: true`, title "Busy", no details). It must not be
14
- * stored as the event by the calendar's owner (or anybody else who may see it in full): they fetch the event again by its uid, which answers with as
15
- * much as they may see.
13
+ * The payload is never stored as the event: when somebody creates or changes a private or confidential event, restapi publishes it on the calendar's channel
14
+ * once, for every subscriber, and that one payload has to be safe for a read-only reader of a shared calendar, so it is the busy block (`redacted: true`,
15
+ * title "Busy", no details). Whoever may see the event in full fetches it again by its uid, which answers with as much as they may see - so every created or
16
+ * changed event is handled that way, an ordinary one (an invitation arriving, an edit on another device) included.
16
17
  */
17
- export function redactedEventUidOf(event) {
18
- if (!EVENT_MODEL.test(event.type) || (event.action !== "create" && event.action !== "update")) {
18
+ export function changedEventOf(event) {
19
+ if (!EVENT_MODEL.test(event.type) || (event.action !== "create" && event.action !== "update" && event.action !== "delete")) {
19
20
  return undefined;
20
21
  }
21
22
  const data = event.data;
22
- return data?.redacted === true && typeof data.uid === "string" ? data.uid : undefined;
23
+ return typeof data?.uid === "string" ? { uid: data.uid, deleted: event.action === "delete" } : undefined;
23
24
  }
@@ -35,6 +35,8 @@ export function useCalendarReminders({ userUid, enabled }) {
35
35
  kind: "calendar",
36
36
  title: notice.title,
37
37
  message: reminderMessage(notice.startDate),
38
+ // Anyone who can invite you can put any address in an event's location: the button says where it goes.
39
+ hint: joinUrl ? `Join Meeting opens ${new URL(joinUrl).host}` : undefined,
38
40
  href: CALENDAR_HREF,
39
41
  sticky: true,
40
42
  actions: [
@@ -0,0 +1,13 @@
1
+ import React, { ReactNode } from "react";
2
+ /**
3
+ * What `AdminShell` offers the page inside it: sends the browser to auth-server to confirm the user's identity again, then back to this page. `undefined`
4
+ * outside the shell, and in one with no auth-server to send the browser to.
5
+ */
6
+ export declare const ReconfirmIdentityContext: React.Context<(() => void) | undefined>;
7
+ /**
8
+ * `Alert` for the failure of an administrator action. When the failure is the server's refusal for want of a recently confirmed identity (see
9
+ * `actionErrorMessage()`), it also offers to confirm it again right there rather than leaving the user to work out that reloading the page does it.
10
+ */
11
+ export default function ActionAlert({ children }: {
12
+ children: ReactNode;
13
+ }): React.JSX.Element;
@@ -0,0 +1,24 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { createContext, useContext } from "react";
7
+ import Alert from "../../../../lib/components/feedback/Alert.js";
8
+ import { isElevationMessage } from "./elevation.js";
9
+ /**
10
+ * What `AdminShell` offers the page inside it: sends the browser to auth-server to confirm the user's identity again, then back to this page. `undefined`
11
+ * outside the shell, and in one with no auth-server to send the browser to.
12
+ */
13
+ export const ReconfirmIdentityContext = createContext(undefined);
14
+ /**
15
+ * `Alert` for the failure of an administrator action. When the failure is the server's refusal for want of a recently confirmed identity (see
16
+ * `actionErrorMessage()`), it also offers to confirm it again right there rather than leaving the user to work out that reloading the page does it.
17
+ */
18
+ export default function ActionAlert({ children }) {
19
+ const reconfirm = useContext(ReconfirmIdentityContext);
20
+ if (!reconfirm || typeof children !== "string" || !isElevationMessage(children)) {
21
+ return _jsx(Alert, { children: children });
22
+ }
23
+ return (_jsxs(Alert, { children: [_jsx("span", { className: "flex-1", children: children }), _jsx("button", { type: "button", className: "shrink-0 font-semibold underline", onClick: reconfirm, children: "Confirm identity again" })] }));
24
+ }
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import Alert from "../../../../../lib/components/feedback/Alert.js";
2
+ import Alert from "../ActionAlert.js";
3
3
  import ComponentsTable from "./ComponentsTable.js";
4
4
  import InstalledPlugins from "./InstalledPlugins.js";
5
5
  import PackagesTable from "./PackagesTable.js";
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { HiOutlineExclamationTriangle } from "react-icons/hi2";
3
3
  import { getPluginStatus, listPlugins } from "../../../../../lib/admin/pluginsApi.js";
4
- import Alert from "../../../../../lib/components/feedback/Alert.js";
4
+ import Alert from "../ActionAlert.js";
5
5
  import Badge from "./Badge.js";
6
6
  import { NO_VALUE } from "./format.js";
7
7
  /** Reads the plugins, and their load status. A status that cannot be read leaves the list without it, not without the list. */
@@ -5,7 +5,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useMemo, useState } from "react";
7
7
  import { HiOutlineArrowDownTray, HiOutlineArrowPath, HiOutlineCheck, HiOutlineExclamationTriangle, HiOutlinePause, HiOutlinePlay, HiOutlineSignal, HiOutlineSignalSlash, HiOutlineStop, HiOutlineTrash, } from "react-icons/hi2";
8
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
9
9
  import Button from "../../../../../lib/components/buttons/Button.js";
10
10
  import Badge from "./Badge.js";
11
11
  import { timestampedFilename } from "./download.js";
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import Alert from "../../../../../lib/components/feedback/Alert.js";
2
+ import Alert from "../ActionAlert.js";
3
3
  import Badge from "./Badge.js";
4
4
  import KubernetesNotice from "./KubernetesNotice.js";
5
5
  import { formatDateTime, NO_VALUE } from "./format.js";
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { HiOutlinePause, HiOutlinePlay } from "react-icons/hi2";
3
- import Alert from "../../../../../lib/components/feedback/Alert.js";
3
+ import Alert from "../ActionAlert.js";
4
4
  import Button from "../../../../../lib/components/buttons/Button.js";
5
5
  import { formatDateTime } from "./format.js";
6
6
  import HostCard from "./HostCard.js";
@@ -4,10 +4,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { updateDistributionList } from "../../../../../lib/admin/distributionListsApi.js";
9
- import Alert from "../../../../../lib/components/feedback/Alert.js";
8
+ import Alert from "../ActionAlert.js";
10
9
  import Button from "../../../../../lib/components/buttons/Button.js";
10
+ import { actionErrorMessage } from "../elevation.js";
11
11
  /**
12
12
  * Lets an admin view and edit a distribution list's members — same load/inline-add-row/per-row-remove
13
13
  * interaction shape as `ShareAccessCard`, but against a plain `memberAddresses: string[]` field on the
@@ -36,7 +36,7 @@ export default function MemberListCard({ list, onUpdate }) {
36
36
  onUpdate(updated);
37
37
  }
38
38
  catch (err) {
39
- setError(err instanceof ApiRequestError ? err.message : "Could not add this member.");
39
+ setError(actionErrorMessage(err, "Could not add this member."));
40
40
  }
41
41
  finally {
42
42
  setSaving(false);
@@ -54,7 +54,7 @@ export default function MemberListCard({ list, onUpdate }) {
54
54
  onUpdate(updated);
55
55
  }
56
56
  catch (err) {
57
- setError(err instanceof ApiRequestError ? err.message : "Could not remove this member.");
57
+ setError(actionErrorMessage(err, "Could not remove this member."));
58
58
  }
59
59
  finally {
60
60
  setSaving(false);
@@ -11,6 +11,15 @@ export declare const ELEVATION_ATTEMPT_KEY = "rapidmx-admin-elevation-attempt";
11
11
  export declare const ELEVATION_RETRY_WINDOW_MS: number;
12
12
  /** Whether `err` is a 403 from an elevation-gated endpoint for a caller whose token isn't elevated. */
13
13
  export declare function isElevationRequired(err: unknown): boolean;
14
+ /** What an administrator action says when the server refuses it because the token isn't elevated (any longer). The console confirms the user's identity
15
+ * when a page opens (see `AdminShell`), so reloading is what sends them round auth-server's confirmation again. */
16
+ export declare const ELEVATION_ACTION_MESSAGE = "This needs you to have recently confirmed your identity. Reload this page to confirm it again, then try once more.";
17
+ /** Whether `text` is one of the messages that say an action needs the user's identity confirmed again (this one, a plugin's or the diagnostics'). */
18
+ export declare function isElevationMessage(text: string): boolean;
19
+ /** The text for an administrator action that failed: the elevation message when it only needed the user to confirm their identity (an action the
20
+ * server newly gates behind an elevated token - truncating or deleting a mailbox, a retention policy, an export of another mailbox, approving an
21
+ * erasure), else the server's own message, else `fallback`. */
22
+ export declare function actionErrorMessage(err: unknown, fallback: string): string;
14
23
  /** auth-server's elevation page: it sends the browser back to `returnTo` once the user has confirmed their
15
24
  * identity, or to its own account page if they cancel. */
16
25
  export declare function elevationUrl(authServerUrl: string, returnTo: string): string;
@@ -18,6 +18,22 @@ export const ELEVATION_RETRY_WINDOW_MS = 2 * 60 * 1000;
18
18
  export function isElevationRequired(err) {
19
19
  return err instanceof ApiRequestError && err.status === 403 && err.code === ELEVATION_REQUIRED_CODE;
20
20
  }
21
+ /** What an administrator action says when the server refuses it because the token isn't elevated (any longer). The console confirms the user's identity
22
+ * when a page opens (see `AdminShell`), so reloading is what sends them round auth-server's confirmation again. */
23
+ export const ELEVATION_ACTION_MESSAGE = "This needs you to have recently confirmed your identity. Reload this page to confirm it again, then try once more.";
24
+ /** Whether `text` is one of the messages that say an action needs the user's identity confirmed again (this one, a plugin's or the diagnostics'). */
25
+ export function isElevationMessage(text) {
26
+ return text.includes("recently confirmed your identity");
27
+ }
28
+ /** The text for an administrator action that failed: the elevation message when it only needed the user to confirm their identity (an action the
29
+ * server newly gates behind an elevated token - truncating or deleting a mailbox, a retention policy, an export of another mailbox, approving an
30
+ * erasure), else the server's own message, else `fallback`. */
31
+ export function actionErrorMessage(err, fallback) {
32
+ if (isElevationRequired(err)) {
33
+ return ELEVATION_ACTION_MESSAGE;
34
+ }
35
+ return err instanceof ApiRequestError ? err.message : fallback;
36
+ }
21
37
  /** auth-server's elevation page: it sends the browser back to `returnTo` once the user has confirmed their
22
38
  * identity, or to its own account page if they cancel. */
23
39
  export function elevationUrl(authServerUrl, returnTo) {
@@ -25,6 +25,8 @@ export interface EscrowScopeKeyAndHoldersFieldsProps {
25
25
  /** Builds an all-zeroes starting point for the "new" page — `notBefore`/`notAfter` default to "now" so an
26
26
  * admin pasting in a cert's own dates only has to touch what actually differs. */
27
27
  export declare function emptyEscrowScopeKeyAndHoldersValue(): EscrowScopeKeyAndHoldersValue;
28
+ /** The message for a key validity date left empty (or not a date), which would otherwise be sent as nothing; `null` when both dates are there. */
29
+ export declare function keyValidityError(value: Pick<EscrowScopeKeyAndHoldersValue, "notBefore" | "notAfter">): string | null;
28
30
  /** The message shown when an administrator tries to make themselves a holder: whoever configures a scope must not
29
31
  * also be able to approve access under it (separation of duties - the server refuses it too). Holders who were
30
32
  * already on the scope (`existingHolderUserUids`) aren't treated as newly added. */
@@ -1,4 +1,10 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ ///////////////////////////////////////////////////////////////////////////////
3
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
4
+ // SPDX-License-Identifier: MPL-2.0
5
+ ///////////////////////////////////////////////////////////////////////////////
6
+ import { useEffect, useState } from "react";
7
+ import { escrowPublicKeyFingerprint, normalizeFingerprint } from "../../../../../lib/crypto/escrowKeys.js";
2
8
  import { toDatetimeLocal } from "../../../../../lib/util/dateInput.js";
3
9
  import { resolveEscrowScopeHolder } from "../../../../../lib/admin/escrowScopesApi.js";
4
10
  import FormField from "../../../../../lib/components/forms/FormField.js";
@@ -19,6 +25,12 @@ export function emptyEscrowScopeKeyAndHoldersValue() {
19
25
  notifySubjectOnAccess: false,
20
26
  };
21
27
  }
28
+ /** The message for a key validity date left empty (or not a date), which would otherwise be sent as nothing; `null` when both dates are there. */
29
+ export function keyValidityError(value) {
30
+ return Number.isNaN(new Date(value.notBefore).getTime()) || Number.isNaN(new Date(value.notAfter).getTime())
31
+ ? "The key's validity dates, not before and not after, are both required."
32
+ : null;
33
+ }
22
34
  /** The message shown when an administrator tries to make themselves a holder: whoever configures a scope must not
23
35
  * also be able to approve access under it (separation of duties - the server refuses it too). Holders who were
24
36
  * already on the scope (`existingHolderUserUids`) aren't treated as newly added. */
@@ -32,5 +44,33 @@ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabl
32
44
  function set(key, next) {
33
45
  onChange({ ...value, [key]: next });
34
46
  }
35
- return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Public key" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "The fields of an already-issued certificate \u2014 this server never generates or holds the corresponding private key." }), _jsx(FormField, { label: "Public key (base64)", htmlFor: "publicKey", children: _jsx("textarea", { id: "publicKey", className: `${INPUT_CLASS} font-mono text-xs`, rows: 4, value: value.publicKey, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("publicKey", e.target.value) }) }), _jsx(FormField, { label: "Key type", htmlFor: "keyType", children: _jsx("input", { id: "keyType", type: "text", className: INPUT_CLASS, value: value.keyType, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("keyType", e.target.value), placeholder: "x509" }) }), _jsx(FormField, { label: "Fingerprint (hex SHA-256)", htmlFor: "fingerprint", children: _jsx("input", { id: "fingerprint", type: "text", className: `${INPUT_CLASS} font-mono`, value: value.fingerprint, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("fingerprint", e.target.value) }) }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-4", children: [_jsx(FormField, { label: "Not before", htmlFor: "notBefore", children: _jsx("input", { id: "notBefore", type: "datetime-local", className: INPUT_CLASS, value: value.notBefore, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notBefore", e.target.value) }) }), _jsx(FormField, { label: "Not after", htmlFor: "notAfter", children: _jsx("input", { id: "notAfter", type: "datetime-local", className: INPUT_CLASS, value: value.notAfter, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notAfter", e.target.value) }) })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Holders & dual control" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Every uid below is granted the eDiscovery/compliance holder role for this scope \u2014 a deliberately separate role from server administration. At least one holder is required." }), _jsx(PrincipalListField, { label: "Holder user uids", values: value.holderUserUids, onChange: (holderUserUids) => set("holderUserUids", holderUserUids), resolve: resolveEscrowScopeHolder, emptyMessage: "No holders added yet.", disabled: disabled }), _jsx(FormField, { label: "Required holders (M-of-N dual control)", htmlFor: "requiredHolders", children: _jsx("input", { id: "requiredHolders", type: "number", className: INPUT_CLASS, value: value.requiredHolders, disabled: disabled, onChange: (e) => set("requiredHolders", Number(e.target.value) || 1) }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.notifySubjectOnAccess, disabled: disabled, onChange: (e) => set("notifySubjectOnAccess", e.target.checked) }), "Notify subject on access"] })] })] }));
47
+ // What is typed in the required-holders box, which may be empty meanwhile (to be typed over): the value is then 0, which the pages refuse on submit
48
+ // ("between 1 and the number of holders"), rather than snapping back to 1 under the cursor.
49
+ const [requiredText, setRequiredText] = useState(String(value.requiredHolders));
50
+ useEffect(() => {
51
+ setRequiredText((text) => (Number(text) === value.requiredHolders ? text : String(value.requiredHolders)));
52
+ }, [value.requiredHolders]);
53
+ // What the pasted key's own SHA-256 fingerprint is (`null`: it is no certificate), so the typed one - which the holders compare out of band -
54
+ // can be checked against it. `undefined` until worked out.
55
+ const [actualFingerprint, setActualFingerprint] = useState(undefined);
56
+ const checking = !keyReadOnly && value.publicKey.trim() !== "";
57
+ useEffect(() => {
58
+ if (!checking) {
59
+ return;
60
+ }
61
+ let cancelled = false;
62
+ setActualFingerprint(undefined);
63
+ void escrowPublicKeyFingerprint(value.publicKey).then((fingerprint) => {
64
+ if (!cancelled) {
65
+ setActualFingerprint(fingerprint);
66
+ }
67
+ });
68
+ return () => {
69
+ cancelled = true;
70
+ };
71
+ }, [checking, value.publicKey]);
72
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Public key" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "The fields of an already-issued certificate \u2014 this server never generates or holds the corresponding private key." }), _jsx(FormField, { label: "Public key (base64)", htmlFor: "publicKey", children: _jsx("textarea", { id: "publicKey", className: `${INPUT_CLASS} font-mono text-xs`, rows: 4, value: value.publicKey, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("publicKey", e.target.value) }) }), _jsx(FormField, { label: "Key type", htmlFor: "keyType", children: _jsx("input", { id: "keyType", type: "text", className: INPUT_CLASS, value: value.keyType, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("keyType", e.target.value), placeholder: "x509" }) }), _jsxs(FormField, { label: "Fingerprint (hex SHA-256)", htmlFor: "fingerprint", children: [_jsx("input", { id: "fingerprint", type: "text", className: `${INPUT_CLASS} font-mono`, value: value.fingerprint, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("fingerprint", e.target.value) }), checking && actualFingerprint === null && (_jsx("p", { role: "alert", className: "text-xs text-danger mt-1.5", children: "This is not a base64-encoded certificate, so no fingerprint can be checked against it." })), checking && actualFingerprint && normalizeFingerprint(value.fingerprint) !== actualFingerprint && (_jsxs("p", { role: "alert", className: "text-xs text-danger mt-1.5", children: ["This fingerprint is not the one of the public key above, whose SHA-256 fingerprint is", " ", _jsx("code", { className: "font-mono break-all", children: actualFingerprint }), ".", " ", _jsx("button", { type: "button", className: "underline font-semibold", disabled: disabled, onClick: () => set("fingerprint", actualFingerprint), children: "Use it" })] })), checking && actualFingerprint && normalizeFingerprint(value.fingerprint) === actualFingerprint && (_jsx("p", { className: "text-xs text-text-muted mt-1.5", children: "This is the fingerprint of the public key above." }))] }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-4", children: [_jsx(FormField, { label: "Not before", htmlFor: "notBefore", children: _jsx("input", { id: "notBefore", type: "datetime-local", className: INPUT_CLASS, value: value.notBefore, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notBefore", e.target.value) }) }), _jsx(FormField, { label: "Not after", htmlFor: "notAfter", children: _jsx("input", { id: "notAfter", type: "datetime-local", className: INPUT_CLASS, value: value.notAfter, disabled: disabled, readOnly: keyReadOnly, onChange: (e) => set("notAfter", e.target.value) }) })] })] }), _jsxs("div", { className: "bg-surface border border-border rounded-md p-6", children: [_jsx("h2", { className: "text-base font-bold uppercase tracking-wide mb-1", children: "Holders & dual control" }), _jsx("p", { className: "text-sm text-text-muted mb-4", children: "Every uid below is granted the eDiscovery/compliance holder role for this scope \u2014 a deliberately separate role from server administration. At least one holder is required." }), _jsx(PrincipalListField, { label: "Holder user uids", values: value.holderUserUids, onChange: (holderUserUids) => set("holderUserUids", holderUserUids), resolve: resolveEscrowScopeHolder, emptyMessage: "No holders added yet.", disabled: disabled }), _jsx(FormField, { label: "Required holders (M-of-N dual control)", htmlFor: "requiredHolders", children: _jsx("input", { id: "requiredHolders", type: "number", className: INPUT_CLASS, value: requiredText, disabled: disabled, onChange: (e) => {
73
+ setRequiredText(e.target.value);
74
+ set("requiredHolders", Number(e.target.value) || 0);
75
+ } }) }), _jsxs("label", { className: "flex items-center gap-2 text-sm", children: [_jsx("input", { type: "checkbox", checked: value.notifySubjectOnAccess, disabled: disabled, onChange: (e) => set("notifySubjectOnAccess", e.target.checked) }), "Notify subject on access"] })] })] }));
36
76
  }
@@ -21,6 +21,7 @@ import AppearanceProvider from "../../../appearance/AppearanceProvider.js";
21
21
  import UserMenu from "../../layout/UserMenu.js";
22
22
  import { clearElevationAttempt, elevationAttemptedRecently, elevationUrl, isElevationRequired, recordElevationAttempt, } from "../elevation.js";
23
23
  import { signOutOfConsole } from "../signOut.js";
24
+ import { ReconfirmIdentityContext } from "../ActionAlert.js";
24
25
  import { pluginIcon } from "../../../plugins/pluginIcons.js";
25
26
  import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
26
27
  /** Sections shown in the persistent icon rail / mobile tab bar — every admin area reachable from
@@ -224,7 +225,7 @@ export default function AdminShell({ active, userUid, authServerUrl, pluginNav,
224
225
  ].join(" "), children: [railExpanded ? (_jsx(HiOutlineChevronDoubleLeft, { size: 20, "aria-hidden": "true", className: "shrink-0" })) : (_jsx(HiOutlineChevronDoubleRight, { size: 20, "aria-hidden": "true", className: "shrink-0" })), railExpanded && _jsx("span", { children: "Collapse" })] })] }), _jsx(Drawer, { open: menuOpen, onClose: () => setMenuOpen(false), title: "Admin", fullScreen: true, children: _jsx("nav", { "aria-label": "Admin menu", className: "flex flex-col gap-1 -mx-2", children: navItems.map(({ id, href, label, icon: Icon }) => (_jsxs("a", { href: href, "aria-current": id === active ? "page" : undefined, onClick: () => setMenuOpen(false), className: [
225
226
  "flex items-center gap-3 px-3 py-2.5 rounded-sm text-sm font-medium",
226
227
  id === active ? "bg-primary/10 text-primary-dark" : "text-text hover:bg-surface-alt",
227
- ].join(" "), children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), label] }, id))) }) }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-4 md:px-6", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("button", { type: "button", className: "md:hidden shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open menu", onClick: () => setMenuOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide truncate", children: activeItem?.label })] }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut, showMailLink: true })] }), _jsx("div", { id: "app-content", className: "flex-1", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
228
+ ].join(" "), children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), label] }, id))) }) }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "rr-solid sticky top-0 z-30 h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-4 md:px-6", children: [_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("button", { type: "button", className: "md:hidden shrink-0 w-9 h-9 flex items-center justify-center rounded-sm text-text-muted hover:bg-surface-alt hover:text-text", "aria-label": "Open menu", onClick: () => setMenuOpen(true), children: _jsx(HiOutlineBars3, { size: 20, "aria-hidden": "true" }) }), _jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide truncate", children: activeItem?.label })] }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut, showMailLink: true })] }), _jsx("div", { id: "app-content", className: "flex-1", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: _jsx(ReconfirmIdentityContext.Provider, { value: authServerUrl ? handleRetryElevation : undefined, children: children }) }) })] })] }) }));
228
229
  }
229
230
  // The user's own colours and background (`AppearanceProvider`), from what the last webmail page cached in this browser: the console never asks the server.
230
231
  return (_jsxs(AppearanceProvider, { userUid: userUid, lookUp: false, children: [content, _jsx(FrameBrandingFooter, { parsed: footer, appTitle: [...adminNavItems(pluginNav), ...OFF_RAIL_ITEMS].find((item) => item.id === active)?.label })] }));
@@ -4,13 +4,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useRef, useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { eraseLeftoverMailbox } from "../../../../../lib/admin/leftoverMailboxApi.js";
9
8
  import { getErasureRequest } from "../../../../../lib/mail/erasureRequestApi.js";
10
- import Alert from "../../../../../lib/components/feedback/Alert.js";
9
+ import Alert from "../ActionAlert.js";
11
10
  import Button from "../../../../../lib/components/buttons/Button.js";
12
11
  import Modal from "../../../../../lib/components/overlays/Modal.js";
13
12
  import { notify } from "../../../notifications/store.js";
13
+ import { actionErrorMessage } from "../elevation.js";
14
14
  const INPUT_CLASS = "w-full text-sm py-2.5 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
15
  const DANGER_BUTTON_CLASS = "!w-auto !bg-none !bg-danger !border-danger hover:!bg-danger";
16
16
  /** How often the request is asked how far the erasure has got. */
@@ -53,7 +53,7 @@ export default function EraseLeftoverDataDialog({ address, counts, resume, doneL
53
53
  }
54
54
  catch (err) {
55
55
  // The server's own words: a legal hold names its matter, a mailbox that exists again says so.
56
- setError(err instanceof ApiRequestError ? err.message : "Could not start the erasure.");
56
+ setError(actionErrorMessage(err, "Could not start the erasure."));
57
57
  setPhase("confirm");
58
58
  }
59
59
  }
@@ -4,13 +4,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
6
  import { useEffect, useState } from "react";
7
- import { ApiRequestError } from "../../../../../lib/util/api.js";
8
7
  import { listEscrowScopes } from "../../../../../lib/admin/escrowScopesApi.js";
9
8
  import { updateMailbox } from "../../../../../lib/mail/mailApi.js";
10
9
  import { useApiClient } from "../../../../../lib/util/apiClientContext.js";
11
- import Alert from "../../../../../lib/components/feedback/Alert.js";
10
+ import Alert from "../ActionAlert.js";
12
11
  import Button from "../../../../../lib/components/buttons/Button.js";
13
12
  import Modal from "../../../../../lib/components/overlays/Modal.js";
13
+ import { actionErrorMessage } from "../elevation.js";
14
14
  const SELECT_CLASS = "text-sm py-2 px-3 border border-border rounded-sm bg-surface text-text focus:outline-none focus:border-primary";
15
15
  /** Enough for any realistic deployment's scopes in one list. */
16
16
  const SCOPE_LIST_LIMIT = 200;
@@ -30,7 +30,7 @@ export default function EscrowScopeCard({ mailbox, onUpdate }) {
30
30
  useEffect(() => {
31
31
  listEscrowScopes({ limit: SCOPE_LIST_LIMIT }, client)
32
32
  .then(setScopes)
33
- .catch((err) => setLoadError(err instanceof ApiRequestError ? err.message : "Could not load escrow scopes."));
33
+ .catch((err) => setLoadError(actionErrorMessage(err, "Could not load escrow scopes.")));
34
34
  }, []);
35
35
  // Changing the scope changes who can recover this mailbox's encrypted mail, so it's confirmed first.
36
36
  function handleSubmit(e) {
@@ -48,7 +48,7 @@ export default function EscrowScopeCard({ mailbox, onUpdate }) {
48
48
  setSaved(true);
49
49
  }
50
50
  catch (err) {
51
- setSaveError(err instanceof ApiRequestError ? err.message : "Could not save the escrow scope.");
51
+ setSaveError(actionErrorMessage(err, "Could not save the escrow scope."));
52
52
  }
53
53
  finally {
54
54
  setSaving(false);