mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-02 15:03:39 +00:00
chore: chat types update
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
export * from "./payments";
|
export * from "./payments";
|
||||||
export * from "./payment-messaging";
|
export * from "./payment-messaging";
|
||||||
|
export * from "./support-attachments";
|
||||||
|
|
||||||
export interface BaseEntity {
|
export interface BaseEntity {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
73
packages/types/src/common/support-attachments.ts
Normal file
73
packages/types/src/common/support-attachments.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Rules shared by the freight and passenger support-chat attachment flows.
|
||||||
|
*
|
||||||
|
* The two chat backends are independent implementations (freight: TypeORM +
|
||||||
|
* polymorphic `FileRecord`; passenger: Prisma + `SupportAttachment`), but the
|
||||||
|
* *contract* a client codes against — what may be uploaded, how large, how the
|
||||||
|
* preview URL behaves — must not drift between them. Keep the limits here so
|
||||||
|
* both APIs validate identically and all four web apps can render one consistent
|
||||||
|
* "file too large / type not allowed" message.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** `FileRecord.resource` discriminator for freight chat attachments. */
|
||||||
|
export const SUPPORT_ATTACHMENT_RESOURCE = "support_message";
|
||||||
|
|
||||||
|
/** Per-file ceiling. Enforced server-side; the UI pre-checks to fail fast. */
|
||||||
|
export const SUPPORT_ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
/** Max files on a single message. */
|
||||||
|
export const SUPPORT_ATTACHMENT_MAX_PER_MESSAGE = 10;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How long a minted preview URL stays valid. Long enough that an open thread
|
||||||
|
* doesn't rot mid-read, short enough that a leaked URL isn't a durable grant.
|
||||||
|
*/
|
||||||
|
export const SUPPORT_ATTACHMENT_URL_TTL_SECONDS = 60 * 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Types accepted on a chat message.
|
||||||
|
*
|
||||||
|
* Deliberately NARROWER than `FilesService.ALLOWED_UPLOAD_MIME` (which also
|
||||||
|
* serves generated PDFs and scanned business documents at 25MB). Chat is
|
||||||
|
* user-to-user, so the blast radius of a bad file is another human clicking it.
|
||||||
|
*
|
||||||
|
* `image/svg+xml` is excluded on purpose and must stay excluded: an SVG is
|
||||||
|
* executable markup, and previewing one inline (`<img>` is safe, but an
|
||||||
|
* `<iframe>`/direct navigation is not) executes any script it carries under the
|
||||||
|
* serving origin. Nothing in chat needs vector uploads.
|
||||||
|
*/
|
||||||
|
export const SUPPORT_ATTACHMENT_ALLOWED_MIME: readonly string[] = [
|
||||||
|
// images (previewable inline)
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/gif",
|
||||||
|
// documents
|
||||||
|
"application/pdf",
|
||||||
|
"application/msword",
|
||||||
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
|
"application/vnd.ms-excel",
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
"text/csv",
|
||||||
|
"text/plain",
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Image subset — these are the ones worth rendering as a thumbnail. */
|
||||||
|
export const SUPPORT_ATTACHMENT_IMAGE_MIME: readonly string[] = [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
"image/gif",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function isSupportAttachmentImage(mimeType: string): boolean {
|
||||||
|
return SUPPORT_ATTACHMENT_IMAGE_MIME.includes(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSupportAttachmentAllowed(mimeType: string): boolean {
|
||||||
|
return SUPPORT_ATTACHMENT_ALLOWED_MIME.includes(mimeType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `accept` attribute for a file input / paste target. */
|
||||||
|
export const SUPPORT_ATTACHMENT_ACCEPT =
|
||||||
|
SUPPORT_ATTACHMENT_ALLOWED_MIME.join(",");
|
||||||
@@ -16,12 +16,37 @@
|
|||||||
* constants shared by the gateway (emitter) and both web apps (subscribers).
|
* constants shared by the gateway (emitter) and both web apps (subscribers).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { SUPPORT_ATTACHMENT_RESOURCE } from "../common/support-attachments";
|
||||||
|
|
||||||
/** Who authored a message — the customer side or a backoffice agent. */
|
/** Who authored a message — the customer side or a backoffice agent. */
|
||||||
export enum SupportAuthorRole {
|
export enum SupportAuthorRole {
|
||||||
CUSTOMER = "CUSTOMER",
|
CUSTOMER = "CUSTOMER",
|
||||||
AGENT = "AGENT",
|
AGENT = "AGENT",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** `FileRecord.resource` value chat attachments are stored under. */
|
||||||
|
export { SUPPORT_ATTACHMENT_RESOURCE };
|
||||||
|
|
||||||
|
/** A file attached to a support message. */
|
||||||
|
export interface SupportAttachmentDto {
|
||||||
|
/**
|
||||||
|
* FileRecord id. For an authenticated download use
|
||||||
|
* `GET /support/attachments/:id?download=1` — the generic `GET /files/:id`
|
||||||
|
* route deliberately refuses chat attachments (it has no ownership check).
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
/** Bytes. */
|
||||||
|
size: number;
|
||||||
|
/**
|
||||||
|
* Short-lived signed URL for inline preview (`<img src>`), minted per response.
|
||||||
|
* Expires — see SUPPORT_ATTACHMENT_URL_TTL_SECONDS. Clients must not persist it;
|
||||||
|
* refetch the thread to renew.
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** A single chat message on the wire. */
|
/** A single chat message on the wire. */
|
||||||
export interface SupportMessageDto {
|
export interface SupportMessageDto {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,7 +55,9 @@ export interface SupportMessageDto {
|
|||||||
authorRole: SupportAuthorRole;
|
authorRole: SupportAuthorRole;
|
||||||
/** Display name of the author, resolved at send time (best-effort). */
|
/** Display name of the author, resolved at send time (best-effort). */
|
||||||
authorName?: string | null;
|
authorName?: string | null;
|
||||||
|
/** Empty string for attachment-only messages. */
|
||||||
body: string;
|
body: string;
|
||||||
|
attachments: SupportAttachmentDto[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,9 +81,17 @@ export interface SupportConversationDto {
|
|||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Post a message. The portal omits the id; the thread is implied by the company. */
|
/**
|
||||||
|
* Post a message. The portal omits the id; the thread is implied by the company.
|
||||||
|
*
|
||||||
|
* Attachments do not travel in this shape — a message carrying files is sent as
|
||||||
|
* `multipart/form-data` with a `body` field plus one or more `attachments` file
|
||||||
|
* parts, so the files are written with `resourceId = message.id` in the same
|
||||||
|
* request. There is no staging area and therefore no orphan-file GC to run.
|
||||||
|
*/
|
||||||
export interface SendSupportMessageDto {
|
export interface SendSupportMessageDto {
|
||||||
body: string;
|
/** Optional only when the request carries at least one attachment. */
|
||||||
|
body?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Agent opens a thread with a company that has none yet. */
|
/** Agent opens a thread with a company that has none yet. */
|
||||||
@@ -81,6 +116,24 @@ export interface SupportConversationListResult {
|
|||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A page of messages, walking **backwards** from newest.
|
||||||
|
*
|
||||||
|
* Chat pages by keyset, not by offset: an inbound message while an agent is
|
||||||
|
* scrolled back would shift every offset by one and duplicate/skip rows across
|
||||||
|
* pages. The cursor pins a fixed point in (createdAt, id), so concurrent inserts
|
||||||
|
* at the head can't disturb pages already read.
|
||||||
|
*/
|
||||||
|
export interface SupportMessageListResult {
|
||||||
|
/** Oldest-first *within the page*, so a page appends/prepends as a block. */
|
||||||
|
items: SupportMessageDto[];
|
||||||
|
/**
|
||||||
|
* Opaque cursor for the next (older) page; null when the thread's start has
|
||||||
|
* been reached. Pass back as `before`.
|
||||||
|
*/
|
||||||
|
nextCursor: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Socket.io event names pushed server → client on the `support-chat` namespace. */
|
/** Socket.io event names pushed server → client on the `support-chat` namespace. */
|
||||||
export const SUPPORT_CHAT_WS_EVENTS = {
|
export const SUPPORT_CHAT_WS_EVENTS = {
|
||||||
/** A new message was added to a conversation the socket can see. */
|
/** A new message was added to a conversation the socket can see. */
|
||||||
|
|||||||
@@ -23,6 +23,33 @@ export enum PassengerSupportSender {
|
|||||||
AGENT = "AGENT",
|
AGENT = "AGENT",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A file attached to a passenger support message.
|
||||||
|
*
|
||||||
|
* Structurally identical to the freight `SupportAttachmentDto` — kept as its own
|
||||||
|
* declaration because the two namespaces are independently versioned and the
|
||||||
|
* backing stores differ (Prisma `SupportAttachment` here, polymorphic
|
||||||
|
* `FileRecord` in freight). The upload rules themselves are shared: see
|
||||||
|
* `SUPPORT_ATTACHMENT_*` in `common/support-attachments`.
|
||||||
|
*/
|
||||||
|
export interface PassengerSupportAttachmentDto {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
mimeType: string;
|
||||||
|
/** Bytes. */
|
||||||
|
size: number;
|
||||||
|
/**
|
||||||
|
* Short-lived signed URL — serves both inline preview and download. Expires
|
||||||
|
* (see SUPPORT_ATTACHMENT_URL_TTL_SECONDS); clients must not persist it,
|
||||||
|
* refetch the thread to renew.
|
||||||
|
*
|
||||||
|
* There is no API-streamed alternative here, unlike freight: this app has no
|
||||||
|
* general file endpoint, so the signed URL is the only handle. It is minted
|
||||||
|
* only into responses the caller was already authorized to receive.
|
||||||
|
*/
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** A single chat message on the wire. */
|
/** A single chat message on the wire. */
|
||||||
export interface PassengerSupportMessageDto {
|
export interface PassengerSupportMessageDto {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -30,7 +57,9 @@ export interface PassengerSupportMessageDto {
|
|||||||
sender: PassengerSupportSender;
|
sender: PassengerSupportSender;
|
||||||
/** Display name of the author, best-effort. */
|
/** Display name of the author, best-effort. */
|
||||||
authorName?: string | null;
|
authorName?: string | null;
|
||||||
|
/** Empty string for attachment-only messages. */
|
||||||
text: string;
|
text: string;
|
||||||
|
attachments: PassengerSupportAttachmentDto[];
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,15 +103,43 @@ export interface CreateGuestSupportConversationDto {
|
|||||||
initialMessage: string;
|
initialMessage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Post a message into an existing conversation. */
|
/**
|
||||||
|
* Post a message into an existing conversation.
|
||||||
|
*
|
||||||
|
* As in freight, attachments travel as `multipart/form-data` (a `text` field
|
||||||
|
* plus `attachments` file parts) rather than as ids in this body, so files are
|
||||||
|
* persisted against the message that owns them in one request.
|
||||||
|
*/
|
||||||
export interface SendPassengerSupportMessageDto {
|
export interface SendPassengerSupportMessageDto {
|
||||||
text: string;
|
/** Optional only when the request carries at least one attachment. */
|
||||||
|
text?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The single device-scoped thread for the portal: conversation + its messages. */
|
/**
|
||||||
|
* The single device-scoped thread for the portal: conversation + its newest page
|
||||||
|
* of messages.
|
||||||
|
*
|
||||||
|
* `messages` is the *first page only* (newest N, oldest-first within the page) —
|
||||||
|
* it is not the whole thread. Page backwards with `nextCursor` via the messages
|
||||||
|
* endpoint, exactly as the backoffice does.
|
||||||
|
*/
|
||||||
export interface PassengerSupportThreadDto {
|
export interface PassengerSupportThreadDto {
|
||||||
conversation: PassengerSupportConversationDto | null;
|
conversation: PassengerSupportConversationDto | null;
|
||||||
messages: PassengerSupportMessageDto[];
|
messages: PassengerSupportMessageDto[];
|
||||||
|
/** Cursor for the next (older) page; null when the thread's start is loaded. */
|
||||||
|
nextCursor: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A page of messages, walking backwards from newest. Keyset — not offset — so a
|
||||||
|
* message arriving while the reader is scrolled back cannot shift or duplicate
|
||||||
|
* pages already fetched. See the freight twin for the full rationale.
|
||||||
|
*/
|
||||||
|
export interface PassengerSupportMessageListResult {
|
||||||
|
/** Oldest-first within the page. */
|
||||||
|
items: PassengerSupportMessageDto[];
|
||||||
|
/** Pass back as `before`; null at the start of the thread. */
|
||||||
|
nextCursor: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Paginated list envelope for the conversations list endpoints. */
|
/** Paginated list envelope for the conversations list endpoints. */
|
||||||
|
|||||||
Reference in New Issue
Block a user