@rapidmx/web-client 0.35.0 → 0.37.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 (268) hide show
  1. package/apps/escrow/matters/[uid].tsx +4 -3
  2. package/apps/shared/calendar/calendarReminders.ts +35 -0
  3. package/apps/shared/calendar/sealedEvents.ts +113 -0
  4. package/apps/shared/calendar/useCalendarReminders.ts +28 -9
  5. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +4 -0
  6. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +89 -3
  7. package/apps/shared/components/calendar/EventDetails.tsx +13 -2
  8. package/apps/shared/components/calendar/EventEditor.tsx +124 -17
  9. package/apps/shared/components/calendar/EventFormParts.tsx +1 -0
  10. package/apps/shared/components/calendar/EventModal.tsx +19 -2
  11. package/apps/shared/components/calendar/RequestChangeForm.tsx +4 -1
  12. package/apps/shared/components/calendar/eventForm.ts +2 -0
  13. package/apps/shared/components/contacts/ContactCard.tsx +13 -1
  14. package/apps/shared/components/contacts/ContactDetailPane.tsx +189 -9
  15. package/apps/shared/components/contacts/ContactForm.tsx +56 -7
  16. package/apps/shared/components/contacts/KeyChangeReview.tsx +5 -1
  17. package/apps/shared/components/contacts/contactCardData.ts +8 -4
  18. package/apps/shared/components/contacts/contactKeys.ts +3 -3
  19. package/apps/shared/components/layout/AppShell.tsx +34 -5
  20. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +30 -1
  21. package/apps/shared/components/layout/SharedMailboxKeyManager.tsx +122 -0
  22. package/apps/shared/components/layout/UnlockPromptProvider.tsx +34 -0
  23. package/apps/shared/components/mail/ConversationList.tsx +1 -1
  24. package/apps/shared/components/mail/InviteCard.tsx +11 -1
  25. package/apps/shared/components/mail/MessageDetailPane.tsx +59 -26
  26. package/apps/shared/components/mail/compose/ComposeWindow.tsx +9 -2
  27. package/apps/shared/components/mail/compose/RecipientInput.tsx +2 -1
  28. package/apps/shared/components/mail/compose/draftResume.ts +28 -3
  29. package/apps/shared/components/mail/compose/protectedContactSuggestions.ts +121 -0
  30. package/apps/shared/components/mail/compose/quotedBody.ts +4 -3
  31. package/apps/shared/components/mail/invite/inviteStore.ts +17 -1
  32. package/apps/shared/components/mail/layout/MailShell.tsx +2 -0
  33. package/apps/shared/components/mail/pinnedSigners.ts +14 -0
  34. package/apps/shared/components/mail/reading/conversationPrint.ts +3 -3
  35. package/apps/shared/components/mail/reading/useMessageActions.ts +4 -1
  36. package/apps/shared/components/settings/KeyExportSetting.tsx +173 -0
  37. package/apps/shared/components/settings/OpenPgpKeySetting.tsx +240 -0
  38. package/apps/shared/components/settings/PolicyLockedNotice.tsx +23 -0
  39. package/apps/shared/components/settings/ZeroAccessSetting.tsx +128 -0
  40. package/apps/shared/components/settings/ZeroAccessTypes.tsx +175 -0
  41. package/apps/shared/components/settings/layout/SettingsShell.tsx +9 -2
  42. package/apps/shared/mail/decryptedMessages.ts +11 -3
  43. package/apps/shared/mail/outbox/sendJob.ts +349 -50
  44. package/apps/shared/mail/outbox/sendOutcomes.ts +91 -20
  45. package/apps/shared/mail/reportNotices.ts +3 -1
  46. package/apps/shared/mail/zeroAccessSource.ts +27 -0
  47. package/apps/shared/search/LocalIndexLifecycle.tsx +84 -24
  48. package/apps/shared/search/localIndexBuilder.ts +57 -20
  49. package/apps/shared/search/localIndexRpcClient.ts +96 -12
  50. package/apps/shared/search/localIndexSchema.ts +20 -4
  51. package/apps/shared/search/localIndexTransport.ts +6 -2
  52. package/apps/shared/search/localIndexWorker.ts +37 -18
  53. package/apps/shared/search/searchTier2.ts +5 -0
  54. package/apps/shared/search/zeroAccessIndexSync.ts +679 -0
  55. package/apps/shared/search/zeroAccessSyncState.ts +70 -0
  56. package/apps/shared/zeroAccess/ProtectedMark.tsx +19 -0
  57. package/apps/shared/zeroAccess/useKeysLocked.ts +25 -0
  58. package/apps/shared/zeroAccess/useOpenedRecord.ts +71 -0
  59. package/apps/www/_shell.tsx +2 -0
  60. package/apps/www/calendar/index.tsx +225 -61
  61. package/apps/www/contacts/index.tsx +67 -11
  62. package/apps/www/index.tsx +19 -3
  63. package/apps/www/settings/encryption/index.tsx +21 -1
  64. package/apps/www/settings/privacy/index.tsx +89 -39
  65. package/apps/www/tasks/index.tsx +24 -6
  66. package/dist/apps/escrow/matters/[uid].js +2 -2
  67. package/dist/apps/shared/calendar/calendarReminders.d.ts +14 -0
  68. package/dist/apps/shared/calendar/calendarReminders.js +26 -0
  69. package/dist/apps/shared/calendar/sealedEvents.d.ts +14 -0
  70. package/dist/apps/shared/calendar/sealedEvents.js +85 -0
  71. package/dist/apps/shared/calendar/useCalendarReminders.d.ts +2 -1
  72. package/dist/apps/shared/calendar/useCalendarReminders.js +27 -9
  73. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +3 -0
  74. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +25 -2
  75. package/dist/apps/shared/components/calendar/EventDetails.js +13 -2
  76. package/dist/apps/shared/components/calendar/EventEditor.js +115 -18
  77. package/dist/apps/shared/components/calendar/EventFormParts.js +1 -1
  78. package/dist/apps/shared/components/calendar/EventModal.d.ts +1 -1
  79. package/dist/apps/shared/components/calendar/EventModal.js +15 -2
  80. package/dist/apps/shared/components/calendar/RequestChangeForm.d.ts +3 -0
  81. package/dist/apps/shared/components/calendar/RequestChangeForm.js +1 -1
  82. package/dist/apps/shared/components/calendar/eventForm.d.ts +2 -0
  83. package/dist/apps/shared/components/contacts/ContactCard.js +12 -1
  84. package/dist/apps/shared/components/contacts/ContactDetailPane.d.ts +2 -1
  85. package/dist/apps/shared/components/contacts/ContactDetailPane.js +103 -9
  86. package/dist/apps/shared/components/contacts/ContactForm.d.ts +4 -1
  87. package/dist/apps/shared/components/contacts/ContactForm.js +37 -7
  88. package/dist/apps/shared/components/contacts/KeyChangeReview.d.ts +3 -1
  89. package/dist/apps/shared/components/contacts/KeyChangeReview.js +3 -2
  90. package/dist/apps/shared/components/contacts/contactCardData.js +8 -4
  91. package/dist/apps/shared/components/contacts/contactKeys.d.ts +1 -1
  92. package/dist/apps/shared/components/contacts/contactKeys.js +3 -3
  93. package/dist/apps/shared/components/layout/AppShell.d.ts +18 -1
  94. package/dist/apps/shared/components/layout/AppShell.js +17 -6
  95. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +27 -2
  96. package/dist/apps/shared/components/layout/SharedMailboxKeyManager.d.ts +17 -0
  97. package/dist/apps/shared/components/layout/SharedMailboxKeyManager.js +98 -0
  98. package/dist/apps/shared/components/layout/UnlockPromptProvider.d.ts +3 -0
  99. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +32 -0
  100. package/dist/apps/shared/components/mail/ConversationList.js +1 -1
  101. package/dist/apps/shared/components/mail/InviteCard.js +11 -1
  102. package/dist/apps/shared/components/mail/MessageDetailPane.js +46 -21
  103. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +2 -2
  104. package/dist/apps/shared/components/mail/compose/RecipientInput.js +3 -2
  105. package/dist/apps/shared/components/mail/compose/draftResume.d.ts +2 -0
  106. package/dist/apps/shared/components/mail/compose/draftResume.js +28 -3
  107. package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.d.ts +14 -0
  108. package/dist/apps/shared/components/mail/compose/protectedContactSuggestions.js +96 -0
  109. package/dist/apps/shared/components/mail/compose/quotedBody.d.ts +1 -1
  110. package/dist/apps/shared/components/mail/compose/quotedBody.js +4 -3
  111. package/dist/apps/shared/components/mail/invite/inviteStore.js +16 -1
  112. package/dist/apps/shared/components/mail/layout/MailShell.js +2 -1
  113. package/dist/apps/shared/components/mail/pinnedSigners.d.ts +6 -0
  114. package/dist/apps/shared/components/mail/pinnedSigners.js +10 -1
  115. package/dist/apps/shared/components/mail/reading/conversationPrint.d.ts +2 -2
  116. package/dist/apps/shared/components/mail/reading/conversationPrint.js +3 -3
  117. package/dist/apps/shared/components/mail/reading/useMessageActions.js +4 -1
  118. package/dist/apps/shared/components/settings/KeyExportSetting.d.ts +13 -0
  119. package/dist/apps/shared/components/settings/KeyExportSetting.js +90 -0
  120. package/dist/apps/shared/components/settings/OpenPgpKeySetting.d.ts +13 -0
  121. package/dist/apps/shared/components/settings/OpenPgpKeySetting.js +143 -0
  122. package/dist/apps/shared/components/settings/PolicyLockedNotice.d.ts +11 -0
  123. package/dist/apps/shared/components/settings/PolicyLockedNotice.js +9 -0
  124. package/dist/apps/shared/components/settings/ZeroAccessSetting.d.ts +16 -0
  125. package/dist/apps/shared/components/settings/ZeroAccessSetting.js +80 -0
  126. package/dist/apps/shared/components/settings/ZeroAccessTypes.d.ts +29 -0
  127. package/dist/apps/shared/components/settings/ZeroAccessTypes.js +107 -0
  128. package/dist/apps/shared/components/settings/layout/SettingsShell.d.ts +3 -0
  129. package/dist/apps/shared/components/settings/layout/SettingsShell.js +3 -2
  130. package/dist/apps/shared/mail/decryptedMessages.d.ts +1 -1
  131. package/dist/apps/shared/mail/decryptedMessages.js +11 -3
  132. package/dist/apps/shared/mail/outbox/sendJob.d.ts +37 -4
  133. package/dist/apps/shared/mail/outbox/sendJob.js +287 -42
  134. package/dist/apps/shared/mail/outbox/sendOutcomes.d.ts +12 -0
  135. package/dist/apps/shared/mail/outbox/sendOutcomes.js +76 -20
  136. package/dist/apps/shared/mail/reportNotices.js +3 -1
  137. package/dist/apps/shared/mail/zeroAccessSource.d.ts +8 -0
  138. package/dist/apps/shared/mail/zeroAccessSource.js +26 -0
  139. package/dist/apps/shared/search/LocalIndexLifecycle.d.ts +2 -0
  140. package/dist/apps/shared/search/LocalIndexLifecycle.js +74 -24
  141. package/dist/apps/shared/search/localIndexBuilder.d.ts +9 -1
  142. package/dist/apps/shared/search/localIndexBuilder.js +50 -17
  143. package/dist/apps/shared/search/localIndexRpcClient.d.ts +5 -39
  144. package/dist/apps/shared/search/localIndexRpcClient.js +129 -11
  145. package/dist/apps/shared/search/localIndexSchema.d.ts +14 -2
  146. package/dist/apps/shared/search/localIndexSchema.js +18 -4
  147. package/dist/apps/shared/search/localIndexTransport.d.ts +6 -2
  148. package/dist/apps/shared/search/localIndexWorker.d.ts +11 -0
  149. package/dist/apps/shared/search/localIndexWorker.js +22 -15
  150. package/dist/apps/shared/search/searchTier2.js +5 -0
  151. package/dist/apps/shared/search/zeroAccessIndexSync.d.ts +102 -0
  152. package/dist/apps/shared/search/zeroAccessIndexSync.js +544 -0
  153. package/dist/apps/shared/search/zeroAccessSyncState.d.ts +20 -0
  154. package/dist/apps/shared/search/zeroAccessSyncState.js +65 -0
  155. package/dist/apps/shared/zeroAccess/ProtectedMark.d.ts +7 -0
  156. package/dist/apps/shared/zeroAccess/ProtectedMark.js +10 -0
  157. package/dist/apps/shared/zeroAccess/useKeysLocked.d.ts +6 -0
  158. package/dist/apps/shared/zeroAccess/useKeysLocked.js +20 -0
  159. package/dist/apps/shared/zeroAccess/useOpenedRecord.d.ts +18 -0
  160. package/dist/apps/shared/zeroAccess/useOpenedRecord.js +66 -0
  161. package/dist/apps/www/_shell.d.ts +1 -1
  162. package/dist/apps/www/_shell.js +2 -2
  163. package/dist/apps/www/calendar/index.js +88 -20
  164. package/dist/apps/www/contacts/index.js +64 -11
  165. package/dist/apps/www/index.js +19 -3
  166. package/dist/apps/www/settings/encryption/index.js +16 -4
  167. package/dist/apps/www/settings/privacy/index.js +47 -6
  168. package/dist/apps/www/tasks/index.js +21 -7
  169. package/dist/lib/admin/domainsApi.d.ts +1 -1
  170. package/dist/lib/admin/escrowAccessRequestsApi.d.ts +6 -0
  171. package/dist/lib/calendar/calendarApi.d.ts +52 -6
  172. package/dist/lib/calendar/calendarApi.js +110 -17
  173. package/dist/lib/calendar/calendarMutations.d.ts +20 -7
  174. package/dist/lib/calendar/calendarMutations.js +73 -14
  175. package/dist/lib/calendar/inviteApi.d.ts +17 -3
  176. package/dist/lib/calendar/inviteApi.js +86 -7
  177. package/dist/lib/calendar/protectedEvents.d.ts +31 -0
  178. package/dist/lib/calendar/protectedEvents.js +113 -0
  179. package/dist/lib/calendar/recurrence.d.ts +2 -0
  180. package/dist/lib/calendar/recurrence.js +16 -0
  181. package/dist/lib/calendar/sealedInvites.d.ts +9 -0
  182. package/dist/lib/calendar/sealedInvites.js +65 -0
  183. package/dist/lib/contacts/contactsApi.d.ts +29 -3
  184. package/dist/lib/contacts/contactsApi.js +23 -7
  185. package/dist/lib/contacts/useContactPhotoSrc.d.ts +5 -2
  186. package/dist/lib/contacts/useContactPhotoSrc.js +91 -11
  187. package/dist/lib/crypto/composeSecurity.d.ts +11 -3
  188. package/dist/lib/crypto/composeSecurity.js +13 -5
  189. package/dist/lib/crypto/keyExport.d.ts +54 -0
  190. package/dist/lib/crypto/keyExport.js +161 -0
  191. package/dist/lib/crypto/keyPairMatch.d.ts +9 -0
  192. package/dist/lib/crypto/keyPairMatch.js +65 -0
  193. package/dist/lib/crypto/keySession.d.ts +64 -3
  194. package/dist/lib/crypto/keySession.js +179 -12
  195. package/dist/lib/crypto/keyvaultApi.d.ts +129 -8
  196. package/dist/lib/crypto/keyvaultApi.js +45 -3
  197. package/dist/lib/crypto/meetingKeys.d.ts +25 -0
  198. package/dist/lib/crypto/meetingKeys.js +87 -0
  199. package/dist/lib/crypto/messageSecurity.d.ts +56 -3
  200. package/dist/lib/crypto/messageSecurity.js +201 -13
  201. package/dist/lib/crypto/mime.d.ts +5 -0
  202. package/dist/lib/crypto/mime.js +25 -0
  203. package/dist/lib/crypto/openPgpKey.d.ts +39 -0
  204. package/dist/lib/crypto/openPgpKey.js +89 -0
  205. package/dist/lib/crypto/openpgp.d.ts +47 -0
  206. package/dist/lib/crypto/openpgp.js +88 -0
  207. package/dist/lib/crypto/pgpMessage.d.ts +51 -0
  208. package/dist/lib/crypto/pgpMessage.js +271 -0
  209. package/dist/lib/crypto/sharedMailboxKeys.d.ts +62 -0
  210. package/dist/lib/crypto/sharedMailboxKeys.js +337 -0
  211. package/dist/lib/crypto/smimeMessage.d.ts +21 -0
  212. package/dist/lib/crypto/smimeMessage.js +11 -7
  213. package/dist/lib/crypto/zeroAccessKey.d.ts +32 -0
  214. package/dist/lib/crypto/zeroAccessKey.js +71 -0
  215. package/dist/lib/mail/conversationsApi.d.ts +6 -0
  216. package/dist/lib/mail/mailApi.d.ts +63 -2
  217. package/dist/lib/mail/mailApi.js +5 -1
  218. package/dist/lib/mail/mailboxImportApi.d.ts +10 -0
  219. package/dist/lib/mail/mailboxImportApi.js +15 -0
  220. package/dist/lib/search/zeroAccessIndexApi.d.ts +85 -0
  221. package/dist/lib/search/zeroAccessIndexApi.js +35 -0
  222. package/dist/lib/tasks/tasksApi.d.ts +11 -1
  223. package/dist/lib/tasks/tasksApi.js +10 -4
  224. package/dist/lib/util/clientPlatform.d.ts +14 -0
  225. package/dist/lib/util/clientPlatform.js +26 -0
  226. package/dist/lib/videoconf/protectedMeetings.d.ts +74 -0
  227. package/dist/lib/videoconf/protectedMeetings.js +181 -0
  228. package/dist/lib/videoconf/videoMeetingsApi.d.ts +25 -4
  229. package/dist/lib/videoconf/videoMeetingsApi.js +1 -1
  230. package/dist/lib/zeroAccess/sealedMoves.d.ts +41 -0
  231. package/dist/lib/zeroAccess/sealedMoves.js +63 -0
  232. package/dist/lib/zeroAccess/sealedRecords.d.ts +29 -0
  233. package/dist/lib/zeroAccess/sealedRecords.js +136 -0
  234. package/lib/admin/domainsApi.ts +1 -1
  235. package/lib/admin/escrowAccessRequestsApi.ts +6 -0
  236. package/lib/calendar/calendarApi.ts +143 -17
  237. package/lib/calendar/calendarMutations.ts +79 -12
  238. package/lib/calendar/inviteApi.ts +101 -8
  239. package/lib/calendar/protectedEvents.ts +143 -0
  240. package/lib/calendar/recurrence.ts +17 -0
  241. package/lib/calendar/sealedInvites.ts +73 -0
  242. package/lib/contacts/contactsApi.ts +43 -17
  243. package/lib/contacts/useContactPhotoSrc.ts +103 -13
  244. package/lib/crypto/composeSecurity.ts +20 -5
  245. package/lib/crypto/keyExport.ts +229 -0
  246. package/lib/crypto/keyPairMatch.ts +69 -0
  247. package/lib/crypto/keySession.ts +216 -12
  248. package/lib/crypto/keyvaultApi.ts +174 -10
  249. package/lib/crypto/meetingKeys.ts +118 -0
  250. package/lib/crypto/messageSecurity.ts +284 -16
  251. package/lib/crypto/mime.ts +26 -0
  252. package/lib/crypto/openPgpKey.ts +118 -0
  253. package/lib/crypto/openpgp.ts +115 -0
  254. package/lib/crypto/pgpMessage.ts +336 -0
  255. package/lib/crypto/sharedMailboxKeys.ts +402 -0
  256. package/lib/crypto/smimeMessage.ts +11 -7
  257. package/lib/crypto/zeroAccessKey.ts +102 -0
  258. package/lib/mail/conversationsApi.ts +3 -0
  259. package/lib/mail/mailApi.ts +60 -3
  260. package/lib/mail/mailboxImportApi.ts +24 -0
  261. package/lib/search/zeroAccessIndexApi.ts +123 -0
  262. package/lib/tasks/tasksApi.ts +19 -5
  263. package/lib/util/clientPlatform.ts +50 -0
  264. package/lib/videoconf/protectedMeetings.ts +221 -0
  265. package/lib/videoconf/videoMeetingsApi.ts +15 -4
  266. package/lib/zeroAccess/sealedMoves.ts +79 -0
  267. package/lib/zeroAccess/sealedRecords.ts +153 -0
  268. package/package.json +14 -5
