@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
@@ -14,7 +14,32 @@ const ACTION_LABELS = {
14
14
  "escrow_access_request.created": "Access request created",
15
15
  "escrow_access_request.approved": "Access request approved",
16
16
  "escrow_access_request.material_read": "Material read",
17
+ "matter_export.requested": "Matter export requested",
18
+ "matter_export.ready": "Matter export ready",
19
+ "matter_export.failed": "Matter export failed",
17
20
  };
21
+ /** The label for an audit action - the raw action name for one this console doesn't know yet (a newer server). */
22
+ export function actionLabel(action) {
23
+ return ACTION_LABELS[action] ?? action;
24
+ }
25
+ const VERIFICATION_FAILURES = {
26
+ link_mismatch: "An entry was deleted, inserted or reordered.",
27
+ hash_mismatch: "An entry was edited after it was written.",
28
+ unknown_algorithm: "An entry uses a hash algorithm this server doesn't recognize.",
29
+ algorithm_downgrade: "An unkeyed entry follows keyed ones, which suggests a forged entry.",
30
+ hmac_key_unavailable: "Audit key not configured: this server can't check keyed entries, so the chain couldn't be verified.",
31
+ truncated: "Entries missing at the end: entries were deleted from the end of the chain.",
32
+ head_mismatch: "The chain's head record doesn't match its last entry.",
33
+ head_mac_mismatch: "The chain's head record was forged or edited.",
34
+ head_missing: "The chain's head record is missing.",
35
+ };
36
+ /** Explains a failed chain verification by its `reason` (plus where it broke, when known). An unrecognized or missing
37
+ * reason (a newer or older server) reads as a generic failure. */
38
+ export function describeVerificationFailure(result) {
39
+ const where = result.brokenAtSequence === undefined ? "" : ` (at sequence ${result.brokenAtSequence})`;
40
+ const known = result.reason === undefined ? undefined : VERIFICATION_FAILURES[result.reason];
41
+ return known ? `Chain integrity check failed${where}. ${known}` : `Chain integrity check failed${where}.`;
42
+ }
18
43
  export default function EscrowAuditLogPage(props) {
19
44
  return (_jsx(EscrowShell, { ...props, active: "auditLog", children: _jsx(EscrowAuditLogContent, {}) }));
20
45
  }
@@ -62,5 +87,5 @@ function EscrowAuditLogContent() {
62
87
  }
63
88
  const hasNextPage = entries.length === PAGE_SIZE;
64
89
  return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between mb-5", children: [_jsx("h1", { className: "text-xl font-bold uppercase tracking-wide", children: "Escrow audit log" }), isTrustedAdmin && (_jsx(Button, { type: "button", className: "!w-auto", loading: verifying, disabled: verifying, onClick: handleVerify, children: "Verify chain integrity" }))] }), _jsx("p", { className: "text-sm text-text-muted mb-5", children: "Every escrow access lifecycle event visible to you \u2014 matters under a scope you hold. A trusted administrator sees every entry across every scope instead." }), verifyError && _jsx(Alert, { children: verifyError }), verifyResult &&
