@rapidmx/web-client 0.4.0 → 0.6.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 (320) hide show
  1. package/README.md +88 -0
  2. package/apps/admin/audit-log/index.tsx +35 -8
  3. package/apps/admin/data-requests/index.tsx +480 -485
  4. package/apps/admin/distribution-lists/[uid].tsx +19 -1
  5. package/apps/admin/escrow-scopes/[uid].tsx +348 -237
  6. package/apps/admin/escrow-scopes/new/index.tsx +8 -2
  7. package/apps/admin/index.tsx +19 -1
  8. package/apps/admin/ingest-queue/index.tsx +44 -5
  9. package/apps/admin/mailboxes/[uid].tsx +211 -184
  10. package/apps/admin/quarantine/index.tsx +46 -14
  11. package/apps/admin/transport-rules/[uid].tsx +30 -1
  12. package/apps/admin/transport-rules/_transportRuleConfig.tsx +38 -2
  13. package/apps/admin/transport-rules/index.tsx +24 -6
  14. package/apps/admin/transport-rules/new/index.tsx +30 -1
  15. package/apps/escrow/_layout.tsx +2 -3
  16. package/apps/escrow/audit-log/index.tsx +196 -168
  17. package/apps/escrow/matters/[uid].tsx +621 -521
  18. package/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.tsx +22 -1
  19. package/apps/shared/components/admin/layout/AdminShell.tsx +28 -9
  20. package/apps/shared/components/admin/mailboxes/EscrowScopeCard.tsx +152 -0
  21. package/apps/shared/components/admin/mailboxes/ResourceSettingsCard.tsx +3 -3
  22. package/apps/shared/components/admin/mailboxes/ShareAccessCard.tsx +55 -11
  23. package/apps/shared/components/admin/settings/BrandingForm.tsx +15 -4
  24. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +18 -7
  25. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +10 -2
  26. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +6 -0
  27. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +45 -12
  28. package/apps/shared/components/admin/settings/PluginsManager.tsx +1235 -595
  29. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +174 -98
  30. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +49 -13
  31. package/apps/shared/components/admin/setup/SetupWizard.tsx +150 -34
  32. package/apps/shared/components/admin/signOut.ts +79 -0
  33. package/apps/shared/components/admin/usePagedList.tsx +129 -0
  34. package/apps/shared/components/calendar/EventModal.tsx +717 -549
  35. package/apps/shared/components/calendar/MonthView.tsx +173 -139
  36. package/apps/shared/components/calendar/RecurrenceEditor.tsx +222 -180
  37. package/apps/shared/components/calendar/SplitDayView.tsx +143 -137
  38. package/apps/shared/components/calendar/TimeGridView.tsx +244 -198
  39. package/apps/shared/components/calendar/allDay.ts +124 -0
  40. package/apps/shared/components/calendar/layout/CalendarShell.tsx +194 -192
  41. package/apps/shared/components/contacts/ContactDetailPane.tsx +100 -21
  42. package/apps/shared/components/contacts/ContactForm.tsx +377 -348
  43. package/apps/shared/components/contacts/ContactsSidebar.tsx +6 -2
  44. package/apps/shared/components/contacts/KeyChangeReview.tsx +192 -0
  45. package/apps/shared/components/contacts/contactKeys.ts +78 -0
  46. package/apps/shared/components/contacts/layout/ContactsShell.tsx +199 -197
  47. package/apps/shared/components/escrow/layout/EscrowShell.tsx +148 -134
  48. package/apps/shared/components/layout/AppShell.tsx +123 -33
  49. package/apps/shared/components/layout/BrandingChrome.tsx +48 -10
  50. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +389 -298
  51. package/apps/shared/components/layout/MailboxProvisioning.tsx +19 -3
  52. package/apps/shared/components/layout/RecoveryCodeUnlock.tsx +350 -0
  53. package/apps/shared/components/layout/UnlockPromptProvider.tsx +257 -128
  54. package/apps/shared/components/mail/ConversationThreadPane.tsx +216 -181
  55. package/apps/shared/components/mail/MessageDetailPane.tsx +1318 -630
  56. package/apps/shared/components/mail/compose/ComposeContext.tsx +20 -15
  57. package/apps/shared/components/mail/compose/ComposeWindow.tsx +854 -132
  58. package/apps/shared/components/mail/compose/composeFlushRegistry.ts +60 -0
  59. package/apps/shared/components/mail/layout/MailShell.tsx +420 -408
  60. package/apps/shared/components/mail/pinnedSigners.ts +124 -0
  61. package/apps/shared/components/mail/verificationSeals.ts +125 -0
  62. package/apps/shared/components/mail/writableMailboxes.ts +101 -0
  63. package/apps/shared/components/rules/RuleBuilder.tsx +311 -276
  64. package/apps/shared/components/settings/layout/SettingsShell.tsx +23 -9
  65. package/apps/shared/components/tasks/layout/TasksShell.tsx +201 -199
  66. package/apps/shared/mail/listAllPages.ts +39 -0
  67. package/apps/shared/plugins/pluginNav.ts +73 -0
  68. package/apps/shared/search/LocalIndexLifecycle.tsx +64 -55
  69. package/apps/shared/search/localIndexBuilder.ts +481 -179
  70. package/apps/shared/search/localIndexRpcClient.ts +231 -29
  71. package/apps/shared/search/localIndexSchema.ts +15 -6
  72. package/apps/shared/search/localIndexStorage.ts +98 -0
  73. package/apps/shared/search/localIndexVFS.ts +32 -1
  74. package/apps/shared/search/localIndexWorker.ts +648 -186
  75. package/apps/www/calendar/index.tsx +438 -417
  76. package/apps/www/contacts/[uid].tsx +21 -2
  77. package/apps/www/contacts/index.tsx +116 -12
  78. package/apps/www/index.tsx +1314 -1041
  79. package/apps/www/messages/[uid].tsx +18 -6
  80. package/apps/www/settings/auto-reply/index.tsx +134 -129
  81. package/apps/www/settings/encryption/index.tsx +739 -216
  82. package/apps/www/settings/filters/[uid].tsx +171 -155
  83. package/apps/www/settings/filters/new/index.tsx +8 -1
  84. package/apps/www/settings/focused-inbox/index.tsx +150 -148
  85. package/apps/www/settings/privacy/index.tsx +494 -436
  86. package/apps/www/settings/read-receipts/index.tsx +148 -125
  87. package/apps/www/settings/sharing/index.tsx +17 -14
  88. package/apps/www/tasks/index.tsx +638 -579
  89. package/dist/apps/admin/_404.d.ts +2 -0
  90. package/dist/apps/admin/_500.d.ts +2 -0
  91. package/dist/apps/admin/_layout.d.ts +8 -0
  92. package/dist/apps/admin/audit-log/index.d.ts +11 -0
  93. package/dist/apps/admin/audit-log/index.js +38 -8
  94. package/dist/apps/admin/branding/index.d.ts +3 -0
  95. package/dist/apps/admin/data-requests/index.d.ts +3 -0
  96. package/dist/apps/admin/data-requests/index.js +52 -72
  97. package/dist/apps/admin/distribution-lists/[uid].d.ts +7 -0
  98. package/dist/apps/admin/distribution-lists/[uid].js +1 -1
  99. package/dist/apps/admin/distribution-lists/index.d.ts +3 -0
  100. package/dist/apps/admin/distribution-lists/new/index.d.ts +3 -0
  101. package/dist/apps/admin/domains/[uid].d.ts +7 -0
  102. package/dist/apps/admin/domains/index.d.ts +3 -0
  103. package/dist/apps/admin/domains/new/index.d.ts +3 -0
  104. package/dist/apps/admin/encryption-policy/index.d.ts +3 -0
  105. package/dist/apps/admin/escrow-scopes/[uid].d.ts +21 -0
  106. package/dist/apps/admin/escrow-scopes/[uid].js +91 -12
  107. package/dist/apps/admin/escrow-scopes/index.d.ts +3 -0
  108. package/dist/apps/admin/escrow-scopes/new/index.d.ts +3 -0
  109. package/dist/apps/admin/escrow-scopes/new/index.js +8 -3
  110. package/dist/apps/admin/index.d.ts +3 -0
  111. package/dist/apps/admin/index.js +4 -1
  112. package/dist/apps/admin/ingest-queue/index.d.ts +4 -0
  113. package/dist/apps/admin/ingest-queue/index.js +24 -6
  114. package/dist/apps/admin/mailbox-policy/index.d.ts +3 -0
  115. package/dist/apps/admin/mailboxes/[uid].d.ts +7 -0
  116. package/dist/apps/admin/mailboxes/[uid].js +20 -4
  117. package/dist/apps/admin/mailboxes/new/index.d.ts +3 -0
  118. package/dist/apps/admin/plugins/index.d.ts +3 -0
  119. package/dist/apps/admin/quarantine/index.d.ts +6 -0
  120. package/dist/apps/admin/quarantine/index.js +20 -11
  121. package/dist/apps/admin/retention-policy/index.d.ts +3 -0
  122. package/dist/apps/admin/setup/index.d.ts +3 -0
  123. package/dist/apps/admin/transport-rules/[uid].d.ts +7 -0
  124. package/dist/apps/admin/transport-rules/[uid].js +18 -2
  125. package/dist/apps/admin/transport-rules/_transportRuleConfig.d.ts +21 -0
  126. package/dist/apps/admin/transport-rules/_transportRuleConfig.js +19 -0
  127. package/dist/apps/admin/transport-rules/index.d.ts +3 -0
  128. package/dist/apps/admin/transport-rules/index.js +22 -6
  129. package/dist/apps/admin/transport-rules/new/index.d.ts +3 -0
  130. package/dist/apps/admin/transport-rules/new/index.js +18 -2
  131. package/dist/apps/escrow/_layout.d.ts +10 -0
  132. package/dist/apps/escrow/_layout.js +3 -3
  133. package/dist/apps/escrow/audit-log/index.d.ts +9 -0
  134. package/dist/apps/escrow/audit-log/index.js +26 -1
  135. package/dist/apps/escrow/index.d.ts +3 -0
  136. package/dist/apps/escrow/matters/[uid].d.ts +7 -0
  137. package/dist/apps/escrow/matters/[uid].js +77 -42
  138. package/dist/apps/escrow/matters/new/index.d.ts +3 -0
  139. package/dist/apps/shared/components/admin/distributionLists/MemberListCard.d.ts +17 -0
  140. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.d.ts +32 -0
  141. package/dist/apps/shared/components/admin/escrowScopes/EscrowScopeKeyAndHoldersFields.js +11 -2
  142. package/dist/apps/shared/components/admin/layout/AdminShell.d.ts +32 -0
  143. package/dist/apps/shared/components/admin/layout/AdminShell.js +15 -6
  144. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.d.ts +12 -0
  145. package/dist/apps/shared/components/admin/mailboxes/EscrowScopeCard.js +72 -0
  146. package/dist/apps/shared/components/admin/mailboxes/MailboxTable.d.ts +6 -0
  147. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.d.ts +19 -0
  148. package/dist/apps/shared/components/admin/mailboxes/ResourceSettingsCard.js +3 -3
  149. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.d.ts +12 -0
  150. package/dist/apps/shared/components/admin/mailboxes/ShareAccessCard.js +22 -4
  151. package/dist/apps/shared/components/admin/settings/BrandingForm.d.ts +9 -0
  152. package/dist/apps/shared/components/admin/settings/BrandingForm.js +15 -4
  153. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.d.ts +12 -0
  154. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +13 -7
  155. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.d.ts +12 -0
  156. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +7 -2
  157. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.d.ts +9 -0
  158. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.d.ts +19 -0
  159. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +6 -0
  160. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.d.ts +10 -0
  161. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +34 -12
  162. package/dist/apps/shared/components/admin/settings/PluginsManager.d.ts +4 -0
  163. package/dist/apps/shared/components/admin/settings/PluginsManager.js +390 -78
  164. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.d.ts +9 -0
  165. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +40 -11
  166. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.d.ts +14 -0
  167. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +29 -13
  168. package/dist/apps/shared/components/admin/setup/SetupWizard.d.ts +36 -0
  169. package/dist/apps/shared/components/admin/setup/SetupWizard.js +87 -21
  170. package/dist/apps/shared/components/admin/signOut.d.ts +23 -0
  171. package/dist/apps/shared/components/admin/signOut.js +75 -0
  172. package/dist/apps/shared/components/admin/usePagedList.d.ts +39 -0
  173. package/dist/apps/shared/components/admin/usePagedList.js +99 -0
  174. package/dist/apps/shared/components/calendar/CalendarListSidebar.d.ts +26 -0
  175. package/dist/apps/shared/components/calendar/EventModal.d.ts +46 -0
  176. package/dist/apps/shared/components/calendar/EventModal.js +142 -21
  177. package/dist/apps/shared/components/calendar/MonthView.d.ts +15 -0
  178. package/dist/apps/shared/components/calendar/MonthView.js +12 -5
  179. package/dist/apps/shared/components/calendar/RecurrenceEditor.d.ts +20 -0
  180. package/dist/apps/shared/components/calendar/RecurrenceEditor.js +40 -5
  181. package/dist/apps/shared/components/calendar/ResourcePicker.d.ts +19 -0
  182. package/dist/apps/shared/components/calendar/SplitDayView.d.ts +28 -0
  183. package/dist/apps/shared/components/calendar/SplitDayView.js +10 -4
  184. package/dist/apps/shared/components/calendar/TimeGridView.d.ts +17 -0
  185. package/dist/apps/shared/components/calendar/TimeGridView.js +23 -7
  186. package/dist/apps/shared/components/calendar/allDay.d.ts +51 -0
  187. package/dist/apps/shared/components/calendar/allDay.js +111 -0
  188. package/dist/apps/shared/components/calendar/layout/CalendarShell.d.ts +40 -0
  189. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +3 -3
  190. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +23 -0
  191. package/dist/apps/shared/components/contacts/ContactDetailPane.js +43 -5
  192. package/dist/apps/shared/components/contacts/ContactForm.d.ts +20 -0
  193. package/dist/apps/shared/components/contacts/ContactForm.js +32 -10
  194. package/dist/apps/shared/components/contacts/ContactsSidebar.d.ts +40 -0
  195. package/dist/apps/shared/components/contacts/ContactsSidebar.js +2 -2
  196. package/dist/apps/shared/components/contacts/ContactsToolbar.d.ts +25 -0
  197. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +40 -0
  198. package/dist/apps/shared/components/contacts/KeyChangeReview.js +63 -0
  199. package/dist/apps/shared/components/contacts/contactKeys.d.ts +29 -0
  200. package/dist/apps/shared/components/contacts/contactKeys.js +64 -0
  201. package/dist/apps/shared/components/contacts/layout/ContactsShell.d.ts +21 -0
  202. package/dist/apps/shared/components/contacts/layout/ContactsShell.js +2 -2
  203. package/dist/apps/shared/components/escrow/layout/EscrowShell.d.ts +25 -0
  204. package/dist/apps/shared/components/escrow/layout/EscrowShell.js +24 -5
  205. package/dist/apps/shared/components/forms/StringListField.d.ts +22 -0
  206. package/dist/apps/shared/components/layout/AppShell.d.ts +49 -0
  207. package/dist/apps/shared/components/layout/AppShell.js +97 -26
  208. package/dist/apps/shared/components/layout/BrandingChrome.d.ts +18 -0
  209. package/dist/apps/shared/components/layout/BrandingChrome.js +45 -10
  210. package/dist/apps/shared/components/layout/KeyEnrollmentGate.d.ts +51 -0
  211. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +49 -12
  212. package/dist/apps/shared/components/layout/MailboxProvisioning.d.ts +13 -0
  213. package/dist/apps/shared/components/layout/MailboxProvisioning.js +10 -2
  214. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.d.ts +66 -0
  215. package/dist/apps/shared/components/layout/RecoveryCodeUnlock.js +174 -0
  216. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +45 -0
  217. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +73 -14
  218. package/dist/apps/shared/components/layout/UserMenu.d.ts +23 -0
  219. package/dist/apps/shared/components/mail/ConversationList.d.ts +11 -0
  220. package/dist/apps/shared/components/mail/ConversationThreadPane.d.ts +23 -0
  221. package/dist/apps/shared/components/mail/ConversationThreadPane.js +50 -12
  222. package/dist/apps/shared/components/mail/MessageDetailPane.d.ts +93 -0
  223. package/dist/apps/shared/components/mail/MessageDetailPane.js +441 -36
  224. package/dist/apps/shared/components/mail/compose/ComposeContext.d.ts +58 -0
  225. package/dist/apps/shared/components/mail/compose/ComposeContext.js +9 -6
  226. package/dist/apps/shared/components/mail/compose/ComposeToolbar.d.ts +15 -0
  227. package/dist/apps/shared/components/mail/compose/ComposeWindow.d.ts +37 -0
  228. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +699 -128
  229. package/dist/apps/shared/components/mail/compose/EmojiPicker.d.ts +14 -0
  230. package/dist/apps/shared/components/mail/compose/GifPicker.d.ts +13 -0
  231. package/dist/apps/shared/components/mail/compose/RichTextEditor.d.ts +34 -0
  232. package/dist/apps/shared/components/mail/compose/ScheduleSendPicker.d.ts +16 -0
  233. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.d.ts +27 -0
  234. package/dist/apps/shared/components/mail/compose/composeFlushRegistry.js +50 -0
  235. package/dist/apps/shared/components/mail/layout/MailShell.d.ts +57 -0
  236. package/dist/apps/shared/components/mail/layout/MailShell.js +13 -4
  237. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +33 -0
  238. package/dist/apps/shared/components/mail/pinnedSigners.js +98 -0
  239. package/dist/apps/shared/components/mail/verificationSeals.d.ts +22 -0
  240. package/dist/apps/shared/components/mail/verificationSeals.js +112 -0
  241. package/dist/apps/shared/components/mail/writableMailboxes.d.ts +15 -0
  242. package/dist/apps/shared/components/mail/writableMailboxes.js +87 -0
  243. package/dist/apps/shared/components/rules/RuleBuilder.d.ts +70 -0
  244. package/dist/apps/shared/components/rules/RuleBuilder.js +34 -6
  245. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +41 -0
  246. package/dist/apps/shared/components/settings/layout/SettingsShell.js +19 -9
  247. package/dist/apps/shared/components/tasks/TasksSidebar.d.ts +41 -0
  248. package/dist/apps/shared/components/tasks/TasksToolbar.d.ts +14 -0
  249. package/dist/apps/shared/components/tasks/layout/TasksShell.d.ts +24 -0
  250. package/dist/apps/shared/components/tasks/layout/TasksShell.js +2 -2
  251. package/dist/apps/shared/mail/findWellKnownFolderUid.d.ts +5 -0
  252. package/dist/apps/shared/mail/listAllPages.d.ts +17 -0
  253. package/dist/apps/shared/mail/listAllPages.js +25 -0
  254. package/dist/apps/shared/plugins/pluginNav.d.ts +39 -0
  255. package/dist/apps/shared/plugins/pluginNav.js +33 -0
  256. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +13 -0
  257. package/dist/apps/shared/search/LocalIndexLifecycle.js +58 -54
  258. package/dist/apps/shared/search/localIndexBlockCipher.d.ts +36 -0
  259. package/dist/apps/shared/search/localIndexBuilder.d.ts +85 -0
  260. package/dist/apps/shared/search/localIndexBuilder.js +292 -37
  261. package/dist/apps/shared/search/localIndexKey.d.ts +4 -0
  262. package/dist/apps/shared/search/localIndexRpcClient.d.ts +71 -0
  263. package/dist/apps/shared/search/localIndexRpcClient.js +191 -26
  264. package/dist/apps/shared/search/localIndexSchema.d.ts +111 -0
  265. package/dist/apps/shared/search/localIndexSchema.js +12 -6
  266. package/dist/apps/shared/search/localIndexSizePreference.d.ts +27 -0
  267. package/dist/apps/shared/search/localIndexStorage.d.ts +28 -0
  268. package/dist/apps/shared/search/localIndexStorage.js +86 -0
  269. package/dist/apps/shared/search/localIndexVFS.d.ts +101 -0
  270. package/dist/apps/shared/search/localIndexVFS.js +68 -40
  271. package/dist/apps/shared/search/localIndexWorker.d.ts +145 -0
  272. package/dist/apps/shared/search/localIndexWorker.js +519 -173
  273. package/dist/apps/shared/search/searchTier2.d.ts +30 -0
  274. package/dist/apps/www/_404.d.ts +2 -0
  275. package/dist/apps/www/_500.d.ts +2 -0
  276. package/dist/apps/www/_layout.d.ts +14 -0
  277. package/dist/apps/www/calendar/index.d.ts +3 -0
  278. package/dist/apps/www/calendar/index.js +28 -10
  279. package/dist/apps/www/contacts/[uid].d.ts +13 -0
  280. package/dist/apps/www/contacts/[uid].js +13 -2
  281. package/dist/apps/www/contacts/index.d.ts +3 -0
  282. package/dist/apps/www/contacts/index.js +80 -10
  283. package/dist/apps/www/index.d.ts +3 -0
  284. package/dist/apps/www/index.js +339 -108
  285. package/dist/apps/www/messages/[uid].d.ts +11 -0
  286. package/dist/apps/www/messages/[uid].js +18 -6
  287. package/dist/apps/www/settings/auto-reply/index.d.ts +4 -0
  288. package/dist/apps/www/settings/auto-reply/index.js +9 -4
  289. package/dist/apps/www/settings/encryption/index.d.ts +7 -0
  290. package/dist/apps/www/settings/encryption/index.js +514 -97
  291. package/dist/apps/www/settings/filters/[uid].d.ts +8 -0
  292. package/dist/apps/www/settings/filters/[uid].js +22 -7
  293. package/dist/apps/www/settings/filters/_mailFilterRuleConfig.d.ts +11 -0
  294. package/dist/apps/www/settings/filters/index.d.ts +4 -0
  295. package/dist/apps/www/settings/filters/new/index.d.ts +4 -0
  296. package/dist/apps/www/settings/filters/new/index.js +8 -2
  297. package/dist/apps/www/settings/focused-inbox/index.d.ts +4 -0
  298. package/dist/apps/www/settings/focused-inbox/index.js +3 -1
  299. package/dist/apps/www/settings/labels/index.d.ts +4 -0
  300. package/dist/apps/www/settings/privacy/index.d.ts +4 -0
  301. package/dist/apps/www/settings/privacy/index.js +57 -22
  302. package/dist/apps/www/settings/read-receipts/index.d.ts +4 -0
  303. package/dist/apps/www/settings/read-receipts/index.js +10 -3
  304. package/dist/apps/www/settings/sharing/index.d.ts +4 -0
  305. package/dist/apps/www/settings/sharing/index.js +7 -11
  306. package/dist/apps/www/settings/signatures/[uid].d.ts +8 -0
  307. package/dist/apps/www/settings/signatures/index.d.ts +4 -0
  308. package/dist/apps/www/settings/signatures/new/index.d.ts +4 -0
  309. package/dist/apps/www/settings/signatures/signatureDefaults.d.ts +10 -0
  310. package/dist/apps/www/tasks/index.d.ts +3 -0
  311. package/dist/apps/www/tasks/index.js +50 -10
  312. package/package.json +3 -3
  313. package/apps/shared/components/booking/AvailabilityEditor.tsx +0 -118
  314. package/apps/www/settings/booking-types/[uid].tsx +0 -300
  315. package/apps/www/settings/booking-types/index.tsx +0 -98
  316. package/apps/www/settings/booking-types/new/index.tsx +0 -212
  317. package/dist/apps/shared/components/booking/AvailabilityEditor.js +0 -45
  318. package/dist/apps/www/settings/booking-types/[uid].js +0 -129
  319. package/dist/apps/www/settings/booking-types/index.js +0 -29
  320. package/dist/apps/www/settings/booking-types/new/index.js +0 -86
