@rapidmx/web-client 0.3.0 → 0.4.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 (102) hide show
  1. package/apps/admin/branding/index.tsx +39 -404
  2. package/apps/admin/domains/[uid].tsx +92 -259
  3. package/apps/admin/encryption-policy/index.tsx +19 -0
  4. package/apps/admin/index.tsx +100 -82
  5. package/apps/admin/mailbox-policy/index.tsx +19 -0
  6. package/apps/admin/mailboxes/new/index.tsx +28 -291
  7. package/apps/admin/plugins/index.tsx +15 -0
  8. package/apps/admin/retention-policy/index.tsx +39 -132
  9. package/apps/admin/setup/index.tsx +15 -0
  10. package/apps/shared/components/admin/layout/AdminShell.tsx +249 -210
  11. package/apps/shared/components/admin/settings/BrandingForm.tsx +374 -0
  12. package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +176 -0
  13. package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +104 -0
  14. package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
  15. package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +302 -0
  16. package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +119 -0
  17. package/apps/shared/components/admin/settings/PluginsManager.tsx +595 -0
  18. package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +98 -0
  19. package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +254 -0
  20. package/apps/shared/components/admin/setup/SetupWizard.tsx +300 -0
  21. package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
  22. package/apps/shared/components/calendar/EventModal.tsx +40 -4
  23. package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
  24. package/apps/shared/components/contacts/ContactForm.tsx +38 -4
  25. package/apps/shared/components/layout/AppShell.tsx +198 -169
  26. package/apps/shared/components/layout/KeyEnrollmentGate.tsx +298 -265
  27. package/apps/shared/components/layout/UnlockPromptProvider.tsx +128 -0
  28. package/apps/shared/components/mail/MessageDetailPane.tsx +630 -595
  29. package/apps/shared/components/mail/compose/ComposeContext.tsx +8 -3
  30. package/apps/shared/components/mail/compose/ComposeWindow.tsx +930 -765
  31. package/apps/shared/components/mail/layout/MailShell.tsx +408 -302
  32. package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
  33. package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
  34. package/apps/shared/search/LocalIndexLifecycle.tsx +89 -0
  35. package/apps/shared/search/localIndexBlockCipher.ts +83 -0
  36. package/apps/shared/search/localIndexBuilder.ts +179 -0
  37. package/apps/shared/search/localIndexKey.ts +27 -0
  38. package/apps/shared/search/localIndexRpcClient.ts +116 -0
  39. package/apps/shared/search/localIndexSchema.ts +227 -0
  40. package/apps/shared/search/localIndexSizePreference.ts +88 -0
  41. package/apps/shared/search/localIndexVFS.ts +235 -0
  42. package/apps/shared/search/localIndexWorker.ts +464 -0
  43. package/apps/shared/search/searchTier2.ts +79 -0
  44. package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
  45. package/apps/www/calendar/index.tsx +31 -21
  46. package/apps/www/contacts/index.tsx +33 -6
  47. package/apps/www/index.tsx +661 -106
  48. package/apps/www/messages/[uid].tsx +5 -1
  49. package/apps/www/settings/encryption/index.tsx +63 -1
  50. package/apps/www/settings/sharing/index.tsx +271 -0
  51. package/apps/www/tasks/index.tsx +53 -4
  52. package/dist/apps/admin/branding/index.js +4 -98
  53. package/dist/apps/admin/domains/[uid].js +5 -68
  54. package/dist/apps/admin/encryption-policy/index.js +8 -0
  55. package/dist/apps/admin/index.js +14 -1
  56. package/dist/apps/admin/mailbox-policy/index.js +8 -0
  57. package/dist/apps/admin/mailboxes/new/index.js +4 -89
  58. package/dist/apps/admin/plugins/index.js +6 -0
  59. package/dist/apps/admin/retention-policy/index.js +3 -36
  60. package/dist/apps/admin/setup/index.js +6 -0
  61. package/dist/apps/shared/components/admin/layout/AdminShell.js +34 -3
  62. package/dist/apps/shared/components/admin/settings/BrandingForm.js +105 -0
  63. package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +77 -0
  64. package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +57 -0
  65. package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
  66. package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +106 -0
  67. package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +52 -0
  68. package/dist/apps/shared/components/admin/settings/PluginsManager.js +260 -0
  69. package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +44 -0
  70. package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +131 -0
  71. package/dist/apps/shared/components/admin/setup/SetupWizard.js +142 -0
  72. package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
  73. package/dist/apps/shared/components/calendar/EventModal.js +16 -4
  74. package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
  75. package/dist/apps/shared/components/contacts/ContactForm.js +16 -5
  76. package/dist/apps/shared/components/layout/AppShell.js +30 -7
  77. package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +15 -3
  78. package/dist/apps/shared/components/layout/UnlockPromptProvider.js +77 -0
  79. package/dist/apps/shared/components/mail/MessageDetailPane.js +27 -2
  80. package/dist/apps/shared/components/mail/compose/ComposeContext.js +2 -2
  81. package/dist/apps/shared/components/mail/compose/ComposeWindow.js +134 -14
  82. package/dist/apps/shared/components/mail/layout/MailShell.js +106 -40
  83. package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
  84. package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
  85. package/dist/apps/shared/search/LocalIndexLifecycle.js +76 -0
  86. package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
  87. package/dist/apps/shared/search/localIndexBuilder.js +153 -0
  88. package/dist/apps/shared/search/localIndexKey.js +25 -0
  89. package/dist/apps/shared/search/localIndexRpcClient.js +78 -0
  90. package/dist/apps/shared/search/localIndexSchema.js +179 -0
  91. package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
  92. package/dist/apps/shared/search/localIndexVFS.js +230 -0
  93. package/dist/apps/shared/search/localIndexWorker.js +341 -0
  94. package/dist/apps/shared/search/searchTier2.js +38 -0
  95. package/dist/apps/www/calendar/index.js +14 -12
  96. package/dist/apps/www/contacts/index.js +15 -6
  97. package/dist/apps/www/index.js +496 -94
  98. package/dist/apps/www/messages/[uid].js +5 -1
  99. package/dist/apps/www/settings/encryption/index.js +30 -2
  100. package/dist/apps/www/settings/sharing/index.js +128 -0
  101. package/dist/apps/www/tasks/index.js +27 -5
  102. package/package.json +3 -2