65
- (verifyResult.valid ? (_jsx("div", { className: "mb-5 py-3 px-3.5 rounded-sm text-sm bg-success/10 text-success font-medium", children: "Chain verified \u2014 no tampering detected." })) : (_jsxs(Alert, { children: ["Chain integrity broken at sequence ", verifyResult.brokenAtSequence, "."] }))), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No audit log entries visible to you yet." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Sequence", "Action", "Holder", "Mailbox", "Occurred"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: entries.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.sequence }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: ACTION_LABELS[entry.action] }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.holderUserUid }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.mailboxUid }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: new Date(entry.occurredAt).toLocaleString() })] }, entry.uid))) })] }) })), _jsxs("div", { className: "flex gap-3 items-center mt-4", children: [_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: page === 0 || loading, onClick: () => setPage((p) => Math.max(0, p - 1)), children: "Previous" }), _jsxs("span", { className: "text-sm text-text-muted", children: ["Page ", page + 1] }), _jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: !hasNextPage || loading, onClick: () => setPage((p) => p + 1), children: "Next" })] })] }));
90
+ (verifyResult.valid ? (_jsx("div", { className: "mb-5 py-3 px-3.5 rounded-sm text-sm bg-success/10 text-success font-medium", children: "Chain verified \u2014 no tampering detected." })) : (_jsx(Alert, { children: describeVerificationFailure(verifyResult) }))), error && _jsx(Alert, { children: error }), loading ? (_jsx("p", { className: "text-sm text-text-muted", children: "Loading\u2026" })) : entries.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No audit log entries visible to you yet." })) : (_jsx("div", { className: "overflow-x-auto", children: _jsxs("table", { className: "w-full text-sm border-collapse", children: [_jsx("thead", { children: _jsx("tr", { children: ["Sequence", "Action", "Holder", "Mailbox", "Occurred"].map((h) => (_jsx("th", { className: "text-left text-xs uppercase tracking-wide text-text-muted py-2 px-2.5 border-b border-border", children: h }, h))) }) }), _jsx("tbody", { children: entries.map((entry) => (_jsxs("tr", { children: [_jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.sequence }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: actionLabel(entry.action) }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.holderUserUid }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: entry.mailboxUid }), _jsx("td", { className: "py-2.5 px-2.5 border-b border-border", children: new Date(entry.occurredAt).toLocaleString() })] }, entry.uid))) })] }) })), _jsxs("div", { className: "flex gap-3 items-center mt-4", children: [_jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: page === 0 || loading, onClick: () => setPage((p) => Math.max(0, p - 1)), children: "Previous" }), _jsxs("span", { className: "text-sm text-text-muted", children: ["Page ", page + 1] }), _jsx(Button, { variant: "secondary", type: "button", className: "!w-auto", disabled: !hasNextPage || loading, onClick: () => setPage((p) => p + 1), children: "Next" })] })] }));
66
91
  }
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { EscrowShellProps } from "../shared/components/escrow/layout/EscrowShell.js";
3
+ export default function MattersPage(props: Omit<EscrowShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { EscrowShellProps } from "../../shared/components/escrow/layout/EscrowShell.js";
3
+ export default function MatterDetailPage(props: Omit<EscrowShellProps, "active"> & {
4
+ params: {
5
+ uid: string;
6
+ };
7
+ }): React.JSX.Element;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  // Copyright (C) 2026 Jean-Philippe Steinmetz
4
4
  // SPDX-License-Identifier: MPL-2.0
5
5
  ///////////////////////////////////////////////////////////////////////////////
6
- import { useEffect, useState } from "react";
6
+ import { useEffect, useRef, useState } from "react";
7
7
  import { ApiRequestError } from "@rapidmx/react-shared/util/api.js";
8
8
  import { closeMatter, getMatter } from "@rapidmx/react-shared/admin/mattersApi.js";
9
9
  import { approveAccessRequest, createAccessRequest, denyAccessRequest, getAccessRequestMaterial, listAccessRequests, } from "@rapidmx/react-shared/admin/escrowAccessRequestsApi.js";
@@ -11,35 +11,44 @@ import { createMatterExportRequest, listMatterExportRequests, matterExportReques
11
11
  import { searchMatter } from "@rapidmx/react-shared/admin/matterSearchApi.js";
12
12
  import { parseSearchQuery } from "@rapidmx/react-shared/search/queryGrammar.js";
13
13
  import EscrowShell from "../../shared/components/escrow/layout/EscrowShell.js";
14
+ import { LoadMoreButton, usePagedList } from "../../shared/components/admin/usePagedList.js";
14
15
  import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
15
16
  import Button from "@rapidmx/react-shared/components/buttons/Button.js";
16
17
  import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
17
18
  import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
18
19
  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";
19
- // `BaseEscrowAccessRequestRoute.find()` always returns every request under every scope the caller holds
20
- // (it unconditionally overwrites any `matterId` filter with its own held-scopes-derived one — see
21
- // `escrowAccessRequestsApi.ts`'s own doc comment) — there is no server-side way to ask for just this one
22
- // matter's requests. This page fetches a generously-sized single page and filters client-side instead of
23
- // paginating a second, matter-scoped list UI on top of an already-scoped one; a holder with more than this
24
- // many *total* in-flight requests across every matter they hold is not the common case this v1 targets.
25
- const REQUESTS_FETCH_LIMIT = 200;
20
+ /** Request statuses still waiting or running - shown as a neutral in-progress badge, never as success. Includes
21
+ * restapi's `processing` (exports/imports) and `in_progress` (an erasure that is running). */
22
+ const IN_PROGRESS_STATUSES = new Set(["pending", "processing", "in_progress"]);
26
23
  function statusBadgeClass(status) {
27
- if (status === "denied" || status === "failed")
24
+ if (status === "failed" || status === "denied")
28
25
  return "bg-danger-bg text-danger";
29
- if (status === "pending")
26
+ if (IN_PROGRESS_STATUSES.has(status))
30
27
  return "bg-surface-alt text-text-muted";
31
28
  return "bg-success text-white";
32
29
  }
30
+ /** A status as shown on its badge (`in_progress` -> `in progress`). */
31
+ function statusLabel(status) {
32
+ return status.replace(/_/g, " ");
33
+ }
33
34
  export default function MatterDetailPage(props) {
34
35
  return (_jsx(EscrowShell, { ...props, active: "matters", children: _jsx(MatterDetailContent, { uid: props.params.uid }) }));
35
36
  }
37
+ function formatDateRange(matter) {
38
+ return `${new Date(matter.dateRangeStart).toLocaleDateString()} – ${new Date(matter.dateRangeEnd).toLocaleDateString()}`;
39
+ }
36
40
  function MatterDetailContent({ uid }) {
37
41
  const [matter, setMatter] = useState(null);
38
- const [requests, setRequests] = useState([]);
39
42
  const [loading, setLoading] = useState(true);
40
43
  const [error, setError] = useState(null);
44
+ // Both lists are fetched for this matter only (`matterId`), newest first, a page at a time. Rows are still
45
+ // checked against this matter in case an older server ignores the filter.
46
+ const requests = usePagedList((params) => listAccessRequests({ ...params, matterId: uid }), "Could not load this matter's access requests.");
47
+ const exportRequests = usePagedList((params) => listMatterExportRequests({ ...params, matterId: uid }), "Could not load this matter's export requests.");
48
+ const [confirmingClose, setConfirmingClose] = useState(false);
41
49
  const [closing, setClosing] = useState(false);
42
50
  const [closeError, setCloseError] = useState(null);
51
+ const [approveTarget, setApproveTarget] = useState(null);
43
52
  const [showNewRequest, setShowNewRequest] = useState(false);
44
53
  const [newRequestMailboxUid, setNewRequestMailboxUid] = useState("");
45
54
  const [creatingRequest, setCreatingRequest] = useState(false);
@@ -51,41 +60,44 @@ function MatterDetailContent({ uid }) {
51
60
  const [materialText, setMaterialText] = useState("");
52
61
  const [materialError, setMaterialError] = useState(null);
53
62
  const [loadingMaterial, setLoadingMaterial] = useState(false);
54
- const [exportRequests, setExportRequests] = useState([]);
63
+ // Bumped whenever the material modal closes (or the matter does), so a slow "Get material" response for a
64
+ // modal that's already gone - or for an earlier request - is never shown.
65
+ const materialSeq = useRef(0);
55
66
  const [creatingExport, setCreatingExport] = useState(false);
56
67
  const [exportError, setExportError] = useState(null);
57
68
  const [searchText, setSearchText] = useState("");
58
69
  const [searching, setSearching] = useState(false);
59
70
  const [searchError, setSearchError] = useState(null);
60
71
  const [searchResults, setSearchResults] = useState(null);
61
- // Same "no server-side single-matter filter, fetch a generous page and filter client-side" shape
62
- // REQUESTS_FETCH_LIMIT's own doc comment already establishes for access requests -
63
- // listMatterExportRequests() has the identical held-scopes-derived scoping, not a per-matter one.
64
- function reload() {
72
+ // Bumped by every search and by closing the matter, so a search still in flight when the matter closes (or
73
+ // superseded by a newer search) never shows its results.
74
+ const searchSeq = useRef(0);
75
+ useEffect(() => {
65
76
  setLoading(true);
66
77
  setError(null);
67
- Promise.all([getMatter(uid), listAccessRequests({ limit: REQUESTS_FETCH_LIMIT }), listMatterExportRequests()])
68
- .then(([loadedMatter, allRequests, allExportRequests]) => {
69
- setMatter(loadedMatter);
70
- setRequests(allRequests.filter((r) => r.matterId === uid));
71
- setExportRequests(allExportRequests.filter((r) => r.matterId === uid));
72
- })
78
+ getMatter(uid)
79
+ .then(setMatter)
73
80
  .catch((err) => setError(err instanceof ApiRequestError ? err.message : "Could not load this matter."))
74
81
  .finally(() => setLoading(false));
75
- }
76
- useEffect(() => {
77
- reload();
82
+ void requests.reload();
83
+ void exportRequests.reload();
78
84
  }, [uid]);
79
- // Only ever invoked from the "Close matter" button below, which itself only renders once `matter` is
85
+ // Only ever invoked from the close-confirmation modal below, which itself only renders once `matter` is
80
86
  // resolved (children only render once loaded — see the early returns further down) — the non-null
81
- // assertion reflects that real invariant, not an unchecked assumption. Matches `DomainDetailContent.
82
- // handleVerify`'s identical pattern.
87
+ // assertion reflects that real invariant, not an unchecked assumption.
83
88
  async function handleClose() {
84
89
  setClosing(true);
85
90
  setCloseError(null);
86
91
  try {
87
92
  const updated = await closeMatter(matter.uid);
88
93
  setMatter(updated);
94
+ setConfirmingClose(false);
95
+ // Nothing more may be read under a closed matter - drop any material or search results already on
96
+ // screen, including a search still in flight, and any approval still being confirmed.
97
+ closeMaterialModal();
98
+ searchSeq.current++;
99
+ setSearchResults(null);
100
+ setSearching(false);
89
101
  }
90
102
  catch (err) {
91
103
  setCloseError(err instanceof ApiRequestError ? err.message : "Could not close this matter.");
@@ -94,6 +106,10 @@ function MatterDetailContent({ uid }) {
94
106
  setClosing(false);
95
107
  }
96
108
  }
109
+ function closeCloseModal() {
110
+ setConfirmingClose(false);
111
+ setCloseError(null);
112
+ }
97
113
  async function handleCreateRequest(e) {
98
114
  e.preventDefault();
99
115
  setNewRequestError(null);
@@ -106,7 +122,7 @@ function MatterDetailContent({ uid }) {
106
122
  await createAccessRequest({ matterId: uid, mailboxUid: newRequestMailboxUid.trim() });
107
123
  setShowNewRequest(false);
108
124
  setNewRequestMailboxUid("");
109
- reload();
125
+ void requests.reload();
110
126
  }
111
127
  catch (err) {
112
128
  setNewRequestError(err instanceof ApiRequestError ? err.message : "Could not create this access request.");
@@ -115,12 +131,15 @@ function MatterDetailContent({ uid }) {
115
131
  setCreatingRequest(false);
116
132
  }
117
133
  }
118
- async function handleApprove(request) {
134
+ // Only ever invoked from the approve-confirmation modal below, which only renders once `approveTarget` is set.
135
+ async function handleApprove() {
136
+ const request = approveTarget;
137
+ setApproveTarget(null);
119
138
  setActingOn((prev) => ({ ...prev, [request.uid]: true }));
120
139
  setActionErrors((prev) => ({ ...prev, [request.uid]: "" }));
121
140
  try {
122
141
  const updated = await approveAccessRequest(request.uid);
123
- setRequests((prev) => prev.map((r) => (r.uid === updated.uid ? updated : r)));
142
+ requests.replaceItem(updated);
124
143
  }
125
144
  catch (err) {
126
145
  setActionErrors((prev) => ({
@@ -137,7 +156,7 @@ function MatterDetailContent({ uid }) {
137
156
  setActionErrors((prev) => ({ ...prev, [request.uid]: "" }));
138
157
  try {
139
158
  const updated = await denyAccessRequest(request.uid);
140
- setRequests((prev) => prev.map((r) => (r.uid === updated.uid ? updated : r)));
159
+ requests.replaceItem(updated);
141
160
  }
142
161
  catch (err) {
143
162
  setActionErrors((prev) => ({
@@ -150,30 +169,39 @@ function MatterDetailContent({ uid }) {
150
169
  }
151
170
  }
152
171
  async function handleGetMaterial(request) {
172
+ const seq = ++materialSeq.current;
153
173
  setMaterialFor(request);
154
174
  setMaterialText("");
155
175
  setMaterialError(null);
156
176
  setLoadingMaterial(true);
157
177
  try {
158
178
  const material = await getAccessRequestMaterial(request.uid);
159
- setMaterialText(JSON.stringify(material.masterKeyWraps, null, 2));
179
+ if (seq === materialSeq.current)
180
+ setMaterialText(JSON.stringify(material.masterKeyWraps, null, 2));
160
181
  }
161
182
  catch (err) {
162
- setMaterialError(err instanceof ApiRequestError ? err.message : "Could not read this request's material.");
183
+ if (seq === materialSeq.current) {
184
+ setMaterialError(err instanceof ApiRequestError ? err.message : "Could not read this request's material.");
185
+ }
163
186
  }
164
187
  finally {
165
- setLoadingMaterial(false);
188
+ if (seq === materialSeq.current)
189
+ setLoadingMaterial(false);
166
190
  }
167
191
  }
168
192
  function closeMaterialModal() {
193
+ materialSeq.current++;
169
194
  setMaterialFor(null);
195
+ setMaterialText("");
196
+ setMaterialError(null);
197
+ setLoadingMaterial(false);
170
198
  }
171
199
  async function handleCreateExport() {
172
200
  setExportError(null);
173
201
  setCreatingExport(true);
174
202
  try {
175
203
  await createMatterExportRequest(uid);
176
- reload();
204
+ void exportRequests.reload();
177
205
  }
178
206
  catch (err) {
179
207
  setExportError(err instanceof ApiRequestError ? err.message : "Could not start this export.");
@@ -184,6 +212,7 @@ function MatterDetailContent({ uid }) {
184
212
  }
185
213
  async function handleSearch(e) {
186
214
  e.preventDefault();
215
+ const seq = ++searchSeq.current;
187
216
  setSearchError(null);
188
217
  setSearching(true);
189
218
  try {
@@ -207,13 +236,17 @@ function MatterDetailContent({ uid }) {
207
236
  flags: parsed.flags,
208
237
  labels: parsed.labels,
209
238
  });
210
- setSearchResults(results);
239
+ if (seq === searchSeq.current)
240
+ setSearchResults(results);
211
241
  }
212
242
  catch (err) {
213
- setSearchError(err instanceof ApiRequestError ? err.message : "Could not search this matter.");
243
+ if (seq === searchSeq.current) {
244
+ setSearchError(err instanceof ApiRequestError ? err.message : "Could not search this matter.");
245
+ }
214
246
  }
215
247
  finally {
216
- setSearching(false);
248
+ if (seq === searchSeq.current)
249
+ setSearching(false);
217
250
  }
218
251
  }
219
252
  if (loading) {
@@ -222,13 +255,15 @@ function MatterDetailContent({ uid }) {
222
255
  if (error || !matter) {
223
256
  return _jsx(Alert, { children: error ?? "Matter not found." });
224
257
  }
225
- return (_jsxs("div", { className: "max-w-4xl flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/escrow", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All matters" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: matter.name })] }), !matter.closedAt && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", loading: closing, disabled: closing, onClick: handleClose, children: "Close matter" }))] }), closeError && _jsx(Alert, { children: closeError }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: matter.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "font-mono text-xs", children: matter.escrowScopeId }), _jsx("dt", { className: "text-text-muted", children: "Custodian mailboxes" }), _jsx("dd", { children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsxs("dd", { children: [new Date(matter.dateRangeStart).toLocaleDateString(), " \u2013", " ", new Date(matter.dateRangeEnd).toLocaleDateString()] }), _jsx("dt", { className: "text-text-muted", children: "Status" }), _jsx("dd", { children: matter.closedAt ? `Closed ${new Date(matter.closedAt).toLocaleString()}` : "Open" })] }) }), _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: "Access requests" }), !matter.closedAt && (_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setShowNewRequest(true), children: "+ New access request" }))] }), requests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No access requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-3", children: requests.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-semibold", children: request.mailboxUid }), _jsxs("div", { className: "text-xs text-text-muted", children: [request.approvals.length, " of ", request.requiredHoldersAtCreation, " approvals \u00B7 requested by ", request.requestedByUserUid] })] }), _jsx("span", { className: [
258
+ const matterRequests = requests.items.filter((request) => request.matterId === uid);
259
+ const matterExportRequests = exportRequests.items.filter((request) => request.matterId === uid);
260
+ return (_jsxs("div", { className: "max-w-4xl flex flex-col gap-5", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("a", { href: "/escrow", className: "text-sm text-primary-dark hover:underline", children: "\u2190 All matters" }), _jsx("h1", { className: "text-xl font-bold tracking-tight mt-1", children: matter.name })] }), !matter.closedAt && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto shrink-0", onClick: () => setConfirmingClose(true), children: "Close matter" }))] }), _jsx("div", { className: "bg-surface border border-border rounded-md p-6", children: _jsxs("dl", { className: "grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 text-sm", children: [_jsx("dt", { className: "text-text-muted", children: "Description" }), _jsx("dd", { children: matter.description ?? "None" }), _jsx("dt", { className: "text-text-muted", children: "Escrow scope" }), _jsx("dd", { className: "font-mono text-xs", children: matter.escrowScopeId }), _jsx("dt", { className: "text-text-muted", children: "Custodian mailboxes" }), _jsx("dd", { children: matter.custodianMailboxUids.join(", ") }), _jsx("dt", { className: "text-text-muted", children: "Date range" }), _jsx("dd", { children: formatDateRange(matter) }), _jsx("dt", { className: "text-text-muted", children: "Status" }), _jsx("dd", { children: matter.closedAt ? `Closed ${new Date(matter.closedAt).toLocaleString()}` : "Open" })] }) }), _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: "Access requests" }), !matter.closedAt && (_jsx(Button, { type: "button", className: "!w-auto", onClick: () => setShowNewRequest(true), children: "+ New access request" }))] }), requests.loadError && _jsx(Alert, { children: requests.loadError }), matterRequests.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: requests.loading ? "Loading…" : "No access requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-3", children: matterRequests.map((request) => (_jsxs("li", { className: "border border-border rounded-sm p-4", children: [_jsxs("div", { className: "flex items-center justify-between gap-3 flex-wrap", children: [_jsxs("div", { children: [_jsx("div", { className: "text-sm font-semibold", children: request.mailboxUid }), _jsxs("div", { className: "text-xs text-text-muted", children: [request.approvals.length, " of ", request.requiredHoldersAtCreation, " approvals \u00B7 requested by ", request.requestedByUserUid] })] }), _jsx("span", { className: [
226
261
  "text-xs font-bold uppercase tracking-wide py-0.5 px-2 rounded-pill",
227
262
  request.status === "denied"
228
263
  ? "bg-danger-bg text-danger"
229
264
  : request.status === "pending"
230
265
  ? "bg-surface-alt text-text-muted"
231
266
  : "bg-success text-white",
232
- ].join(" "), children: request.status })] }), actionErrors[request.uid] && _jsx(Alert, { children: actionErrors[request.uid] }), _jsxs("div", { className: "flex gap-3 mt-3", children: [request.status === "pending" && (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", className: "!w-auto", loading: actingOn[request.uid], disabled: actingOn[request.uid], onClick: () => handleApprove(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" })] })), (request.status === "approved" || request.status === "fulfilled") && (_jsx(Button, { type: "button", variant: "secondary", className: "!w-auto", onClick: () => handleGetMaterial(request), children: "Get material" }))] })] }, request.uid))) }))] }), _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" }), _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.length === 0 ? (_jsx("p", { className: "text-sm text-text-muted", children: "No export requests yet." })) : (_jsx("ul", { className: "flex flex-col gap-2", children: exportRequests.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: request.status }), 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" }), _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 &&
233
- (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))) })))] }), _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 })] })) })] }));
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 })] })) })] }));
234
269
  }