@@ -1,134 +1,148 @@
1
- ///////////////////////////////////////////////////////////////////////////////
2
- // Copyright (C) 2026 Jean-Philippe Steinmetz
3
- // SPDX-License-Identifier: MPL-2.0
4
- ///////////////////////////////////////////////////////////////////////////////
5
- import "../../../styles/app.css";
6
- import React, { PropsWithChildren, ReactNode, useEffect, useState } from "react";
7
- import { HiOutlineClipboardDocumentList, HiOutlineFolderOpen } from "react-icons/hi2";
8
- import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
- import { listMatters } from "@rapidmx/react-shared/admin/mattersApi.js";
10
- import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
11
- import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
12
- import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
13
- import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
14
- import { BrandingFooter, BrandingHeader } from "../../layout/BrandingChrome.js";
15
- import UserMenu from "../../layout/UserMenu.js";
16
-
17
- export type EscrowSection = "matters" | "auditLog";
18
-
19
- export interface EscrowShellProps {
20
- active: EscrowSection;
21
- userUid?: string;
22
- authServerUrl?: string;
23
- }
24
-
25
- type Status = "checking" | "error" | "authorized";
26
-
27
- const NAV_ITEMS: NavItem[] = [
28
- { id: "matters", href: "/escrow", label: "Matters", icon: HiOutlineFolderOpen },
29
- { id: "auditLog", href: "/escrow/audit-log", label: "Audit Log", icon: HiOutlineClipboardDocumentList },
30
- ];
31
-
32
- /**
33
- * Gates every `apps/escrow` page behind "is this an authenticated caller at all" — deliberately **not**
34
- * the same "does a canary endpoint 403 me" pattern `AdminShell` uses for the trusted `admin` role.
35
- * `specs/end-to-end_encryption.md`'s "Separation of duties" makes holder-ness a per-`EscrowScope` grant
36
- * (`EscrowScope.holderUserUids`), checked directly against `holderUserUids` in every restapi route that
37
- * needs it (`requireEscrowHolder()`) — never a role, and never global. There is no canary endpoint a
38
- * non-holder gets a clean 403 from: `GET /escrow/matters` (`BaseMatterRoute.find()`) returns `200 []` for
39
- * *any* authenticated caller who holds no scope at all, the same as it would for a holder of zero matters
40
- * — so unlike `AdminShell`, this shell cannot distinguish "holds nothing" from "holds no scope at all" up
41
- * front, and does not try to. It only confirms the caller is signed in and the API is reachable; every
42
- * actual holder-gated action (create a Matter, approve/deny a request, read material) is enforced
43
- * server-side and surfaces its own 403 `ApiRequestError` on the specific page that attempted it, same as
44
- * any other gated action elsewhere in this codebase. A non-holder who reaches `/escrow` simply sees an
45
- * empty Matters list and gets a real error if they try to act on something they don't hold — this is a
46
- * deliberate, documented scope trim rather than an oversight (see this session's own report on it).
47
- */
48
- export default function EscrowShell({ active, userUid, authServerUrl, children }: PropsWithChildren<EscrowShellProps>) {
49
- const [status, setStatus] = useState<Status>("checking");
50
- const [error, setError] = useState<string | null>(null);
51
- const { branding, iconSrc } = useBranding();
52
-
53
- useRedirectIfUnauthenticated(userUid, authServerUrl);
54
-
55
- useEffect(() => {
56
- if (!userUid) {
57
- return;
58
- }
59
- // limit=1 - this call exists only to confirm the API is reachable for this signed-in caller, not
60
- // to read any real data (every page below fetches its own data itself).
61
- listMatters({ limit: 1 })
62
- .then(() => setStatus("authorized"))
63
- .catch((err) => {
64
- setError(err instanceof ApiRequestError ? err.message : "Could not verify escrow console access.");
65
- setStatus("error");
66
- });
67
- }, [userUid]);
68
-
69
- function handleSignOut() {
70
- window.location.href = authServerUrl ?? "/";
71
- }
72
-
73
- let content: ReactNode;
74
- if (!userUid || status === "checking") {
75
- content = <div className="min-h-screen" />;
76
- } else if (status === "error") {
77
- content = (
78
- <div className="min-h-screen flex items-center justify-center p-8">
79
- <div className="w-full max-w-md">
80
- <Alert>{error}</Alert>
81
- </div>
82
- </div>
83
- );
84
- } else {
85
- const activeItem = NAV_ITEMS.find((item) => item.id === active);
86
- content = (
87
- <div className="min-h-screen flex flex-col bg-surface-alt">
88
- <div className="flex-1 flex min-h-0">
89
- <nav
90
- aria-label="Escrow sections"
91
- className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
92
- >
93
- <img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
94
- {NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (
95
- <a
96
- key={id}
97
- href={href}
98
- aria-label={label}
99
- aria-current={id === active ? "page" : undefined}
100
- title={label}
101
- className={[
102
- "w-10 h-10 flex items-center justify-center rounded-sm",
103
- id === active
104
- ? "bg-primary/10 text-primary-dark"
105
- : "text-text-muted hover:bg-surface-alt hover:text-text",
106
- ].join(" ")}
107
- >
108
- <Icon size={20} aria-hidden="true" />
109
- </a>
110
- ))}
111
- </nav>
112
- <BottomTabBar apps={NAV_ITEMS} active={active} />
113
- <div className="flex-1 flex flex-col min-w-0">
114
- <header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
115
- <span className="font-display font-bold text-lg uppercase tracking-wide">{activeItem?.label}</span>
116
- <UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} />
117
- </header>
118
- <div className="flex-1 pb-14 md:pb-0">
119
- <main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
120
- </div>
121
- </div>
122
- </div>
123
- </div>
124
- );
125
- }
126
-
127
- return (
128
- <>
129
- <BrandingHeader branding={branding} />
130
- {content}
131
- <BrandingFooter branding={branding} />
132
- </>
133
- );
134
- }
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import "../../../styles/app.css";
6
+ import React, { PropsWithChildren, ReactNode, useEffect, useState } from "react";
7
+ import { HiOutlineClipboardDocumentList, HiOutlineFolderOpen } from "react-icons/hi2";
8
+ import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
9
+ import { listMatters } from "@rapidmx/react-shared/admin/mattersApi.js";
10
+ import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
11
+ import { getBranding } from "@rapidmx/react-shared/branding/brandingApi.js";
12
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
13
+ import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
14
+ import UserMenu from "../../layout/UserMenu.js";
15
+ import { signOutOfConsole } from "../../admin/signOut.js";
16
+
17
+ export type EscrowSection = "matters" | "auditLog";
18
+
19
+ export interface EscrowShellProps {
20
+ active: EscrowSection;
21
+ userUid?: string;
22
+ authServerUrl?: string;
23
+ }
24
+
25
+ type Status = "checking" | "error" | "authorized";
26
+
27
+ const DEFAULT_ICON_SRC = "/images/logo.svg";
28
+
29
+ const NAV_ITEMS: NavItem[] = [
30
+ { id: "matters", href: "/escrow", label: "Matters", icon: HiOutlineFolderOpen },
31
+ { id: "auditLog", href: "/escrow/audit-log", label: "Audit Log", icon: HiOutlineClipboardDocumentList },
32
+ ];
33
+
34
+ /**
35
+ * Gates every `apps/escrow` page behind "is this an authenticated caller at all" — deliberately **not**
36
+ * the same "does a canary endpoint 403 me" pattern `AdminShell` uses for the trusted `admin` role.
37
+ * `specs/end-to-end_encryption.md`'s "Separation of duties" makes holder-ness a per-`EscrowScope` grant
38
+ * (`EscrowScope.holderUserUids`), checked directly against `holderUserUids` in every restapi route that
39
+ * needs it (`requireEscrowHolder()`) — never a role, and never global. There is no canary endpoint a
40
+ * non-holder gets a clean 403 from: `GET /escrow/matters` (`BaseMatterRoute.find()`) returns `200 []` for
41
+ * *any* authenticated caller who holds no scope at all, the same as it would for a holder of zero matters
42
+ * — so unlike `AdminShell`, this shell cannot distinguish "holds nothing" from "holds no scope at all" up
43
+ * front, and does not try to. It only confirms the caller is signed in and the API is reachable; every
44
+ * actual holder-gated action (create a Matter, approve/deny a request, read material) is enforced
45
+ * server-side and surfaces its own 403 `ApiRequestError` on the specific page that attempted it, same as
46
+ * any other gated action elsewhere in this codebase. A non-holder who reaches `/escrow` simply sees an
47
+ * empty Matters list and gets a real error if they try to act on something they don't hold — this is a
48
+ * deliberate, documented scope trim rather than an oversight (see this session's own report on it).
49
+ */
50
+ export default function EscrowShell({ active, userUid, authServerUrl, children }: PropsWithChildren<EscrowShellProps>) {
51
+ const [status, setStatus] = useState<Status>("checking");
52
+ const [error, setError] = useState<string | null>(null);
53
+ const [iconSrc, setIconSrc] = useState(DEFAULT_ICON_SRC);
54
+
55
+ useRedirectIfUnauthenticated(userUid, authServerUrl);
56
+
57
+ // Only the icon is taken from branding here. The escrow console deliberately skips `useBranding()`, which
58
+ // would inject the custom stylesheet, and never renders the admin-configured header/footer HTML - this
59
+ // console handles escrow key material, so no admin-supplied markup or CSS runs on it.
60
+ useEffect(() => {
61
+ let cancelled = false;
62
+ getBranding()
63
+ .then((branding) => {
64
+ if (!cancelled) setIconSrc(branding.iconUrl || branding.logoUrl || DEFAULT_ICON_SRC);
65
+ })
66
+ .catch(() => {
67
+ // Decorative only - keep the default icon.
68
+ });
69
+ return () => {
70
+ cancelled = true;
71
+ };
72
+ }, []);
73
+
74
+ useEffect(() => {
75
+ if (!userUid) {
76
+ return;
77
+ }
78
+ // limit=1 - this call exists only to confirm the API is reachable for this signed-in caller, not
79
+ // to read any real data (every page below fetches its own data itself).
80
+ listMatters({ limit: 1 })
81
+ .then(() => setStatus("authorized"))
82
+ .catch((err) => {
83
+ setError(err instanceof ApiRequestError ? err.message : "Could not verify escrow console access.");
84
+ setStatus("error");
85
+ });
86
+ }, [userUid]);
87
+
88
+ function handleSignOut() {
89
+ // Ends the auth-server session and tells other tabs, not just navigates - see `signOutOfConsole()`.
90
+ void signOutOfConsole(authServerUrl);
91
+ }
92
+
93
+ let content: ReactNode;
94
+ if (!userUid || status === "checking") {
95
+ content = <div className="min-h-screen" />;
96
+ } else if (status === "error") {
97
+ content = (
98
+ <div className="min-h-screen flex items-center justify-center p-8">
99
+ <div className="w-full max-w-md">
100
+ <Alert>{error}</Alert>
101
+ </div>
102
+ </div>
103
+ );
104
+ } else {
105
+ const activeItem = NAV_ITEMS.find((item) => item.id === active);
106
+ content = (
107
+ <div className="min-h-screen flex flex-col bg-surface-alt">
108
+ <div className="flex-1 flex min-h-0">
109
+ <nav
110
+ aria-label="Escrow sections"
111
+ className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
112
+ >
113
+ <img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
114
+ {NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (
115
+ <a
116
+ key={id}
117
+ href={href}
118
+ aria-label={label}
119
+ aria-current={id === active ? "page" : undefined}
120
+ title={label}
121
+ className={[
122
+ "w-10 h-10 flex items-center justify-center rounded-sm",
123
+ id === active
124
+ ? "bg-primary/10 text-primary-dark"
125
+ : "text-text-muted hover:bg-surface-alt hover:text-text",
126
+ ].join(" ")}
127
+ >
128
+ <Icon size={20} aria-hidden="true" />
129
+ </a>
130
+ ))}
131
+ </nav>
132
+ <BottomTabBar apps={NAV_ITEMS} active={active} />
133
+ <div className="flex-1 flex flex-col min-w-0">
134
+ <header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
135
+ <span className="font-display font-bold text-lg uppercase tracking-wide">{activeItem?.label}</span>
136
+ <UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} />
137
+ </header>
138
+ <div className="flex-1 pb-14 md:pb-0">
139
+ <main className="max-w-6xl mx-auto px-6 py-8">{children}</main>
140
+ </div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ );
145
+ }
146
+
147
+ return <>{content}</>;
148
+ }
@@ -3,30 +3,66 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import "../../styles/app.css";
6
- import React, { PropsWithChildren, useState, useEffect } from "react";
6
+ import React, { PropsWithChildren, useEffect, useRef, useState } from "react";
7
7
  import type { IconType } from "react-icons";