@@ -0,0 +1,128 @@
1
+ ///////////////////////////////////////////////////////////////////////////////
2
+ // Copyright (C) 2026 Jean-Philippe Steinmetz
3
+ // SPDX-License-Identifier: MPL-2.0
4
+ ///////////////////////////////////////////////////////////////////////////////
5
+ import React, { createContext, useCallback, useContext, useState } from "react";
6
+ import { getUnlockedKeys, unlockWithPassword, UnlockedKeys } from "@rapidmx/react-shared/crypto/keySession.js";
7
+ import type { PublicKey } from "@rapidmx/react-shared/crypto/keyvaultApi.js";
8
+ import Modal from "@rapidmx/react-shared/components/overlays/Modal.js";
9
+ import Alert from "@rapidmx/react-shared/components/feedback/Alert.js";
10
+ import Button from "@rapidmx/react-shared/components/buttons/Button.js";
11
+ import FormField from "@rapidmx/react-shared/components/forms/FormField.js";
12
+
13
+ interface PendingUnlock {
14
+ mailboxUid: string;
15
+ mailboxKeys: PublicKey[];
16
+ resolve: (keys: UnlockedKeys) => void;
17
+ reject: (err: Error) => void;
18
+ }
19
+
20
+ interface UnlockPromptContextValue {
21
+ /**
22
+ * Resolves immediately with this mailbox's already-unlocked keys if it has any this session -
23
+ * showing no UI at all. Otherwise shows an unlock dialog on top of whatever the user is currently
24
+ * doing and resolves once they enter the correct password, or rejects if they dismiss it.
25
+ */
26
+ requestUnlock(mailboxUid: string, mailboxKeys: PublicKey[]): Promise<UnlockedKeys>;
27
+ }
28
+
29
+ const UnlockPromptContext = createContext<UnlockPromptContextValue | null>(null);
30
+
31
+ /**
32
+ * The on-demand counterpart to `KeyEnrollmentGate`'s blocking, first-sign-in-only provisioning flow.
33
+ * Per `specs/end-to-end_encryption.md`, unlocking is only actually *required* to (1) sign or encrypt an
34
+ * outgoing message, (2) read an already-encrypted message, or (3) change encryption settings that touch
35
+ * the master key - not merely to have a mailbox with an existing vault resolve. Mounted once in
36
+ * `AppShell.tsx` (shared by every top-level app shell: Mail, Calendar, Contacts, Tasks, Settings), so
37
+ * any of those three call sites (`ComposeWindow`'s sign/encrypt toggles, `MessageDetailPane`'s encrypted-
38
+ * message view, `Settings > Encryption`) can request an unlock right where it's needed via
39
+ * `useUnlockPrompt()`, instead of the whole app being blocked behind an unlock screen the moment a
40
+ * mailbox happens to resolve - see `KeyEnrollmentGate`'s `blocking` prop, which `MailShell` now sets to
41
+ * `false` for exactly this reason.
42
+ */
43
+ export function UnlockPromptProvider({ children }: { children: React.ReactNode }) {
44
+ const [pending, setPending] = useState<PendingUnlock | null>(null);
45
+ const [password, setPassword] = useState("");
46
+ const [error, setError] = useState<string | null>(null);
47
+ const [unlocking, setUnlocking] = useState(false);
48
+
49
+ const requestUnlock = useCallback((mailboxUid: string, mailboxKeys: PublicKey[]): Promise<UnlockedKeys> => {
50
+ const existing = getUnlockedKeys(mailboxUid);
51
+ if (existing) {
52
+ return Promise.resolve(existing);
53
+ }
54
+ return new Promise<UnlockedKeys>((resolve, reject) => {
55
+ setPassword("");
56
+ setError(null);
57
+ setPending({ mailboxUid, mailboxKeys, resolve, reject });
58
+ });
59
+ }, []);
60
+
61
+ function handleCancel() {
62
+ pending?.reject(new Error("Unlock cancelled."));
63
+ setPending(null);
64
+ }
65
+
66
+ async function handleSubmit(e: React.FormEvent) {
67
+ e.preventDefault();
68
+ // Only reachable via the form below, which never renders while `pending` is null.
69
+ const request = pending!;
70
+ setError(null);
71
+ setUnlocking(true);
72
+ try {
73
+ await unlockWithPassword(request.mailboxUid, request.mailboxKeys, password);
74
+ // Always reachable: unlockWithPassword() just populated keySession.ts's session store for
75
+ // this exact mailboxUid, or threw before we get here.
76
+ const unlocked = getUnlockedKeys(request.mailboxUid)!;
77
+ request.resolve(unlocked);
78
+ setPending(null);
79
+ } catch {
80
+ // Deliberately generic - see KeyEnrollmentGate's identical handleUnlock() for why.
81
+ setError("Incorrect password.");
82
+ } finally {
83
+ setUnlocking(false);
84
+ }
85
+ }
86
+
87
+ return (
88
+ <UnlockPromptContext.Provider value={{ requestUnlock }}>
89
+ {children}
90
+ <Modal open={!!pending} onClose={handleCancel} title="Unlock your mailbox">
91
+ <p className="text-sm text-text-muted mb-5">Enter your encryption password to continue.</p>
92
+ {error && <Alert>{error}</Alert>}
93
+ <form onSubmit={handleSubmit}>
94
+ <FormField label="Encryption password" htmlFor="unlock-prompt-password">
95
+ <input
96
+ id="unlock-prompt-password"
97
+ type="password"
98
+ className="w-full text-sm border border-border rounded-sm py-1.5 px-2 bg-surface"
99
+ value={password}
100
+ onChange={(e) => setPassword(e.target.value)}
101
+ disabled={unlocking}
102
+ autoComplete="current-password"
103
+ autoFocus
104
+ />
105
+ </FormField>
106
+ <div className="flex gap-2">
107
+ <Button type="submit" loading={unlocking} disabled={unlocking}>
108
+ Unlock
109
+ </Button>
110
+ <Button type="button" variant="secondary" onClick={handleCancel} disabled={unlocking}>
111
+ Cancel
112
+ </Button>
113
+ </div>
114
+ </form>
115
+ </Modal>
116
+ </UnlockPromptContext.Provider>
117
+ );
118
+ }
119
+
120
+ /** Throws outside an `<UnlockPromptProvider>` - every app shell mounts one (see `AppShell.tsx`), so
121
+ * reaching this from any real page is a wiring bug, not a runtime condition to degrade gracefully from. */
122
+ export function useUnlockPrompt(): UnlockPromptContextValue {
123
+ const ctx = useContext(UnlockPromptContext);
124
+ if (!ctx) {
125
+ throw new Error("useUnlockPrompt() must be used within an UnlockPromptProvider (see AppShell.tsx).");
126
+ }
127
+ return ctx;
128
+ }