@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.
- package/apps/admin/branding/index.tsx +39 -404
- package/apps/admin/domains/[uid].tsx +92 -259
- package/apps/admin/encryption-policy/index.tsx +19 -0
- package/apps/admin/index.tsx +100 -82
- package/apps/admin/mailbox-policy/index.tsx +19 -0
- package/apps/admin/mailboxes/new/index.tsx +28 -291
- package/apps/admin/plugins/index.tsx +15 -0
- package/apps/admin/retention-policy/index.tsx +39 -132
- package/apps/admin/setup/index.tsx +15 -0
- package/apps/shared/components/admin/layout/AdminShell.tsx +249 -210
- package/apps/shared/components/admin/settings/BrandingForm.tsx +374 -0
- package/apps/shared/components/admin/settings/DomainDnsSetup.tsx +176 -0
- package/apps/shared/components/admin/settings/EncryptionPolicyForm.tsx +104 -0
- package/apps/shared/components/admin/settings/LoadedSettingsForm.tsx +34 -0
- package/apps/shared/components/admin/settings/MailboxCreateForm.tsx +302 -0
- package/apps/shared/components/admin/settings/MailboxPolicyForm.tsx +119 -0
- package/apps/shared/components/admin/settings/PluginsManager.tsx +595 -0
- package/apps/shared/components/admin/settings/RetentionPolicyForm.tsx +98 -0
- package/apps/shared/components/admin/setup/EscrowSetupStep.tsx +254 -0
- package/apps/shared/components/admin/setup/SetupWizard.tsx +300 -0
- package/apps/shared/components/calendar/CalendarListSidebar.tsx +120 -76
- package/apps/shared/components/calendar/EventModal.tsx +40 -4
- package/apps/shared/components/calendar/layout/CalendarShell.tsx +80 -99
- package/apps/shared/components/contacts/ContactForm.tsx +38 -4
- package/apps/shared/components/layout/AppShell.tsx +198 -169
- package/apps/shared/components/layout/KeyEnrollmentGate.tsx +298 -265
- package/apps/shared/components/layout/UnlockPromptProvider.tsx +128 -0
- package/apps/shared/components/mail/MessageDetailPane.tsx +630 -595
- package/apps/shared/components/mail/compose/ComposeContext.tsx +8 -3
- package/apps/shared/components/mail/compose/ComposeWindow.tsx +930 -765
- package/apps/shared/components/mail/layout/MailShell.tsx +408 -302
- package/apps/shared/components/settings/layout/SettingsShell.tsx +1 -0
- package/apps/shared/mail/findWellKnownFolderUid.ts +13 -0
- package/apps/shared/search/LocalIndexLifecycle.tsx +89 -0
- package/apps/shared/search/localIndexBlockCipher.ts +83 -0
- package/apps/shared/search/localIndexBuilder.ts +179 -0
- package/apps/shared/search/localIndexKey.ts +27 -0
- package/apps/shared/search/localIndexRpcClient.ts +116 -0
- package/apps/shared/search/localIndexSchema.ts +227 -0
- package/apps/shared/search/localIndexSizePreference.ts +88 -0
- package/apps/shared/search/localIndexVFS.ts +235 -0
- package/apps/shared/search/localIndexWorker.ts +464 -0
- package/apps/shared/search/searchTier2.ts +79 -0
- package/apps/shared/search/wa-sqlite-shims.d.ts +44 -0
- package/apps/www/calendar/index.tsx +31 -21
- package/apps/www/contacts/index.tsx +33 -6
- package/apps/www/index.tsx +661 -106
- package/apps/www/messages/[uid].tsx +5 -1
- package/apps/www/settings/encryption/index.tsx +63 -1
- package/apps/www/settings/sharing/index.tsx +271 -0
- package/apps/www/tasks/index.tsx +53 -4
- package/dist/apps/admin/branding/index.js +4 -98
- package/dist/apps/admin/domains/[uid].js +5 -68
- package/dist/apps/admin/encryption-policy/index.js +8 -0
- package/dist/apps/admin/index.js +14 -1
- package/dist/apps/admin/mailbox-policy/index.js +8 -0
- package/dist/apps/admin/mailboxes/new/index.js +4 -89
- package/dist/apps/admin/plugins/index.js +6 -0
- package/dist/apps/admin/retention-policy/index.js +3 -36
- package/dist/apps/admin/setup/index.js +6 -0
- package/dist/apps/shared/components/admin/layout/AdminShell.js +34 -3
- package/dist/apps/shared/components/admin/settings/BrandingForm.js +105 -0
- package/dist/apps/shared/components/admin/settings/DomainDnsSetup.js +77 -0
- package/dist/apps/shared/components/admin/settings/EncryptionPolicyForm.js +57 -0
- package/dist/apps/shared/components/admin/settings/LoadedSettingsForm.js +25 -0
- package/dist/apps/shared/components/admin/settings/MailboxCreateForm.js +106 -0
- package/dist/apps/shared/components/admin/settings/MailboxPolicyForm.js +52 -0
- package/dist/apps/shared/components/admin/settings/PluginsManager.js +260 -0
- package/dist/apps/shared/components/admin/settings/RetentionPolicyForm.js +44 -0
- package/dist/apps/shared/components/admin/setup/EscrowSetupStep.js +131 -0
- package/dist/apps/shared/components/admin/setup/SetupWizard.js +142 -0
- package/dist/apps/shared/components/calendar/CalendarListSidebar.js +24 -13
- package/dist/apps/shared/components/calendar/EventModal.js +16 -4
- package/dist/apps/shared/components/calendar/layout/CalendarShell.js +47 -39
- package/dist/apps/shared/components/contacts/ContactForm.js +16 -5
- package/dist/apps/shared/components/layout/AppShell.js +30 -7
- package/dist/apps/shared/components/layout/KeyEnrollmentGate.js +15 -3
- package/dist/apps/shared/components/layout/UnlockPromptProvider.js +77 -0
- package/dist/apps/shared/components/mail/MessageDetailPane.js +27 -2
- package/dist/apps/shared/components/mail/compose/ComposeContext.js +2 -2
- package/dist/apps/shared/components/mail/compose/ComposeWindow.js +134 -14
- package/dist/apps/shared/components/mail/layout/MailShell.js +106 -40
- package/dist/apps/shared/components/settings/layout/SettingsShell.js +1 -0
- package/dist/apps/shared/mail/findWellKnownFolderUid.js +12 -0
- package/dist/apps/shared/search/LocalIndexLifecycle.js +76 -0
- package/dist/apps/shared/search/localIndexBlockCipher.js +63 -0
- package/dist/apps/shared/search/localIndexBuilder.js +153 -0
- package/dist/apps/shared/search/localIndexKey.js +25 -0
- package/dist/apps/shared/search/localIndexRpcClient.js +78 -0
- package/dist/apps/shared/search/localIndexSchema.js +179 -0
- package/dist/apps/shared/search/localIndexSizePreference.js +78 -0
- package/dist/apps/shared/search/localIndexVFS.js +230 -0
- package/dist/apps/shared/search/localIndexWorker.js +341 -0
- package/dist/apps/shared/search/searchTier2.js +38 -0
- package/dist/apps/www/calendar/index.js +14 -12
- package/dist/apps/www/contacts/index.js +15 -6
- package/dist/apps/www/index.js +496 -94
- package/dist/apps/www/messages/[uid].js +5 -1
- package/dist/apps/www/settings/encryption/index.js +30 -2
- package/dist/apps/www/settings/sharing/index.js +128 -0
- package/dist/apps/www/tasks/index.js +27 -5
- 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
|
+
}
|