@@ -0,0 +1,3 @@
1
+ import React from "react";
2
+ import { EscrowShellProps } from "../../../shared/components/escrow/layout/EscrowShell.js";
3
+ export default function NewMatterPage(props: Omit<EscrowShellProps, "active">): React.JSX.Element;
@@ -0,0 +1,17 @@
1
+ import React from "react";
2
+ import { DistributionList } from "@rapidmx/react-shared/admin/distributionListsApi.js";
3
+ export interface MemberListCardProps {
4
+ list: DistributionList;
5
+ /** Called with the freshly-saved list (new `version`, updated `memberAddresses`) after a successful
6
+ * add/remove, so the parent detail page can keep its own copy of the list in sync — mirrors
7
+ * `ShareAccessCard`'s `reload()` pattern, just pushed up rather than re-fetched, since the caller
8
+ * already has everything the response would return. */
9
+ onUpdate: (list: DistributionList) => void;
10
+ }
11
+ /**
12
+ * Lets an admin view and edit a distribution list's members — same load/inline-add-row/per-row-remove
13
+ * interaction shape as `ShareAccessCard`, but against a plain `memberAddresses: string[]` field on the
14
+ * `DistributionList` itself via read-modify-write `PUT`, not a separate ACL endpoint (a distribution
15
+ * list has no per-record ACL to speak of — see `BaseDistributionListRoute`'s own doc comment).
16
+ */
17
+ export default function MemberListCard({ list, onUpdate }: MemberListCardProps): React.JSX.Element;
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ /** The part of an `EscrowScope` shared by the "new" and "[uid]" pages — everything except `name`/
3
+ * `description`, which stay plain inline `FormField`s on each page (same split `distribution-lists`
4
+ * already uses: simple top-level fields inline, the more involved sub-structure factored out). An admin
5
+ * pastes in the fields of an already-issued certificate here; nothing in this app generates a keypair —
6
+ * see `EscrowScopePublicKey`'s own doc comment in `escrowScopesApi.ts`. */
7
+ export interface EscrowScopeKeyAndHoldersValue {
8
+ publicKey: string;
9
+ keyType: string;
10
+ fingerprint: string;
11
+ /** `<input type="datetime-local">` value — see `toDatetimeLocal()`. */
12
+ notBefore: string;
13
+ notAfter: string;
14
+ holderUserUids: string[];
15
+ requiredHolders: number;
16
+ notifySubjectOnAccess: boolean;
17
+ }
18
+ export interface EscrowScopeKeyAndHoldersFieldsProps {
19
+ value: EscrowScopeKeyAndHoldersValue;
20
+ onChange: (value: EscrowScopeKeyAndHoldersValue) => void;
21
+ disabled?: boolean;
22
+ /** Shows the public key fields without letting them be edited, e.g. for keys generated in the browser. */
23
+ keyReadOnly?: boolean;
24
+ }
25
+ /** Builds an all-zeroes starting point for the "new" page — `notBefore`/`notAfter` default to "now" so an
26
+ * admin pasting in a cert's own dates only has to touch what actually differs. */
27
+ export declare function emptyEscrowScopeKeyAndHoldersValue(): EscrowScopeKeyAndHoldersValue;
28
+ /** The message shown when an administrator tries to make themselves a holder: whoever configures a scope must not
29
+ * also be able to approve access under it (separation of duties - the server refuses it too). Holders who were
30
+ * already on the scope (`existingHolderUserUids`) aren't treated as newly added. */
31
+ export declare function selfAsHolderError(holderUserUids: string[], adminUid: string | undefined, existingHolderUserUids?: string[]): string | null;
32
+ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabled, keyReadOnly }: EscrowScopeKeyAndHoldersFieldsProps): React.JSX.Element;
@@ -18,9 +18,18 @@ export function emptyEscrowScopeKeyAndHoldersValue() {
18
18
  notifySubjectOnAccess: false,
19
19
  };
