@imola-solutions/secure-messaging
v0.4.0
Published
Secure messaging console for external users (email-authenticated). Threaded conversations linked to any record type (license applications, incidents, meetings). Consumers inject apiClient + currentUser.
Readme
@imola-solutions/secure-messaging
Secure messaging console for external users (email-authenticated). Threaded
conversations linked to any record type — license applications, trainee
applications, incidents, meetings, etc. Extracted from BOPC-ITMP. Consumers
inject an apiClient (Supabase-compatible) plus a currentUser.
Install
npm install @imola-solutions/secure-messaging @imola-solutions/uiPeer dependencies (must be installed in the host app):
react/react-dom>=18@mui/material>=5 and@emotion/react/@emotion/styled>=11@phosphor-icons/react>=2dayjs>=1
toast and dayjs are re-exported from @imola-solutions/ui, so sonner
comes in transitively via the UI package.
Setup
1. Build a secure-message service
createSecureMessageService is a factory that binds the module to the host
app's API client and current user. The API client must expose a Supabase-style
fluent surface: .from(table).select/.insert/.eq/.in/.or/.order/.limit/.maybeSingle().
import { createSecureMessageService } from "@imola-solutions/secure-messaging";
import { createClient } from "@/lib/services/client"; // host app's Supabase client
const secureMessageService = createSecureMessageService({
apiClient: createClient(),
currentUser: {
id: "USR-000",
name: "Sofia Rivers",
email: "[email protected]",
},
});2. Drop the panel into a host record page
import { RecordSecureMessagesPanel } from "@imola-solutions/secure-messaging";
<RecordSecureMessagesPanel
secureMessageService={secureMessageService}
recordType="license_applications"
recordId={application.id}
recipientName={application.applicant_first_name + " " + application.applicant_last_name}
recipientEmail={application.applicant_email}
/>The panel loads every thread linked to (recordType, recordId), merges their
messages chronologically, and posts replies into the newest thread — starting
a new thread if none exists.
3. Optional — application link picker in a global composer
import { ApplicationLinkPicker } from "@imola-solutions/secure-messaging";
<ApplicationLinkPicker
secureMessageService={secureMessageService}
onChange={(link) => setLink(link)} // { recordType, recordId } or null
/>API
createSecureMessageService({ apiClient, currentUser, onAudit?, aiExtensions? })
Returns { listRecordThreads, sendRecordMessage, searchApplications, summarizeThread, translateMessage, hasAi, currentUser }.
apiClient(required) — Supabase-compatible fluent client.currentUser(required) —{ id, name, email }for the signed-in staff user.onAudit(optional) — see Compliance & audit.aiExtensions(optional) — see AI extensions.
Compliance & audit
Every mutation and every AI call emits a structured event you can pipe to your audit log (Splunk, Elastic, DB table, etc.):
const secureMessageService = createSecureMessageService({
apiClient,
currentUser,
onAudit: (event) => {
// event: { action, actorId, targetType?, targetId?, timestamp, metadata?, success }
myAuditLogger.record({
...event,
product: "bopc",
surface: "secure-messaging",
});
},
});Actions emitted:
secure-message.thread.create— new thread created for a recordsecure-message.send— message persisted (or blocked by AI moderation)secure-message.read— record threads loadedai.moderate,ai.summarize,ai.translate— AI action invoked
Audit callback exceptions are caught silently so a logger outage never breaks the messaging flow.
AI extensions
Pass any subset via aiExtensions. Each is optional — the corresponding
capability flag on service.hasAi tells your UI whether to render the action.
const secureMessageService = createSecureMessageService({
apiClient,
currentUser,
aiExtensions: {
// Pre-send gate. Return { allowed: false, reason } to block a message.
moderateMessage: async (content) => {
const result = await callModerationAPI(content);
return { allowed: result.score < 0.8, reason: result.category };
},
summarizeThread: async (messages) => callLLM(`Summarize: ${JSON.stringify(messages)}`),
// For external recipients in different languages.
translateMessage: async (content, targetLang) => callTranslator(content, targetLang),
},
});
if (secureMessageService.hasAi.moderate) {
// Enable the AI-moderated send button
}Moderation flow. When moderateMessage returns { allowed: false },
sendRecordMessage returns { data: null, error } and emits an audit event
with metadata.reason === "ai-moderation-blocked". If the moderator itself
throws, the send fails open (message is persisted).
Data model
The service expects two Postgres tables (or a compatible view surface):
secure_message_threads(id, related_record_type, related_record_id, created_by, recipient_email, recipient_name, created_at, updated_at)secure_messages(id, thread_id, sender_id, sender_type, content, created_at)
The application-link-picker additionally queries license_applications and
trainee_applications (columns id, applicant_first_name, applicant_last_name,
confirmation_code, created_at). Pass secureMessageService.searchApplications(q, { tables })
with a custom tables list to point at different lookup surfaces.
What was refactored from BOPC
- The BOPC
secure-message-service.jsmodule (thinapiFetchwrappers over/api/secure-messages/*) was replaced by a factory that speaks the same Supabase-fluent dialect as the rest of the imola-components monorepo. If your host app still runs the REST router, wrap it in a Supabase-compatible shim before injecting. - The panel now takes an injected
secureMessageServiceprop; the picker no longer importscreateClientdirectly. sonnertoastis consumed through@imola-solutions/ui(as with chat).
