feat(postbox): add governed content protection profiles
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -164,6 +164,10 @@ export default function PostboxPage({
|
||||
() => groupings.find((grouping) => grouping.id === selectedScope) ?? null,
|
||||
[groupings, selectedScope]
|
||||
);
|
||||
const composeTarget = useMemo(
|
||||
() => postboxes.find((postbox) => postbox.id === messageDraft.postbox_id) ?? null,
|
||||
[messageDraft.postbox_id, postboxes]
|
||||
);
|
||||
const scopePostboxIds = useMemo(() => {
|
||||
if (selectedPostboxId) return [selectedPostboxId];
|
||||
if (selectedGrouping) {
|
||||
@@ -179,6 +183,9 @@ export default function PostboxPage({
|
||||
const replyDisabledReason = postboxBusyReason(false, busy)
|
||||
?? (!canReply ? POSTBOX_INTERFACE_I18N.noReplyReason : undefined)
|
||||
?? (!selectedMessage ? POSTBOX_INTERFACE_I18N.noMessage : undefined)
|
||||
?? (selectedMessage?.encryption_profile === "external_e2ee_v1"
|
||||
? "Replies to E2EE messages must be created by an approved encryption client."
|
||||
: undefined)
|
||||
?? (selectedMessage?.availability !== "available"
|
||||
? POSTBOX_INTERFACE_I18N.unavailableMessage
|
||||
: undefined);
|
||||
@@ -509,7 +516,10 @@ export default function PostboxPage({
|
||||
}
|
||||
|
||||
function openCompose() {
|
||||
const postbox = selectedPostbox ?? postboxes[0] ?? null;
|
||||
const postbox = selectedPostbox
|
||||
?? postboxes.find((item) => item.encryption_profile !== "external_e2ee_v1")
|
||||
?? postboxes[0]
|
||||
?? null;
|
||||
if (!postbox) return;
|
||||
openComposeFor(postbox);
|
||||
}
|
||||
@@ -543,7 +553,11 @@ export default function PostboxPage({
|
||||
}
|
||||
|
||||
async function submitMessage(): Promise<boolean> {
|
||||
if (!messageDraft.postbox_id || !messageDraft.subject.trim()) return false;
|
||||
if (
|
||||
!messageDraft.postbox_id
|
||||
|| !messageDraft.subject.trim()
|
||||
|| composeTarget?.encryption_profile === "external_e2ee_v1"
|
||||
) return false;
|
||||
setBusy(true);
|
||||
setError("");
|
||||
const participants = messageDraft.recipients
|
||||
@@ -1034,9 +1048,10 @@ export default function PostboxPage({
|
||||
disabled={
|
||||
busy ||
|
||||
!messageDraft.postbox_id ||
|
||||
!messageDraft.subject.trim()
|
||||
!messageDraft.subject.trim() ||
|
||||
composeTarget?.encryption_profile === "external_e2ee_v1"
|
||||
}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? ((!messageDraft.postbox_id || !messageDraft.subject.trim()) ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined)}
|
||||
disabledReason={postboxBusyReason(false, busy) ?? (composeTarget?.encryption_profile === "external_e2ee_v1" ? "This browser editor has no E2EE private-key custody. Use an approved encryption client for this Postbox." : ((!messageDraft.postbox_id || !messageDraft.subject.trim()) ? POSTBOX_INTERFACE_I18N.incompleteDraft : undefined))}
|
||||
>
|
||||
<Send size={16} /> Send
|
||||
</Button>
|
||||
@@ -1044,6 +1059,13 @@ export default function PostboxPage({
|
||||
}
|
||||
>
|
||||
<FormGrid columns={2} gap="small" collapseAt="narrow" className="postbox-compose-grid">
|
||||
{composeTarget?.encryption_profile === "external_e2ee_v1" ? (
|
||||
<div className="postbox-compose-wide">
|
||||
<DismissibleAlert tone="info" compact resetKey={composeTarget.id}>
|
||||
This Postbox requires externally produced E2EE. Use an approved client that supplies ciphertext, a signed manifest, wrapped keys, and a verified content digest; this browser editor never asks for or stores the private key.
|
||||
</DismissibleAlert>
|
||||
</div>
|
||||
) : null}
|
||||
<FormField label="Postbox" documentation={POSTBOX_FIELD_DOCUMENTATION}>
|
||||
<select
|
||||
value={messageDraft.postbox_id}
|
||||
@@ -1204,7 +1226,19 @@ function MessageDetail({
|
||||
</dl>
|
||||
</section>
|
||||
<section className="postbox-body">
|
||||
<p>{message.body_text || "No plaintext body is available for this message."}</p>
|
||||
{message.encryption_profile === "external_e2ee_v1" ? (
|
||||
<>
|
||||
<DismissibleAlert tone="info" compact resetKey={message.id}>
|
||||
This message is end-to-end encrypted. GovOPlaN stores and authorizes its envelope but cannot decrypt the content; open it with the institution's approved client.
|
||||
</DismissibleAlert>
|
||||
<dl className="postbox-provenance">
|
||||
<div><dt>Ciphertext</dt><dd>{message.ciphertext_ref || "Not recorded"}</dd></div>
|
||||
<div><dt>Signed manifest</dt><dd>{message.signed_manifest_ref || "Not recorded"}</dd></div>
|
||||
</dl>
|
||||
</>
|
||||
) : (
|
||||
<p>{message.body_text || "No plaintext body is available for this message."}</p>
|
||||
)}
|
||||
</section>
|
||||
{message.participants.length ? (
|
||||
<section className="postbox-participants">
|
||||
|
||||
Reference in New Issue
Block a user