20
20
  }
21
- export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabled }) {
21
+ /** The message shown when an administrator tries to make themselves a holder: whoever configures a scope must not
22
+ * also be able to approve access under it (separation of duties - the server refuses it too). Holders who were
23
+ * already on the scope (`existingHolderUserUids`) aren't treated as newly added. */
24
+ export function selfAsHolderError(holderUserUids, adminUid, existingHolderUserUids = []) {
25
+ if (!adminUid || !holderUserUids.includes(adminUid) || existingHolderUserUids.includes(adminUid)) {
26
+ return null;
27
+ }
28
+ return "You can't add yourself as a holder of an escrow scope you configure. Ask another administrator to add you, or choose different holders.";
29
+ }
30
+ export default function EscrowScopeKeyAndHoldersFields({ value, onChange, disabled, keyReadOnly }) {
22
31
  function set(key, next) {
23
32
  onChange({ ...value, [key]: next });
24
33
  }
25
- 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, 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, 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, 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, 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, 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(StringListField, { label: "Holder user uids", id: "holderUserUids", values: value.holderUserUids, onChange: (holderUserUids) => set("holderUserUids", holderUserUids), placeholder: "User uid to add", 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"] })] })] }));
34
+ 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(StringListField, { label: "Holder user uids", id: "holderUserUids", values: value.holderUserUids, onChange: (holderUserUids) => set("holderUserUids", holderUserUids), placeholder: "User uid to add", 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"] })] })] }));
26
35
  }
@@ -0,0 +1,32 @@
1
+ import "../../../styles/app.css";
2
+ import React, { PropsWithChildren } from "react";
3
+ import { NavItem } from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
4
+ import { PluginNav, PluginNavProps } from "../../../plugins/pluginNav.js";
5
+ export type AdminSection = "mailboxes" | "quarantine" | "ingestQueue" | "domains" | "auditLog" | "distributionLists" | "transportRules" | "escrowScopes" | "retentionPolicy" | "encryptionPolicy" | "mailboxPolicy" | "setup" | "dataRequests" | "plugins" | "branding";
6
+ /** A core `AdminSection`, or a plugin's `adminNav` item id (see `PluginNav`). */
7
+ export type AdminShellActive = AdminSection | (string & {});
8
+ export interface AdminShellProps extends PluginNavProps {
9
+ /** Which icon in the rail is highlighted as the current section - a plugin admin page passes its own
10
+ * `adminNav` item id. */
11
+ active: AdminShellActive;
12
+ /** Populated automatically by the framework from an authenticated request (e.g. a valid `jwt` cookie). */
13
+ userUid?: string;
14
+ /** auth-server's base URL, injected via the route's `fetchProps` — see `src/mongo/routes/AdminConsoleRoute.ts`. */
15
+ authServerUrl?: string;
16
+ /**
17
+ * Where `mailApi.ts`'s `impersonateUser()`/`stopImpersonating()` should call: the real auth-server in
18
+ * production, or `""` under `yarn dev` to call this app's own local dev-only impersonation endpoint
19
+ * instead — see `AdminConsoleRoute`'s `fetchProps` and `src/dev/DevImpersonationRoute.ts`.
20
+ */
21
+ impersonationBaseUrl?: string;
22
+ }
23
+ /** `NAV_ITEMS` followed by the plugins' `adminNav` items (generic icon). Core ids win, including the
24
+ * off-rail sections' - see `mergePluginNavItems`. */
25
+ export declare function adminNavItems(pluginNav?: PluginNav): NavItem[];
26
+ /**
27
+ * Gates every `apps/admin` page behind the `admin` trusted role. Uses `GET /api/admin/release-notes` (any
28
+ * `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a canary: a 200 means the
29
+ * caller's JWT carries a trusted role, a 403 means it doesn't. There is no local step-up/elevation flow
30
+ * (that would need a cross-origin call to auth-server's own elevation endpoint — not wired up yet).
31
+ */
32
+ export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }: PropsWithChildren<AdminShellProps>): React.JSX.Element;
@@ -14,6 +14,8 @@ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
14
14
  import BottomTabBar from "@rapidmx/react-shared/components/navigation/BottomTabBar.js";
15
15
  import { BrandingFooter, BrandingHeader } from "../../layout/BrandingChrome.js";
16
16
  import UserMenu from "../../layout/UserMenu.js";
17
+ import { signOutOfConsole } from "../signOut.js";
18
+ import { mergePluginNavItems } from "../../../plugins/pluginNav.js";
17
19
  /** Sections shown in the persistent icon rail / mobile tab bar — every admin area reachable from
18
20
  * anywhere in the console. Deliberately excludes `quarantine`/`ingestQueue`: those are scoped to a
19
21
  * single mailbox and only ever reached via links on that mailbox's own detail page (see
@@ -75,14 +77,19 @@ const MAILBOX_SCOPED_ITEMS = [
75
77
  ];
76
78
  /** The setup wizard - reached by redirect or from the Mailboxes page, not from the rail. */
77
79
  const SETUP_ITEM = { id: "setup", href: "/admin/setup", label: "Setup", icon: HiOutlineRocketLaunch };
78
- const ALL_ITEMS = [...NAV_ITEMS, ...MAILBOX_SCOPED_ITEMS, SETUP_ITEM];
80
+ const OFF_RAIL_ITEMS = [...MAILBOX_SCOPED_ITEMS, SETUP_ITEM];
81
+ /** `NAV_ITEMS` followed by the plugins' `adminNav` items (generic icon). Core ids win, including the
82
+ * off-rail sections' - see `mergePluginNavItems`. */
83
+ export function adminNavItems(pluginNav) {
84
+ return mergePluginNavItems(NAV_ITEMS, pluginNav?.adminNav, ({ id, href, label }) => ({ id, href, label, icon: HiOutlinePuzzlePiece }), OFF_RAIL_ITEMS.map((item) => item.id));
85
+ }
79
86
  /**
80
87
  * Gates every `apps/admin` page behind the `admin` trusted role. Uses `GET /api/admin/release-notes` (any
81
88
  * `BaseAdminRoute` endpoint works — this one is side-effect-free) purely as a canary: a 200 means the
82
89
  * caller's JWT carries a trusted role, a 403 means it doesn't. There is no local step-up/elevation flow
83
90
  * (that would need a cross-origin call to auth-server's own elevation endpoint — not wired up yet).
84
91
  */
85
- export default function AdminShell({ active, userUid, authServerUrl, children }) {
92
+ export default function AdminShell({ active, userUid, authServerUrl, pluginNav, children }) {
86
93
  const [status, setStatus] = useState("checking");
87
94
  const [error, setError] = useState(null);
88
95
  const { branding, iconSrc } = useBranding();
@@ -118,7 +125,8 @@ export default function AdminShell({ active, userUid, authServerUrl, children })
118
125
  });
119
126
  }, [userUid]);
120
127
  function handleSignOut() {
121
- window.location.href = authServerUrl ?? "/";
128
+ // Ends the auth-server session and tells other tabs, not just navigates - see `signOutOfConsole()`.
129
+ void signOutOfConsole(authServerUrl);
122
130
  }
123
131
  let content;
124
132
  if (!userUid || status === "checking") {
@@ -131,13 +139,14 @@ export default function AdminShell({ active, userUid, authServerUrl, children })
131
139
  content = (_jsx("div", { className: "min-h-screen flex items-center justify-center p-8", children: _jsx("div", { className: "w-full max-w-md", children: _jsx(Alert, { children: error }) }) }));
132
140
  }
133
141
  else {
134
- const activeItem = ALL_ITEMS.find((item) => item.id === active);
135
- content = (_jsx("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Admin sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), NAV_ITEMS.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
142
+ const navItems = adminNavItems(pluginNav);
143
+ const activeItem = [...navItems, ...OFF_RAIL_ITEMS].find((item) => item.id === active);
144
+ content = (_jsx("div", { className: "min-h-screen flex flex-col bg-surface-alt", children: _jsxs("div", { className: "flex-1 flex min-h-0", children: [_jsxs("nav", { "aria-label": "Admin sections", className: "hidden md:flex w-16 shrink-0 bg-surface border-r border-border flex-col items-center py-3 gap-1", children: [_jsx("img", { src: iconSrc, width: "96", height: "96", alt: "", className: "mb-3" }), navItems.map(({ id, href, label, icon: Icon }) => (_jsx("a", { href: href, "aria-label": label, "aria-current": id === active ? "page" : undefined, title: label, className: [
136
145
  "w-10 h-10 flex items-center justify-center rounded-sm",
137
146
  id === active
138
147
  ? "bg-primary/10 text-primary-dark"
139
148
  : "text-text-muted hover:bg-surface-alt hover:text-text",
140
- ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: NAV_ITEMS, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
149
+ ].join(" "), children: _jsx(Icon, { size: 20, "aria-hidden": "true" }) }, id)))] }), _jsx(BottomTabBar, { apps: navItems, active: active }), _jsxs("div", { className: "flex-1 flex flex-col min-w-0", children: [_jsxs("header", { className: "h-16 shrink-0 bg-surface border-b border-border flex items-center justify-between gap-4 px-6", children: [_jsx("span", { className: "font-display font-bold text-lg uppercase tracking-wide", children: activeItem?.label }), _jsx(UserMenu, { userUid: userUid, authServerUrl: authServerUrl, onSignOut: handleSignOut })] }), _jsx("div", { className: "flex-1 pb-14 md:pb-0", children: _jsx("main", { className: "max-w-6xl mx-auto px-6 py-8", children: children }) })] })] }) }));
141
150
  }
142
151
  return (_jsxs(_Fragment, { children: [_jsx(BrandingHeader, { branding: branding }), content, _jsx(BrandingFooter, { branding: branding })] }));
143
152
  }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+ import { Mailbox } from "@rapidmx/react-shared/mail/mailApi.js";
3
+ export interface EscrowScopeCardProps {
4
+ mailbox: Mailbox;
5
+ /** Called with the saved mailbox, same push-up pattern as `ResourceSettingsCard`. */
6
+ onUpdate: (mailbox: Mailbox) => void;
7
+ }
8
+ /**
9
+ * Assigns a mailbox to an escrow scope (or none), which decides whose holders can recover its encrypted mail. Only
10
+ * shown in the admin console, whose trusted-role gate the server enforces again for `escrowScopeId`.
11
+ */
12
+ export default function EscrowScopeCard({ mailbox, onUpdate }: EscrowScopeCardProps): React.JSX.Element;