@@ -21,6 +21,7 @@
21
21
  * `apiFetch<T>()` call (see `crypto/signingProviderApi.ts`'s own note on the same choice).
22
22
  */
23
23
 
24
+ import type { MeetingProtectionInput } from "./protectedMeetings.js";
24
25
  import { ApiClient, withClient } from "../util/api.js";
25
26
  import { ListParams, buildQuery } from "../util/apiQuery.js";
26
27
 
@@ -47,6 +48,11 @@ export interface VideoMeeting {
47
48
  * (its oldest non-cancelled public meeting - see that app's own doc comment) out of `listVideoMeetings()`'s
48
49
  * results. */
49
50
  dateCreated: string;
51
+ /** Protected end to end (meet-plugin `specs/meeting_privacy.md`): `title` is `"Meeting"`, the real one sealed, and every link works
52
+ * only with the meeting key after `#` - see `protectedMeetings.ts`. */
53
+ keyProtected?: boolean | null;
54
+ /** A protected meeting's key, sealed to the organizer's zero-access certificate. */
55
+ keyWraps?: { mailboxUid: string; recipientFingerprint: string; kind: string; cms: string }[] | null;
50
56
  }
51
57
 
52
58
  /**
@@ -80,8 +86,9 @@ export interface VideoMeetingInviteeJoinInfo extends VideoMeetingInvitee {
80
86
  export interface CreateVideoMeetingInput {
81
87
  /** The mailbox that owns the meeting — the one whose ACL every later call on it is checked against. */
82
88
  mailboxUid: string;
83
- /** Non-empty, at most 200 characters (the server rejects anything else with a 400). */
84
- title: string;
89
+ /** Non-empty, at most 200 characters (the server rejects anything else with a 400). Omitted for a protected meeting, whose title is
90
+ * sealed in `protection`. */
91
+ title?: string;
85
92
  visibility: VideoMeetingVisibility;
86
93
  /** The `CalendarEvent` this meeting is being minted for, when there is one. */
87
94
  calendarEventUid?: string;
@@ -90,7 +97,9 @@ export interface CreateVideoMeetingInput {
90
97
  /** Required, and non-empty, for a `"private"` meeting — the server mints one personal join link per entry
91
98
  * and 400s a private meeting with none. Never send the organizer here: they reach their own meeting
92
99
  * through ownership (and `organizerJoinUrl`), not as a guest. */
93
- invitees?: VideoMeetingInvitee[];
100
+ invitees?: (VideoMeetingInvitee | { email: string; sealedFields?: { displayName: string } })[];
101
+ /** Makes the meeting protected end to end - `protectMeeting()` in `protectedMeetings.ts`. */
102
+ protection?: MeetingProtectionInput;
94
103
  }
95
104
 
96
105
  export interface VideoMeetingCreateResult {
@@ -120,9 +129,11 @@ export function createVideoMeeting(input: CreateVideoMeetingInput, client?: ApiC
120
129
  export interface UpdateVideoMeetingInput {
121
130
  title?: string;
122
131
  status?: "cancelled";
132
+ /** A protected meeting's new key expiry (ISO), when its event's end moved - `followMeetingKeyExpiry()` in `protectedMeetings.ts`. */
133
+ keyExpiresAt?: string;
123
134
  }
124
135
 
125
- /** Renames or cancels a meeting. Requires UPDATE on its owning mailbox. */
136
+ /** Renames, cancels or moves the key expiry of a meeting. Requires UPDATE on its owning mailbox. */
126
137
  export function updateVideoMeeting(uid: string, input: UpdateVideoMeetingInput, client?: ApiClient): Promise<VideoMeeting> {
127
138
  return withClient(client, `/mail/video-meetings/${encodeURIComponent(uid)}`, { method: "PUT", body: JSON.stringify(input) });
128
139
  }
@@ -0,0 +1,79 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * A calendar event, contact or task whose content is sealed to its mailbox's zero-access key - field by field (`sealedFields`), as one
7
+ * blob (`sealedContent`, an event from a zero-access invitation) or its photo (`photoSealed`) - can't move to another mailbox: what it
8
+ * keeps sealed is addressed to the mailbox it is in, and the server can't seal it again to the destination's key. restapi refuses such a
9
+ * move with a `409` (`ZeroAccessRouteSealing.refuseSealedMove()`).
10
+ *
11
+ * The record API wrappers (`updateContact()`, `updateCalendarEvent()`, `updateTask()`) refuse it before sending anything when they know the
12
+ * record (`guardSealedMove()`), and turn the server's refusal into the same `SealedMoveError` when they don't - one request, so nothing is
13
+ * left half moved. A UI offering a move asks `canMoveToMailbox()` first, and shows `SealedMoveError`'s message.
14
+ */
15
+ import { ApiRequestError } from "../util/api.js";
16
+
17
+ /** The fields of a record that say whether its content is sealed, and where. */
18
+ export interface MovableRecord {
19
+ mailboxUid: string;
20
+ sealedFields?: Record<string, string> | null;
21
+ sealedContent?: unknown;
22
+ photoSealed?: boolean | null;
23
+ }
24
+
25
+ /** What a refused move says. */
26
+ export const SEALED_MOVE_MESSAGE =
27
+ "This item is protected by its mailbox's zero-access encryption, so it can't be moved to another mailbox. Create a copy there instead.";
28
+
29
+ /** `SealedMoveError.code`. */
30
+ export const SEALED_MOVE_CODE = "sealed-move";
31
+
32
+ /** restapi's `ApiErrors.INVALID_REQUEST`, the code of its sealed-move refusal and of its other zero-access refusals (a version conflict,
33
+ * also a `409`, is `api-012`; an identifier already taken, `api-011`). */
34
+ export const INVALID_REQUEST_CODE = "api-003";
35
+
36
+ /** A move of a protected record to another mailbox, refused - by this client before sending it, or by the server. Nothing was written. */
37
+ export class SealedMoveError extends ApiRequestError {
38
+ constructor(message: string = SEALED_MOVE_MESSAGE) {
39
+ super(message, 409, SEALED_MOVE_CODE);
40
+ this.name = "SealedMoveError";
41
+ }
42
+ }
43
+
44
+ /** Whether `record` keeps any of its content sealed to its mailbox's key. */
45
+ export function isSealedRecord(record: MovableRecord): boolean {
46
+ return (!!record.sealedFields && Object.keys(record.sealedFields).length > 0) || !!record.sealedContent || !!record.photoSealed;
47
+ }
48
+
49
+ /** Whether `record` may move to mailbox `targetMailboxUid`: always within its own mailbox, and to another only when nothing of it is sealed. */
50
+ export function canMoveToMailbox(record: MovableRecord, targetMailboxUid: string): boolean {
51
+ return targetMailboxUid === record.mailboxUid || !isSealedRecord(record);
52
+ }
53
+
54
+ /** Whether `err` is restapi's refusal of a sealed record's move. */
55
+ export function isSealedMoveRefusal(err: unknown): err is ApiRequestError {
56
+ return err instanceof SealedMoveError || (err instanceof ApiRequestError && err.status === 409 && err.code === INVALID_REQUEST_CODE);
57
+ }
58
+
59
+ /**
60
+ * Runs `write`, a save of a record that sets its `mailboxUid` to `targetMailboxUid` (`undefined`: the save doesn't set it). With `current`,
61
+ * the record as this device has it, a move of a sealed one is refused before anything is sent. A move the server refuses for the same reason
62
+ * rejects with `SealedMoveError` too.
63
+ */
64
+ export async function guardSealedMove<T>(current: MovableRecord | undefined, targetMailboxUid: string | undefined, write: () => Promise<T>): Promise<T> {
65
+ if (targetMailboxUid === undefined) {
66
+ return write();
67
+ }
68
+ if (current && !canMoveToMailbox(current, targetMailboxUid)) {
69
+ throw new SealedMoveError();
70
+ }
71
+ try {
72
+ return await write();
73
+ } catch (err) {
74
+ if (current?.mailboxUid !== targetMailboxUid && isSealedMoveRefusal(err) && !(err instanceof SealedMoveError)) {
75
+ throw new SealedMoveError();
76
+ }
77
+ throw err;
78
+ }
79
+ }
@@ -0,0 +1,153 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ /**
6
+ * Calendar events, contacts and tasks of a mailbox whose zero-access encryption covers their type keep their protected fields only
7
+ * sealed, field by field, to the mailbox's zero-access key (`sealedFields`; the readable column holds a placeholder - restapi
8
+ * `specs/search.md` §3 *What It Covers*). They are opened here with the mailbox's unlocked keys; while it is locked a record keeps its
9
+ * placeholders. The decryption code is loaded only when a sealed record is actually seen.
10
+ *
11
+ * Writing needs nothing here: the client sends plaintext and the server seals it on arrival.
12
+ */
13
+ import { fromBase64 } from "../crypto/encoding.js";
14
+ import { getUnlockedKeys, subscribeKeySession } from "../crypto/keySession.js";
15
+
16
+ /** A record that may carry sealed fields. */
17
+ export interface SealedRecord {
18
+ mailboxUid: string;
19
+ /** Protected field name -> base64 CMS of its JSON value. */
20
+ sealedFields?: Record<string, string> | null;
21
+ }
22
+
23
+ /** Opened values by sealed text - the same sealed text always opens to the same value. Cleared with `clearSealedRecords()`. */
24
+ const opened: Map<string, unknown> = new Map();
25
+
26
+ /** Forgets every opened value (a mailbox locking, or the test seam). */
27
+ export function clearSealedRecords(): void {
28
+ opened.clear();
29
+ }
30
+
31
+ /** Set on a record whose every sealed field was opened. A symbol, so it survives a spread copy (an occurrence of an event) and is never sent. */
32
+ const OPENED = Symbol("openedSealedRecord");
33
+
34
+ /** Whether every protected field of `record` holds its real value - opened on this device - rather than its placeholder. */
35
+ export function isOpenedRecord(record: SealedRecord | undefined): boolean {
36
+ return !!record && (record as any)[OPENED] === true;
37
+ }
38
+
39
+ /** Whether `record` has protected fields. */
40
+ export function isProtectedRecord(record: SealedRecord | undefined): boolean {
41
+ return !!record?.sealedFields && Object.keys(record.sealedFields).length > 0;
42
+ }
43
+
44
+ /** `record` with every sealed field (of `only`, when given) this device can open set to its value; `undefined` when it can't open them
45
+ * (locked, or not its key). */
46
+ async function openRecord<T extends SealedRecord>(record: T, only?: readonly string[]): Promise<T | undefined> {
47
+ const keys = getUnlockedKeys(record.mailboxUid)?.zeroAccessKeys;
48
+ if (!keys?.length) {
49
+ return undefined;
50
+ }
51
+ try {
52
+ const values: Record<string, unknown> = {};
53
+ for (const [name, sealed] of Object.entries(record.sealedFields!)) {
54
+ if (only && !only.includes(name)) {
55
+ continue;
56
+ }
57
+ if (!opened.has(sealed)) {
58
+ const { decryptEnvelopedDataWithKeys } = await import("../crypto/smime.js");
59
+ const value: unknown = JSON.parse(new TextDecoder().decode(await decryptEnvelopedDataWithKeys(fromBase64(sealed), keys)));
60
+ // The mailbox locked while this was opened: the lock's clear has already run, so nothing is kept or shown.
61
+ if (!getUnlockedKeys(record.mailboxUid)) {
62
+ return undefined;
63
+ }
64
+ opened.set(sealed, value);
65
+ }
66
+ values[name] = opened.get(sealed);
67
+ }
68
+ return only ? { ...record, ...values } : { ...record, ...values, [OPENED]: true };
69
+ } catch {
70
+ return undefined;
71
+ }
72
+ }
73
+
74
+ let watchingKeys = false;
75
+
76
+ /** Opened values go when keys are locked (which signing out does), so nothing opened outlives its key session. Subscribed when the
77
+ * first protected record is opened, not on import. */
78
+ function forgetOnLock(): void {
79
+ if (watchingKeys) {
80
+ return;
81
+ }
82
+ watchingKeys = true;
83
+ subscribeKeySession((event) => {
84
+ if (event.state === "locked") {
85
+ clearSealedRecords();
86
+ }
87
+ });
88
+ }
89
+
90
+ /** `records` with every protected one this device can open shown with its real values - only the fields in `only`, when given (a list
91
+ * shows a contact's name before anyone asks for its phone numbers: each field is one decryption). Never rejects. */
92
+ export async function openSealedRecords<T extends SealedRecord>(records: T[], only?: readonly string[]): Promise<T[]> {
93
+ if (!records.some(isProtectedRecord)) {
94
+ return records;
95
+ }
96
+ forgetOnLock();
97
+ return Promise.all(records.map(async (record) => (isProtectedRecord(record) ? ((await openRecord(record, only)) ?? record) : record)));
98
+ }
99
+
100
+ /** `der` (a blob the server sealed to `mailboxUid`'s zero-access key, e.g. a protected contact's photo) opened, or `undefined` when this
101
+ * device can't (the mailbox is locked, or it isn't its key). */
102
+ export async function openSealedBytes(mailboxUid: string, der: Uint8Array): Promise<Uint8Array | undefined> {
103
+ const keys = getUnlockedKeys(mailboxUid)?.zeroAccessKeys;
104
+ if (!keys?.length) {
105
+ return undefined;
106
+ }
107
+ try {
108
+ const { decryptEnvelopedDataWithKeys } = await import("../crypto/smime.js");
109
+ const bytes = await decryptEnvelopedDataWithKeys(der, keys);
110
+ // Locked while it was opened: not handed out.
111
+ return getUnlockedKeys(mailboxUid) ? bytes : undefined;
112
+ } catch {
113
+ return undefined;
114
+ }
115
+ }
116
+
117
+ /** A field's value for comparing what a write sends with what was shown: every empty form (`null`, `undefined`, `""`, `[]`) is one. */
118
+ function comparable(value: unknown): string {
119
+ return value === null || value === undefined || value === "" || (Array.isArray(value) && value.length === 0) ? "" : JSON.stringify(value);
120
+ }
121
+
122
+ /**
123
+ * `input` as written to `shown` - the record the edit started from, as this device showed it: unchanged when `shown` isn't protected.
124
+ * Otherwise without the protected fields (those `shown.sealedFields` names) whose values are what `shown` has: an opened record's write
125
+ * sends only what changed, and a placeholder a record that couldn't be opened showed is never sent back as its value. A field of an opened
126
+ * record that held a value and is emptied is named in `clearedFields`: restapi applies an empty value of a sealed field as a clear only
127
+ * then (`ZeroAccessRouteSealing.takeClearedFields()`), since an empty placeholder sent back by a client that couldn't open the record would
128
+ * otherwise wipe the sealed value for good. `opened`: `shown` holds the real values (by default, whether it was opened here).
129
+ */
130
+ export function protectedRecordWrite<T extends object>(shown: SealedRecord | undefined, input: T, opened: boolean = isOpenedRecord(shown)): T {
131
+ if (!isProtectedRecord(shown)) {
132
+ return input;
133
+ }
134
+ const changes: Record<string, unknown> = { ...(input as Record<string, unknown>) };
135
+ const clearedFields: string[] = [];
136
+ for (const field of Object.keys(shown!.sealedFields!)) {
137
+ if (!(field in changes)) {
138
+ continue;
139
+ }
140
+ const value: string = comparable(changes[field]);
141
+ if (value === comparable((shown as unknown as Record<string, unknown>)[field])) {
142
+ delete changes[field];
143
+ } else if (value === "" && opened) {
144
+ clearedFields.push(field);
145
+ }
146
+ }
147
+ return (clearedFields.length > 0 ? { ...changes, clearedFields } : changes) as T;
148
+ }
149
+
150
+ /** `openSealedRecords()` for one record. */
151
+ export async function openSealedRecord<T extends SealedRecord>(record: T, only?: readonly string[]): Promise<T> {
152
+ return (await openSealedRecords([record], only))[0];
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rapidmx/web-client",
3
- "version": "0.35.0",
3
+ "version": "0.37.0",
4
4
  "description": "RapidMX's webmail and admin console React UI for web-based clients.",
5
5
  "type": "module",
6
6
  "repository": "https://github.com/rapidmx/web-client",
@@ -37,7 +37,7 @@
37
37
  "trailingComma": "es5"
38
38
  },
39
39
  "peerDependencies": {
40
- "@rapidrest/react": ">=2.1.0 <3",
40
+ "@rapidrest/react": "2.x",
41
41
  "react": "19.x",
42
42
  "react-dom": "19.x"
43
43
  },
@@ -59,15 +59,17 @@
59
59
  "@tiptap/starter-kit": "^3.31.3",
60
60
  "asn1js": "^3.0.10",
61
61
  "date-fns": "^4.4.0",
62
- "dompurify": "^3.4.15",
62
+ "dompurify": "^3.4.17",
63
63
  "hash-wasm": "^4.12.0",
64
+ "openpgp": "^6.3.2",
64
65
  "pkijs": "^3.4.0",
65
66
  "react-icons": "^5.7.0",
66
67
  "reflect-metadata": "^0.2.2",
67
68
  "rrule": "^2.8.1"
68
69
  },
69
70
  "devDependencies": {
70
- "@rapidrest/react": "^2.1.0",
71
+ "@openpgp/web-stream-tools": "~0.3.1",
72
+ "@rapidrest/react": "^2.2.0",
71
73
  "@tailwindcss/vite": "^4.3.3",
72
74
  "@testing-library/dom": "^10.4.1",
73
75
  "@testing-library/jest-dom": "^7.0.1",
@@ -92,9 +94,16 @@
92
94
  "tailwindcss": "^4.3.3",
93
95
  "tsx": "^4.23.13",
94
96
  "typescript": "^6.0.3",
95
- "vite": "^8.2.2",
97
+ "vite": "^8.3.5",
96
98
  "vitest": "^5.0.0"
97
99
  },
100
+ "resolutions": {
101
+ "brace-expansion@npm:^1.1.7": "^1.1.21",
102
+ "brace-expansion@npm:^5.0.8": "^5.0.12",
103
+ "source-map-js@npm:^1.2.1": "^1.2.2",
104
+ "undici@npm:^8.4.1": "^8.12.3",
105
+ "undici@npm:^8.9.0": "^8.12.3"
106
+ },
98
107
  "packageManager": "yarn@4.5.3",
99
108
  "engines": {
100
109
  "node": ">=24.0.0"