Files
govoplan-postbox/webui/src/api/postbox.ts
T

765 lines
20 KiB
TypeScript

import {
apiFetch,
apiPath,
apiPostJson,
apiUrl,
authHeaders,
type ApiSettings
} from "@govoplan/core-webui";
export type PostboxAccessDecision = {
allowed: boolean;
action: string;
postbox_id: string;
reason_code: string;
explanation: string;
organization_unit_id?: string | null;
function_id?: string | null;
assignment_ids: string[];
assignment_sources: string[];
selected_assignment_id?: string | null;
holder_count: number;
vacant: boolean;
classification: string;
classification_allowed: boolean;
binding_status: string;
};
export type PostboxDirectoryItem = {
id: string;
tenant_id: string;
address: string;
address_key: string;
name: string;
status: string;
classification: string;
organization_unit_id?: string | null;
organization_unit_name?: string | null;
function_id?: string | null;
function_name?: string | null;
context_key?: string | null;
template_revision_id?: string | null;
holder_count: number;
vacant: boolean;
encryption_profile: PostboxProtectionProfileId;
key_epoch: number;
encryption_vault_id?: string | null;
protection_policy: PostboxProtectionPolicy;
grouping_policy: PostboxGroupingPolicy;
access?: PostboxAccessDecision | null;
resource_revision: number;
etag: string;
};
export type PostboxProtectionProfileId =
| "plaintext_v1"
| "server_envelope_v1"
| "external_e2ee_v1";
export type PostboxProtectionPolicy = {
new_incumbent_history: "all_retained" | "since_assignment" | "bounded_days";
history_days?: number | null;
ordinary_rotation: "rewrap" | "reencrypt";
compromise_rotation: "rewrap" | "reencrypt";
recovery_authority: "disabled" | "user_consent" | "institutional_key_holders" | "dual_control";
recovery_quorum: number;
handover_authority: "user_consent" | "institutional_key_holders" | "dual_control";
handover_quorum: number;
emergency_access: "disabled" | "dual_control";
emergency_quorum: number;
export_authority: "user_consent" | "institutional_key_holders" | "dual_control";
export_quorum: number;
destruction_authority: "institutional_key_holders" | "dual_control";
destruction_quorum: number;
external_recipient_assurance: "disabled" | "email_otp" | "strong_identity";
vacancy_escalation_content_access: "metadata_only";
};
export type PostboxGroupingPolicy = {
mode: "allow" | "same_classification" | "separate";
reason?: string | null;
};
export type PostboxProtectionProfile = {
id: PostboxProtectionProfileId;
label: string;
description: string;
server_can_decrypt: boolean;
requires_encryption_module: boolean;
requires_external_client: boolean;
available: boolean;
standard: boolean;
};
export type PostboxProtectionTransition = {
id: string;
postbox_id: string;
source_profile: string;
target_profile: string;
source_vault_id?: string | null;
target_vault_id?: string | null;
history_mode: string;
authority_mode: string;
required_quorum: number;
evidence_refs: string[];
reason: string;
state: string;
message_count: number;
completed_count: number;
failed_count: number;
requested_by?: string | null;
activated_at?: string | null;
completed_at?: string | null;
resource_revision: number;
etag: string;
configuration_snapshot: Record<string, unknown>;
items: Array<{
id: string;
message_id: string;
source_profile: string;
target_profile: string;
state: string;
source_digest?: string | null;
target_digest?: string | null;
error_code?: string | null;
}>;
};
export type PostboxParticipant = {
kind: string;
reference_type: string;
reference_id?: string | null;
label?: string | null;
address?: string | null;
};
export type PostboxAttachment = {
reference_type: string;
reference_id: string;
name?: string | null;
media_type?: string | null;
size_bytes?: number | null;
digest?: string | null;
metadata: Record<string, unknown>;
};
export type PostboxAttachmentResolution = PostboxAttachment & {
available: boolean;
reason_code: string;
file_asset_id?: string | null;
file_version_id?: string | null;
download_path?: string | null;
provenance: Record<string, unknown>;
};
export type PostboxMessage = {
id: string;
tenant_id: string;
postbox_id: string;
subject: string;
body_text?: string | null;
status: string;
availability: "available" | "withdrawn" | "expired";
classification: string;
sender_label?: string | null;
delivered_at: string;
read_at?: string | null;
acknowledged_at?: string | null;
expires_at?: string | null;
withdrawn_at?: string | null;
producer_module?: string | null;
producer_resource_type?: string | null;
producer_resource_id?: string | null;
in_reply_to_message_id?: string | null;
replaces_message_id?: string | null;
encryption_profile: PostboxProtectionProfileId;
key_epoch: number;
ciphertext_ref?: string | null;
signed_manifest_ref?: string | null;
wrapped_keys: Array<{
recipient_type: string;
recipient_id: string;
key_epoch: number;
wrapped_key_ref: string;
algorithm?: string | null;
metadata: Record<string, unknown>;
}>;
external_recipient_tokens: Array<{
token_id: string;
state: "pending" | "available" | "fetched" | "expired" | "revoked";
expires_at?: string | null;
one_time: boolean;
key_fetched_at?: string | null;
revoked_at?: string | null;
assurance_profile?: string | null;
metadata: Record<string, unknown>;
}>;
participants: PostboxParticipant[];
attachments: PostboxAttachment[];
metadata: Record<string, unknown>;
};
export type PostboxGrouping = {
id: string;
name: string;
is_default: boolean;
postbox_ids: string[];
total_count: number;
unread_count: number;
constraints: Array<{
code: "source_requires_separation" | "classification_separation_required";
mode: PostboxGroupingPolicy["mode"];
postbox_id: string;
reason?: string | null;
enforced_by: "postbox_configuration";
}>;
resource_revision: number;
etag: string;
created_at: string;
updated_at: string;
};
export type PostboxOrganizationFunction = {
id: string;
slug: string;
name: string;
function_type_id?: string | null;
delegable: boolean;
act_in_place_allowed: boolean;
};
export type PostboxOrganizationUnit = {
id: string;
slug: string;
name: string;
unit_type_id?: string | null;
parent_id?: string | null;
functions: PostboxOrganizationFunction[];
};
export type PostboxOrganizationRelationType = {
id: string;
slug: string;
name: string;
structure_id?: string | null;
is_hierarchical: boolean;
status: string;
};
export type PostboxOrganizationStructure = {
id: string;
slug: string;
name: string;
structure_kind: string;
status: string;
relation_types: PostboxOrganizationRelationType[];
};
export type PostboxOrganizationTargets = {
units: PostboxOrganizationUnit[];
structures: PostboxOrganizationStructure[];
};
export type PostboxRoutingPolicy = {
linked_copy: {
enabled: boolean;
structure_id?: string | null;
relation_type_ids: string[];
max_depth: number;
stop_unit_id?: string | null;
stop_unit_type_id?: string | null;
target_function_type_id?: string | null;
target_template_id?: string | null;
fanout: "nearest" | "all";
allowed_classifications: string[];
allowed_producer_modules: string[];
require_expiry: boolean;
max_retention_days?: number | null;
};
attention: {
mode: "none" | "vacancy_escalation";
delay_minutes?: number | null;
};
shared_visibility: {
mode: "none";
};
};
export type PostboxTemplateRevision = {
id: string;
revision: number;
function_type_id?: string | null;
scope_kind: "tenant" | "unit" | "subtree" | "unit_type";
scope_id?: string | null;
scope_structure_id?: string | null;
scope_relation_type_ids: string[];
name_pattern: string;
address_pattern: string;
classification: string;
allow_vacant_delivery: boolean;
portal_visible: boolean;
encryption_profile: string;
encryption_vault_id?: string | null;
protection_policy: PostboxProtectionPolicy;
grouping_policy: PostboxGroupingPolicy;
history_policy: Record<string, unknown>;
routing_policy: PostboxRoutingPolicy;
retention_policy: Record<string, unknown>;
published_at?: string | null;
created_at: string;
};
export type PostboxTemplate = {
id: string;
tenant_id: string;
slug: string;
name: string;
description?: string | null;
status: string;
current_revision: number;
resource_revision: number;
etag: string;
published_revision_id?: string | null;
revisions: PostboxTemplateRevision[];
created_at: string;
updated_at: string;
};
export type PostboxTemplateRevisionPayload = Pick<
PostboxTemplateRevision,
| "function_type_id"
| "scope_kind"
| "scope_id"
| "scope_structure_id"
| "scope_relation_type_ids"
| "name_pattern"
| "address_pattern"
| "classification"
| "allow_vacant_delivery"
| "portal_visible"
| "encryption_profile"
| "encryption_vault_id"
| "protection_policy"
| "grouping_policy"
| "routing_policy"
>;
export type PostboxTemplateCreatePayload = PostboxTemplateRevisionPayload & {
slug: string;
name: string;
description?: string | null;
};
export type PostboxTemplatePreviewTarget = {
organization_unit_id: string;
organization_unit_name: string;
function_id: string;
function_name: string;
address: string;
name: string;
holder_count: number;
vacant: boolean;
status: string;
existing_postbox_id?: string | null;
diagnostics: string[];
};
export type PostboxTemplatePreview = {
targets: PostboxTemplatePreviewTarget[];
total: number;
ready_count: number;
existing_count: number;
vacant_count: number;
blocked_count: number;
truncated: boolean;
diagnostics: string[];
};
export type PostboxExactCreatePayload = {
name: string;
description?: string | null;
organization_unit_id: string;
function_id: string;
address_key?: string | null;
classification: string;
portal_visible: boolean;
encryption_profile: PostboxProtectionProfileId;
encryption_vault_id?: string | null;
protection_policy: PostboxProtectionPolicy;
grouping_policy: PostboxGroupingPolicy;
};
export type PostboxMessageAuthoringPayload = {
idempotency_key: string;
subject: string;
body_text?: string | null;
ciphertext_ref?: string | null;
signed_manifest_ref?: string | null;
wrapped_keys?: PostboxMessage["wrapped_keys"];
classification: string;
participants: PostboxParticipant[];
attachments: PostboxAttachment[];
metadata: Record<string, unknown>;
};
type PostboxGroupingPayload = Pick<
PostboxGrouping,
"name" | "is_default" | "postbox_ids"
>;
export async function listPostboxes(settings: ApiSettings): Promise<PostboxDirectoryItem[]> {
const response = await apiFetch<{ postboxes: PostboxDirectoryItem[] }>(
settings,
"/api/v1/postbox/directory"
);
return response.postboxes;
}
export async function listPostboxMessages(
settings: ApiSettings,
postboxIds: string[],
limit = 100,
offset = 0,
query = "",
state: "all" | "unread" | "read" | "acknowledged" = "all"
): Promise<{ messages: PostboxMessage[]; total: number; limit: number; offset: number }> {
return apiFetch(
settings,
apiPath("/api/v1/postbox/messages", {
postbox_id: postboxIds,
limit,
offset,
q: query || undefined,
state
})
);
}
export function getPostboxMessage(
settings: ApiSettings,
messageId: string
): Promise<PostboxMessage> {
return apiFetch(settings, `/api/v1/postbox/messages/${encodeURIComponent(messageId)}`);
}
export async function resolvePostboxAttachments(
settings: ApiSettings,
messageId: string
): Promise<PostboxAttachmentResolution[]> {
const response = await apiFetch<{ attachments: PostboxAttachmentResolution[] }>(
settings,
`/api/v1/postbox/messages/${encodeURIComponent(messageId)}/attachment-resolutions`
);
return response.attachments;
}
export async function downloadPostboxAttachment(
settings: ApiSettings,
attachment: PostboxAttachmentResolution
): Promise<void> {
if (!attachment.available || !attachment.download_path) {
throw new Error("This attachment payload is not available.");
}
const response = await fetch(apiUrl(settings, attachment.download_path), {
headers: authHeaders(settings),
credentials: "include"
});
if (!response.ok) {
throw new Error(`Attachment download failed (${response.status}).`);
}
const objectUrl = URL.createObjectURL(await response.blob());
const link = document.createElement("a");
link.href = objectUrl;
link.download = attachment.name || attachment.reference_id;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectUrl);
}
export function markPostboxMessage(
settings: ApiSettings,
messageId: string,
state: "read" | "acknowledged"
): Promise<PostboxMessage> {
return apiFetch(
settings,
`/api/v1/postbox/messages/${encodeURIComponent(messageId)}/state`,
{
method: "PATCH",
body: JSON.stringify({ state })
}
);
}
export function createPostboxMessage(
settings: ApiSettings,
postboxId: string,
payload: PostboxMessageAuthoringPayload
): Promise<PostboxMessage> {
return apiPostJson(settings, "/api/v1/postbox/messages", {
...payload,
postbox_id: postboxId
});
}
export function replyToPostboxMessage(
settings: ApiSettings,
messageId: string,
payload: PostboxMessageAuthoringPayload
): Promise<PostboxMessage> {
return apiPostJson(
settings,
`/api/v1/postbox/messages/${encodeURIComponent(messageId)}/replies`,
payload
);
}
export async function listPostboxGroupings(settings: ApiSettings): Promise<PostboxGrouping[]> {
const response = await apiFetch<{ groupings: PostboxGrouping[] }>(
settings,
"/api/v1/postbox/groupings"
);
return response.groupings;
}
export function createPostboxGrouping(
settings: ApiSettings,
payload: PostboxGroupingPayload
): Promise<PostboxGrouping> {
return apiPostJson(settings, "/api/v1/postbox/groupings", payload);
}
export function updatePostboxGrouping(
settings: ApiSettings,
grouping: PostboxGrouping,
payload: PostboxGroupingPayload
): Promise<PostboxGrouping> {
return apiFetch(
settings,
`/api/v1/postbox/groupings/${encodeURIComponent(grouping.id)}`,
{
method: "PUT",
headers: { "If-Match": grouping.etag },
body: JSON.stringify({ ...payload, base_revision: grouping.resource_revision })
}
);
}
export function deletePostboxGrouping(
settings: ApiSettings,
grouping: PostboxGrouping
): Promise<void> {
return apiFetch(
settings,
`/api/v1/postbox/groupings/${encodeURIComponent(grouping.id)}`,
{
method: "DELETE",
headers: { "If-Match": grouping.etag },
body: JSON.stringify({ base_revision: grouping.resource_revision })
}
);
}
export async function listAdminPostboxes(settings: ApiSettings): Promise<PostboxDirectoryItem[]> {
const response = await apiFetch<{ postboxes: PostboxDirectoryItem[] }>(
settings,
"/api/v1/postbox/admin/postboxes"
);
return response.postboxes;
}
export async function listPostboxProtectionProfiles(
settings: ApiSettings
): Promise<{ standard_profile: PostboxProtectionProfileId; profiles: PostboxProtectionProfile[] }> {
return apiFetch(settings, "/api/v1/postbox/admin/protection-profiles");
}
export async function listPostboxProtectionTransitions(
settings: ApiSettings,
postboxId: string
): Promise<PostboxProtectionTransition[]> {
const response = await apiFetch<{ transitions: PostboxProtectionTransition[] }>(
settings,
`/api/v1/postbox/admin/postboxes/${encodeURIComponent(postboxId)}/protection-transitions`
);
return response.transitions;
}
export function createPostboxProtectionTransition(
settings: ApiSettings,
postbox: PostboxDirectoryItem,
payload: {
idempotency_key: string;
target_profile: PostboxProtectionProfileId;
target_vault_id?: string | null;
history_mode: "future_only" | "migrate_history";
authority_mode: "user_consent" | "institutional_key_holders" | "dual_control";
required_quorum: number;
user_consent_refs: string[];
institutional_authorization_refs: string[];
reason: string;
acknowledge_irreversibility: boolean;
}
): Promise<PostboxProtectionTransition> {
return apiPostJson(
settings,
`/api/v1/postbox/admin/postboxes/${encodeURIComponent(postbox.id)}/protection-transitions`,
{ ...payload, base_revision: postbox.resource_revision },
{ headers: { "If-Match": postbox.etag } }
);
}
export function updatePostboxProtectionPolicy(
settings: ApiSettings,
postbox: PostboxDirectoryItem,
protectionPolicy: PostboxProtectionPolicy
): Promise<PostboxDirectoryItem> {
return apiFetch(
settings,
`/api/v1/postbox/admin/postboxes/${encodeURIComponent(postbox.id)}/protection-policy`,
{
method: "PUT",
headers: { "If-Match": postbox.etag },
body: JSON.stringify({
base_revision: postbox.resource_revision,
protection_policy: protectionPolicy
})
}
);
}
export function updatePostboxGroupingPolicy(
settings: ApiSettings,
postbox: PostboxDirectoryItem,
groupingPolicy: PostboxGroupingPolicy
): Promise<PostboxDirectoryItem> {
return apiFetch(
settings,
`/api/v1/postbox/admin/postboxes/${encodeURIComponent(postbox.id)}/grouping-policy`,
{
method: "PUT",
headers: { "If-Match": postbox.etag },
body: JSON.stringify({
base_revision: postbox.resource_revision,
grouping_policy: groupingPolicy
})
}
);
}
export async function listPostboxOrganizationTargets(
settings: ApiSettings
): Promise<PostboxOrganizationTargets> {
return apiFetch<PostboxOrganizationTargets>(
settings,
"/api/v1/postbox/admin/organization-targets"
);
}
export function createExactPostbox(
settings: ApiSettings,
payload: PostboxExactCreatePayload
): Promise<PostboxDirectoryItem> {
return apiPostJson(settings, "/api/v1/postbox/admin/postboxes", payload);
}
export function archivePostbox(
settings: ApiSettings,
postbox: PostboxDirectoryItem
): Promise<PostboxDirectoryItem> {
return apiFetch(
settings,
`/api/v1/postbox/admin/postboxes/${encodeURIComponent(postbox.id)}`,
{
method: "DELETE",
headers: { "If-Match": postbox.etag },
body: JSON.stringify({ base_revision: postbox.resource_revision })
}
);
}
export async function listPostboxTemplates(settings: ApiSettings): Promise<PostboxTemplate[]> {
const response = await apiFetch<{ templates: PostboxTemplate[] }>(
settings,
"/api/v1/postbox/admin/templates"
);
return response.templates;
}
export function createPostboxTemplate(
settings: ApiSettings,
payload: PostboxTemplateCreatePayload
): Promise<PostboxTemplate> {
return apiPostJson(settings, "/api/v1/postbox/admin/templates", payload);
}
export function previewPostboxTemplate(
settings: ApiSettings,
payload: PostboxTemplateCreatePayload & {
template_id?: string | null;
context_key?: string | null;
limit?: number;
}
): Promise<PostboxTemplatePreview> {
return apiPostJson(
settings,
"/api/v1/postbox/admin/templates/preview",
payload
);
}
export function revisePostboxTemplate(
settings: ApiSettings,
template: PostboxTemplate,
payload: PostboxTemplateRevisionPayload
): Promise<PostboxTemplate> {
return apiPostJson(
settings,
`/api/v1/postbox/admin/templates/${encodeURIComponent(template.id)}/revisions`,
{ ...payload, base_revision: template.resource_revision },
{ headers: { "If-Match": template.etag } }
);
}
export function publishPostboxTemplate(
settings: ApiSettings,
template: PostboxTemplate,
revision?: number
): Promise<PostboxTemplate> {
return apiPostJson(
settings,
`/api/v1/postbox/admin/templates/${encodeURIComponent(template.id)}/publish`,
{ revision: revision ?? null, base_revision: template.resource_revision },
{ headers: { "If-Match": template.etag } }
);
}
export function retirePostboxTemplate(
settings: ApiSettings,
template: PostboxTemplate
): Promise<PostboxTemplate> {
return apiPostJson(
settings,
`/api/v1/postbox/admin/templates/${encodeURIComponent(template.id)}/retire`,
{ base_revision: template.resource_revision },
{ headers: { "If-Match": template.etag } }
);
}
export function materializePostboxTemplate(
settings: ApiSettings,
templateId: string,
payload: {
organization_unit_id: string;
function_id: string;
context_key?: string | null;
}
): Promise<PostboxDirectoryItem> {
return apiPostJson(
settings,
`/api/v1/postbox/admin/templates/${encodeURIComponent(templateId)}/materialize`,
payload
);
}