8
- import { HiOutlineCalendarDays, HiOutlineClipboardDocumentList, HiOutlineEnvelope, HiOutlineUsers } from "react-icons/hi2";
8
+ import {
9
+ HiOutlineCalendarDays,
10
+ HiOutlineClipboardDocumentList,
11
+ HiOutlineEnvelope,
12
+ HiOutlinePuzzlePiece,
13
+ HiOutlineUsers,
14
+ } from "react-icons/hi2";
9
15
  import { useRedirectIfUnauthenticated } from "@rapidmx/react-shared/auth/session.js";
10
16
  import { getSetupStatus } from "@rapidmx/react-shared/admin/setupApi.js";
11
17
  import { stopImpersonating } from "@rapidmx/react-shared/mail/mailApi.js";
12
18
  import useBranding from "@rapidmx/react-shared/branding/useBranding.js";
13
19
  import { useIdleKeyTimeout } from "@rapidmx/react-shared/crypto/useIdleKeyTimeout.js";
14
20
  import ComposeProvider from "../mail/compose/ComposeContext.js";
15
- import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
21
+ import { flushComposeDrafts, markSigningOut } from "../mail/compose/composeFlushRegistry.js";
22
+ import BottomTabBar, { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
16
23
  import { BrandingFooter, BrandingHeader } from "./BrandingChrome.js";
17
24
  import UserMenu from "./UserMenu.js";
18
25
  import { UnlockPromptProvider } from "./UnlockPromptProvider.js";
19
- import { destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
26
+ import { SIGN_OUT_CHANNEL, destroyAllLocalIndexes } from "../../search/localIndexRpcClient.js";
27
+ import { authApiFetch } from "@rapidmx/react-shared/util/api.js";
28
+ import { destroyUnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
29
+ import { clearPinnedSignerCache } from "../mail/pinnedSigners.js";
30
+ import { mergePluginNavItems, PluginNav, PluginNavProps } from "../../plugins/pluginNav.js";
31
+
32
+ /** How long sign-out waits for auth-server's logout before navigating anyway. */
33
+ export const LOGOUT_TIMEOUT_MS = 3_000;
34
+
35
+ /**
36
+ * Calls auth-server's logout (`POST /api/auth/logout`, cross-origin with credentials), which clears the auth
37
+ * cookie and invalidates the session's refresh token. Bounded by `LOGOUT_TIMEOUT_MS` and never rejects - a
38
+ * failure (unreachable server, CORS) must not keep the user from leaving.
39
+ */
40
+ async function logOutOfAuthServer(authServerUrl: string | undefined): Promise<void> {
41
+ if (!authServerUrl) {
42
+ return;
43
+ }
44
+ const controller = new AbortController();
45
+ const timer = setTimeout(() => controller.abort(), LOGOUT_TIMEOUT_MS);
46
+ try {
47
+ await authApiFetch(authServerUrl, "/auth/logout", { method: "POST", signal: controller.signal });
48
+ } catch {
49
+ // Navigate anyway - see this function's doc comment.
50
+ } finally {
51
+ clearTimeout(timer);
52
+ }
53
+ }
20
54
 
21
55
  export type AppShellApp = "mail" | "calendar" | "contacts" | "tasks";
22
56
 
23
57
  /** `"settings"` is a valid `active` value but deliberately has no entry in `APPS` below — Settings is
24
58
  * reached via a `UserMenu` item, not a 5th rail icon (see `SettingsShell.tsx`), so it highlights no
25
- * rail/tab icon at all; only the header title (`ACTIVE_LABELS` below) needs to account for it. */
26
- export type AppShellActive = AppShellApp | "settings";
59
+ * rail/tab icon at all; only the header title needs to account for it. Any other string is a plugin's
60
+ * `appRail` item id (see `PluginNav`). */
61
+ export type AppShellActive = AppShellApp | "settings" | (string & {});
27
62
 
28
- export interface AppShellProps {
29
- /** Which icon in the rail is highlighted as the current app — `"settings"` highlights none. */
63
+ export interface AppShellProps extends PluginNavProps {
64
+ /** Which icon in the rail is highlighted as the current app — `"settings"` highlights none, and a plugin
65
+ * app page passes its own `appRail` item id. */
30
66
  active: AppShellActive;
31
67
  /** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
32
68
  userUid?: string;
@@ -58,15 +94,19 @@ export const APPS: AppDef[] = [
58
94
  { id: "tasks", href: "/tasks", label: "Tasks", icon: HiOutlineClipboardDocumentList },
59
95
  ];
60
96
 
61
- /** The header title for every valid `active` value — a superset of `APPS`' own labels since `"settings"`
62
- * has no rail icon (and so no `AppDef`) but still needs a header title. */
63
- const ACTIVE_LABELS: Record<AppShellActive, string> = {
64
- mail: "Mail",
65
- calendar: "Calendar",
66
- contacts: "Contacts",
67
- tasks: "Tasks",
68
- settings: "Settings",
69
- };
97
+ /** Ids plugin `appRail` items can't take besides `APPS`' own - `"settings"` has no rail icon but is still a
98
+ * core `active` value. */
99
+ const RESERVED_APP_IDS = ["settings"];
100
+
101
+ /** `APPS` followed by the plugins' `appRail` items (generic icon), core ids winning - see `mergePluginNavItems`. */
102
+ export function appRailItems(pluginNav?: PluginNav): NavItem[] {
103
+ return mergePluginNavItems<NavItem>(
104
+ APPS,
105
+ pluginNav?.appRail,
106
+ ({ id, href, label }) => ({ id, href, label, icon: HiOutlinePuzzlePiece }),
107
+ RESERVED_APP_IDS,
108
+ );
109
+ }
70
110
 
71
111
  /**
72
112
  * The persistent chrome shared by every webmail app (Mail, Calendar, Contacts, Tasks): a left icon rail for
@@ -82,9 +122,11 @@ export default function AppShell({
82
122
  impersonating,
83
123
  impersonationBaseUrl,
84
124
  trusted,
125
+ pluginNav,
85
126
  children,
86
127
  }: PropsWithChildren<AppShellProps>) {
87
128
  const [stoppingImpersonation, setStoppingImpersonation] = useState(false);
129
+ const signingOutRef = useRef(false);
88
130
  const { branding, iconSrc } = useBranding();
89
131
 
90
132
  useRedirectIfUnauthenticated(userUid, authServerUrl);
@@ -92,10 +134,11 @@ export default function AppShell({
92
134
  // specifically so activity in *any* app resets the idle clock - see that hook's own doc comment.
93
135
  useIdleKeyTimeout();
94
136
 
95
- // An administrator on a server that hasn't finished first-run setup is sent to the setup wizard. Everyone else
96
- // gets a 403 from the setup status check (it's admin-only), which is ignored along with any other failure.
137
+ // An administrator on a server that hasn't finished first-run setup is sent to the setup wizard. The status
138
+ // check is admin-only, so it's only made for a trusted caller - everyone else would just get a 403 on every
139
+ // page load. Any failure is ignored.
97
140
  useEffect(() => {
98
- if (!userUid || impersonating) {
141
+ if (!userUid || impersonating || !trusted) {
99
142
  return;
100
143
  }
101
144
  getSetupStatus()
@@ -105,15 +148,56 @@ export default function AppShell({
105
148
  }
106
149
  })
107
150
  .catch(() => undefined);
108
- }, [userUid, impersonating]);
109
-
110
- function handleSignOut() {
111
- // Best-effort, fire-and-forget: the Tier 2 local index MUST be destroyed on explicit logout, the
112
- // same as unlocked keys themselves (spec §11) - this component has no specific mailboxUid of its
113
- // own to pass (it's shared by every app, not just Mail), so this destroys every mailbox's index
114
- // the session has open rather than needing one threaded through. Not awaited - the navigation
115
- // below shouldn't wait on it, and destroyAllLocalIndexes() never throws either way.
116
- void destroyAllLocalIndexes();
151
+ }, [userUid, impersonating, trusted]);
152
+
153
+ // Another tab signing out (this app's `handleSignOut`, or the admin/escrow consoles' `signOutOfConsole()`)
154
+ // ended this session too - its auth cookie is gone - so this tab destroys its own unlocked keys and every
155
+ // local search index on the device, then leaves as well. The consoles have no local-index client of their
156
+ // own, so this is what actually removes the indexes after a console sign-out (the console also records a
157
+ // pending deletion, retried on the next mail load, in case no mail tab is open). `destroyAllLocalIndexes()`
158
+ // announces the sign-out on this same channel, which this tab then hears itself, so the ref is set first:
159
+ // each tab reacts once, and the tab that started the sign-out ignores its own announcement.
160
+ useEffect(() => {
161
+ if (!userUid || typeof BroadcastChannel === "undefined") {
162
+ return;
163
+ }
164
+ const channel = new BroadcastChannel(SIGN_OUT_CHANNEL);
165
+ channel.addEventListener("message", (event: MessageEvent<{ type?: string }>) => {
166
+ if (event.data?.type !== "sign-out" || signingOutRef.current) {
167
+ return;
168
+ }
169
+ signingOutRef.current = true;
170
+ // Compose windows must not ask "Leave site?" - that would let this forced navigation be cancelled.
171
+ markSigningOut();
172
+ destroyUnlockedKeys();
173
+ clearPinnedSignerCache();
174
+ // Bounded by its own timeout and never rejects - awaited so navigating doesn't kill the Worker mid-delete.
175
+ void destroyAllLocalIndexes().then(() => {
176
+ window.location.href = authServerUrl ?? "/";
177
+ });
178
+ });
179
+ return () => channel.close();
180
+ }, [userUid, authServerUrl]);
181
+
182
+ async function handleSignOut() {
183
+ signingOutRef.current = true;
184
+ // Before flushing and navigating: compose windows then skip their "Leave site?" prompt, which could
185
+ // otherwise cancel the sign-out's own navigation.
186
+ markSigningOut();
187
+ // Unlocked private keys never outlive an explicit sign-out.
188
+ destroyUnlockedKeys();
189
+ // Trusted signer pins read from contacts don't outlive the session either.
190
+ clearPinnedSignerCache();
191
+ // Open compose windows save edits still waiting on their autosave debounce while the session is still
192
+ // valid - logout invalidates it. Bounded the same way as logout itself, and never rejects.
193
+ await flushComposeDrafts(LOGOUT_TIMEOUT_MS);
194
+ // The Tier 2 local index MUST be destroyed on explicit logout, the same as unlocked keys themselves
195
+ // (spec §11). Destroys every index on this device (not only mailboxes opened this page load) and is
196
+ // awaited before navigating - a navigation tears down the Worker mid-delete otherwise.
197
+ // destroyAllLocalIndexes() is bounded by its own timeout and never rejects, so sign-out can't hang.
198
+ // In parallel, auth-server's logout clears the auth cookie and invalidates the session's refresh
199
+ // token - without it, "Sign Out" would only navigate away from a still-valid session.
200
+ await Promise.all([destroyAllLocalIndexes(), logOutOfAuthServer(authServerUrl)]);
117
201
  window.location.href = authServerUrl ?? "/";
118
202
  }
119
203
 
@@ -134,12 +218,18 @@ export default function AppShell({
134
218
  return <div className="min-h-screen" />;
135
219
  }
136
220
 
221
+ const apps = appRailItems(pluginNav);
222
+ // The header title: "settings" has no rail item, everything else is labelled by its own rail item.
223
+ const activeLabel = active === "settings" ? "Settings" : apps.find((app) => app.id === active)?.label;
224
+
137
225
  return (
138
226
  // Mounted here, not scoped to Mail/Settings, for the same reason as useIdleKeyTimeout() above -
139
227
  // ComposeWindow's sign/encrypt toggles and MessageDetailPane's encrypted-message view (both Mail)
140
228
  // are today's only useUnlockPrompt() callers, but this needs to be available to any app shell.
141
229
  <UnlockPromptProvider>
142
- <ComposeProvider userUid={userUid}>
230
+ {/* An impersonating admin acts with the impersonated user's access, so their own trusted role
231
+ mustn't skip the per-mailbox checks. */}
232
+ <ComposeProvider userUid={userUid} trusted={!!trusted && !impersonating}>
143
233
  <div className="min-h-screen flex flex-col bg-surface-alt">
144
234
  <BrandingHeader branding={branding} />
145
235
  {impersonating && (
@@ -163,7 +253,7 @@ export default function AppShell({
163
253
  className="hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1"
164
254
  >
165
255
  <img src={iconSrc} width="96" height="96" alt="" className="mb-3" />
166
- {APPS.map(({ id, href, label, icon: Icon }) => (
256
+ {apps.map(({ id, href, label, icon: Icon }) => (
167
257
  <a
168
258
  key={id}
169
259
  href={href}
@@ -181,10 +271,10 @@ export default function AppShell({
181
271
  </a>
182
272
  ))}
183
273
  </nav>
184
- <BottomTabBar apps={APPS} active={active} />
274
+ <BottomTabBar apps={apps} active={active} />
185
275
  <div className="flex-1 flex flex-col min-w-0">
186
276
  <header className="h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6">
187
- <span className="font-display font-bold text-lg uppercase tracking-wide">{ACTIVE_LABELS[active]}</span>
277
+ <span className="font-display font-bold text-lg uppercase tracking-wide">{activeLabel}</span>
188
278
  <UserMenu userUid={userUid} authServerUrl={authServerUrl} onSignOut={handleSignOut} showAdminLink={trusted} showSettingsLink />
189
279
  </header>
190
280
  <div className="flex-1 flex min-h-0 pb-14 md:pb-0">{children}</div>
@@ -3,23 +3,61 @@
3
3
  // SPDX-License-Identifier: MPL-2.0
4
4
  ///////////////////////////////////////////////////////////////////////////////
5
5
  import React from "react";
6
+ import DOMPurify from "dompurify";
6
7
  import { Branding } from "@rapidmx/react-shared/branding/brandingApi.js";
7
8
 
9
+ /** Elements that could run script, restyle the whole page, collect input, or embed another page - stripped on
10
+ * top of DOMPurify's defaults (which already drop `<script>`, `on*` handlers, and `javascript:` URLs). */
11
+ const FORBIDDEN_TAGS = [
12
+ "script",
13
+ "style",
14
+ "form",
15
+ "input",
16
+ "button",
17
+ "textarea",
18
+ "select",
19
+ "option",
20
+ "iframe",
21
+ "frame",
22
+ "frameset",
23
+ "object",
24
+ "embed",
25
+ "link",
26
+ "meta",
27
+ "base",
28
+ "svg",
29
+ "math",
30
+ ];
31
+ const FORBIDDEN_ATTRS = ["action", "formaction", "srcdoc"];
32
+
8
33
  /**
9
- * Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`) — shared by every
10
- * chrome in this project (`AppShell`, `AdminShell`, and the public `apps/book` pages) so the setting applies
11
- * everywhere its own admin UI copy (`apps/admin/branding/index.tsx`) claims it does.
34
+ * Sanitizes admin-configured branding HTML before it's rendered. The server sanitizes on save too; this also
35
+ * covers anything stored before that. Returns `""` where there's no DOM to sanitize with (server-side
36
+ * rendering), so unsanitized HTML is never emitted.
12
37
  */
13
- export function BrandingHeader({ branding }: { branding: Branding | null }) {
14
- if (!branding?.headerHtml) {
15
- return null;
38
+ export function sanitizeBrandingHtml(html: string): string {
39
+ if (!DOMPurify.isSupported) {
40
+ return "";
16
41
  }
17
- return <div dangerouslySetInnerHTML={{ __html: branding.headerHtml }} />;
42
+ return DOMPurify.sanitize(html, { FORBID_TAGS: FORBIDDEN_TAGS, FORBID_ATTR: FORBIDDEN_ATTRS });
18
43
  }
19
44
 
20
- export function BrandingFooter({ branding }: { branding: Branding | null }) {
21
- if (!branding?.footerHtml) {
45
+ function BrandingHtml({ html }: { html: string | undefined }) {
46
+ const clean = html ? sanitizeBrandingHtml(html) : "";
47
+ if (!clean) {
22
48
  return null;
23
49
  }
24
- return <div dangerouslySetInnerHTML={{ __html: branding.footerHtml }} />;
50
+ return <div dangerouslySetInnerHTML={{ __html: clean }} />;
51
+ }
52
+
53
+ /**
54
+ * Renders the admin-configured `Branding.headerHtml`/`footerHtml` (see `useBranding()`), sanitized - shared by
55
+ * the webmail and admin chromes. The escrow console deliberately renders no branding HTML at all.
56
+ */
57
+ export function BrandingHeader({ branding }: { branding: Branding | null }) {
58
+ return <BrandingHtml html={branding?.headerHtml} />;
59
+ }
60
+
61
+ export function BrandingFooter({ branding }: { branding: Branding | null }) {
62
+ return <BrandingHtml html={branding?.footerHtml} />;
25
63
  }