@dxv-systems/helpdesk 0.0.0-stage → 0.2.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/README.md +272 -2
- package/dist/bounce.d.ts +13 -0
- package/dist/bounce.js +79 -0
- package/dist/controls.d.ts +13 -0
- package/dist/controls.js +82 -0
- package/dist/draft-body.d.ts +4 -0
- package/dist/draft-body.js +38 -0
- package/dist/drafting.d.ts +18 -0
- package/dist/drafting.js +243 -0
- package/dist/errors.d.ts +60 -0
- package/dist/errors.js +133 -0
- package/dist/inbox/handlers.d.ts +36 -0
- package/dist/inbox/handlers.js +523 -0
- package/dist/inbox/messages.d.ts +34 -0
- package/dist/inbox/messages.js +66 -0
- package/dist/inbox/plain-text.d.ts +1 -0
- package/dist/inbox/plain-text.js +58 -0
- package/dist/inbox/types.d.ts +116 -0
- package/dist/inbox/types.js +2 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +66 -0
- package/dist/ingest.d.ts +25 -0
- package/dist/ingest.js +335 -0
- package/dist/internal.d.ts +8 -0
- package/dist/internal.js +44 -0
- package/dist/noise.d.ts +17 -0
- package/dist/noise.js +77 -0
- package/dist/react.d.ts +97 -0
- package/dist/react.js +576 -0
- package/dist/reply-attachments.d.ts +28 -0
- package/dist/reply-attachments.js +119 -0
- package/dist/sending.d.ts +34 -0
- package/dist/sending.js +268 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +10 -0
- package/dist/states.d.ts +13 -0
- package/dist/states.js +62 -0
- package/dist/subject.d.ts +2 -0
- package/dist/subject.js +30 -0
- package/dist/testing/index.d.ts +97 -0
- package/dist/testing/index.js +472 -0
- package/dist/threading.d.ts +18 -0
- package/dist/threading.js +66 -0
- package/dist/types.d.ts +339 -0
- package/dist/types.js +2 -0
- package/package.json +71 -4
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
export interface MailAddress {
|
|
2
|
+
address: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MessageBody {
|
|
6
|
+
contentType: "html" | "text";
|
|
7
|
+
content: string;
|
|
8
|
+
}
|
|
9
|
+
export interface InternetMessageHeader {
|
|
10
|
+
name: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
export type AttachmentKind = "file" | "item" | "reference";
|
|
14
|
+
export interface AttachmentInfo {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
contentType: string;
|
|
18
|
+
size: number;
|
|
19
|
+
isInline: boolean;
|
|
20
|
+
kind: AttachmentKind;
|
|
21
|
+
}
|
|
22
|
+
export interface MessageSummary {
|
|
23
|
+
id: string;
|
|
24
|
+
subject?: string;
|
|
25
|
+
from?: MailAddress;
|
|
26
|
+
receivedDateTime?: string;
|
|
27
|
+
internetMessageId?: string;
|
|
28
|
+
conversationId?: string;
|
|
29
|
+
isRead?: boolean;
|
|
30
|
+
isDraft?: boolean;
|
|
31
|
+
categories?: string[];
|
|
32
|
+
hasAttachments?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface DeltaItem extends MessageSummary {
|
|
35
|
+
removed: boolean;
|
|
36
|
+
removedReason?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface DeltaPage {
|
|
39
|
+
items: DeltaItem[];
|
|
40
|
+
nextLink: string | null;
|
|
41
|
+
deltaLink: string | null;
|
|
42
|
+
}
|
|
43
|
+
export interface DeltaOptions {
|
|
44
|
+
folderId?: string;
|
|
45
|
+
link?: string;
|
|
46
|
+
since?: Date | string;
|
|
47
|
+
pageSize?: number;
|
|
48
|
+
}
|
|
49
|
+
export interface Message extends MessageSummary {
|
|
50
|
+
body: MessageBody;
|
|
51
|
+
sentDateTime?: string;
|
|
52
|
+
toRecipients: MailAddress[];
|
|
53
|
+
ccRecipients: MailAddress[];
|
|
54
|
+
replyTo: MailAddress[];
|
|
55
|
+
internetMessageHeaders?: InternetMessageHeader[];
|
|
56
|
+
inReplyTo?: string;
|
|
57
|
+
references?: string[];
|
|
58
|
+
}
|
|
59
|
+
export interface GetMessageOptions {
|
|
60
|
+
bodyType?: "html" | "text";
|
|
61
|
+
includeHeaders?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export interface CreateReplyOptions {
|
|
64
|
+
replyAll?: boolean;
|
|
65
|
+
comment?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface MailboxGateway {
|
|
68
|
+
readonly address: string;
|
|
69
|
+
delta(options: DeltaOptions): Promise<DeltaPage>;
|
|
70
|
+
getMessage(id: string, options?: GetMessageOptions): Promise<Message | null>;
|
|
71
|
+
createReply(messageId: string, options?: CreateReplyOptions): Promise<Message>;
|
|
72
|
+
updateDraftBody(draftId: string, body: MessageBody): Promise<void>;
|
|
73
|
+
sendDraft(draftId: string): Promise<void>;
|
|
74
|
+
listAttachments?(messageId: string): Promise<AttachmentInfo[]>;
|
|
75
|
+
downloadAttachment?(messageId: string, attachmentId: string): Promise<Uint8Array>;
|
|
76
|
+
addAttachment?(draftId: string, attachment: {
|
|
77
|
+
name: string;
|
|
78
|
+
contentType: string;
|
|
79
|
+
content: Uint8Array;
|
|
80
|
+
}): Promise<unknown>;
|
|
81
|
+
}
|
|
82
|
+
export interface Clock {
|
|
83
|
+
now(): Date;
|
|
84
|
+
}
|
|
85
|
+
export interface ReplyAttachment {
|
|
86
|
+
id: string;
|
|
87
|
+
threadId: string;
|
|
88
|
+
mailbox: string;
|
|
89
|
+
fileId: string;
|
|
90
|
+
name: string;
|
|
91
|
+
contentType: string;
|
|
92
|
+
size: number;
|
|
93
|
+
addedBy: string;
|
|
94
|
+
createdAt: string;
|
|
95
|
+
placedDraftId: string | null;
|
|
96
|
+
sentIntentId: string | null;
|
|
97
|
+
}
|
|
98
|
+
export type ReplyAttachmentPatch = Partial<Pick<ReplyAttachment, "placedDraftId" | "sentIntentId">>;
|
|
99
|
+
export interface ReplyAttachmentStore {
|
|
100
|
+
add(attachment: ReplyAttachment): Promise<void>;
|
|
101
|
+
listForThread(threadId: string): Promise<ReplyAttachment[]>;
|
|
102
|
+
update(id: string, patch: ReplyAttachmentPatch): Promise<void>;
|
|
103
|
+
removeUnsent(id: string): Promise<ReplyAttachment | null>;
|
|
104
|
+
}
|
|
105
|
+
export interface StoredReplyFile {
|
|
106
|
+
fileId: string;
|
|
107
|
+
name: string;
|
|
108
|
+
contentType: string;
|
|
109
|
+
size: number;
|
|
110
|
+
}
|
|
111
|
+
export interface ReplyFiles {
|
|
112
|
+
createUpload(input: {
|
|
113
|
+
staffId: string;
|
|
114
|
+
name: string;
|
|
115
|
+
contentType: string;
|
|
116
|
+
size: number;
|
|
117
|
+
}): Promise<{
|
|
118
|
+
uploadUrl: string;
|
|
119
|
+
uploadToken: string;
|
|
120
|
+
contentType: string;
|
|
121
|
+
}>;
|
|
122
|
+
completeUpload(input: {
|
|
123
|
+
staffId: string;
|
|
124
|
+
uploadToken: string;
|
|
125
|
+
}): Promise<StoredReplyFile>;
|
|
126
|
+
read(input: {
|
|
127
|
+
fileId: string;
|
|
128
|
+
staffId: string;
|
|
129
|
+
}): Promise<Uint8Array>;
|
|
130
|
+
remove(input: {
|
|
131
|
+
fileId: string;
|
|
132
|
+
staffId: string;
|
|
133
|
+
}): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
export interface ReplyAttachmentsDeps {
|
|
136
|
+
store: ReplyAttachmentStore;
|
|
137
|
+
files: ReplyFiles;
|
|
138
|
+
}
|
|
139
|
+
export type TicketState = "new" | "ignored" | "drafted" | "review" | "sent" | "waiting" | "closed";
|
|
140
|
+
export type ReviewReason = "low_confidence" | "policy" | "stale_draft" | "drafter_failed" | "staff_edited" | "no_draft" | "delivery_failed";
|
|
141
|
+
export interface Thread {
|
|
142
|
+
id: string;
|
|
143
|
+
mailbox: string;
|
|
144
|
+
state: TicketState;
|
|
145
|
+
subjectKey: string;
|
|
146
|
+
subject: string;
|
|
147
|
+
customerAddress: string;
|
|
148
|
+
participants: string[];
|
|
149
|
+
createdAt: string;
|
|
150
|
+
lastActivityAt: string;
|
|
151
|
+
latestInboundId: string | null;
|
|
152
|
+
reviewReason: ReviewReason | null;
|
|
153
|
+
autoReplyCount: number;
|
|
154
|
+
relatedThreadId: string | null;
|
|
155
|
+
dismissedBy: string | null;
|
|
156
|
+
}
|
|
157
|
+
export type ThreadPatch = Partial<Pick<Thread, "state" | "participants" | "lastActivityAt" | "latestInboundId" | "reviewReason" | "autoReplyCount" | "dismissedBy">>;
|
|
158
|
+
export interface ThreadUpdate {
|
|
159
|
+
threadId: string;
|
|
160
|
+
expectedState: TicketState;
|
|
161
|
+
patch: ThreadPatch;
|
|
162
|
+
}
|
|
163
|
+
export type NoiseReason = "auto_submitted" | "precedence" | "bounce" | "out_of_office" | "mailing_list" | "machine_sender" | "own_address" | "connected_mailbox" | "none";
|
|
164
|
+
export interface StoredMessage {
|
|
165
|
+
id: string;
|
|
166
|
+
mailbox: string;
|
|
167
|
+
threadId: string;
|
|
168
|
+
internetMessageId: string | null;
|
|
169
|
+
conversationId: string | null;
|
|
170
|
+
subject: string;
|
|
171
|
+
fromAddress: string;
|
|
172
|
+
receivedAt: string;
|
|
173
|
+
noise: {
|
|
174
|
+
action: "ignore" | "process";
|
|
175
|
+
reason: NoiseReason;
|
|
176
|
+
};
|
|
177
|
+
suppressAutoResponse: boolean;
|
|
178
|
+
removedAt: string | null;
|
|
179
|
+
}
|
|
180
|
+
export interface SyncState {
|
|
181
|
+
link: string | null;
|
|
182
|
+
connectedAt: string;
|
|
183
|
+
lastReceivedAt: string | null;
|
|
184
|
+
failingSince?: string | null;
|
|
185
|
+
failures?: number;
|
|
186
|
+
lastError?: string | null;
|
|
187
|
+
}
|
|
188
|
+
export interface SyncFailing {
|
|
189
|
+
status: "failing";
|
|
190
|
+
failures: number;
|
|
191
|
+
failingSince: string;
|
|
192
|
+
error: string;
|
|
193
|
+
}
|
|
194
|
+
export type DraftRunStatus = "requested" | "started" | "succeeded" | "failed";
|
|
195
|
+
export interface DraftRun {
|
|
196
|
+
mailbox: string;
|
|
197
|
+
threadId: string;
|
|
198
|
+
inboundMessageId: string;
|
|
199
|
+
generation: number;
|
|
200
|
+
idempotencyKey: string;
|
|
201
|
+
status: DraftRunStatus;
|
|
202
|
+
externalRunId: string | null;
|
|
203
|
+
requestedAt: string;
|
|
204
|
+
error: string | null;
|
|
205
|
+
text: string | null;
|
|
206
|
+
confidence: number | null;
|
|
207
|
+
reason: string | null;
|
|
208
|
+
category: string | null;
|
|
209
|
+
placedDraftId: string | null;
|
|
210
|
+
placedBodyHtml: string | null;
|
|
211
|
+
commentText: string | null;
|
|
212
|
+
}
|
|
213
|
+
export type SendStatus = "intended" | "sending" | "sent" | "failed" | "unknown";
|
|
214
|
+
export interface SendIntent {
|
|
215
|
+
id: string;
|
|
216
|
+
mailbox: string;
|
|
217
|
+
threadId: string;
|
|
218
|
+
draftId: string | null;
|
|
219
|
+
approvedBy: string;
|
|
220
|
+
status: SendStatus;
|
|
221
|
+
createdAt: string;
|
|
222
|
+
updatedAt: string;
|
|
223
|
+
error: string | null;
|
|
224
|
+
}
|
|
225
|
+
export type SendIntentPatch = Partial<Pick<SendIntent, "status" | "updatedAt" | "error" | "draftId">>;
|
|
226
|
+
export interface HelpdeskStore {
|
|
227
|
+
getSyncState(mailbox: string): Promise<SyncState | null>;
|
|
228
|
+
saveSyncState(mailbox: string, state: SyncState): Promise<void>;
|
|
229
|
+
getMessage(mailbox: string, id: string): Promise<StoredMessage | null>;
|
|
230
|
+
markMessageRemoved(mailbox: string, id: string, at: string): Promise<void>;
|
|
231
|
+
getThread(id: string): Promise<Thread | null>;
|
|
232
|
+
getLatestMessageForThread(threadId: string): Promise<StoredMessage | null>;
|
|
233
|
+
findThreadsByConversationId(mailbox: string, conversationId: string): Promise<Thread[]>;
|
|
234
|
+
findMessageThreads(mailbox: string, internetMessageIds: string[]): Promise<Array<{
|
|
235
|
+
threadId: string;
|
|
236
|
+
internetMessageId: string;
|
|
237
|
+
}>>;
|
|
238
|
+
findThreadsBySubjectKey(mailbox: string, subjectKey: string, activeSince: string): Promise<Thread[]>;
|
|
239
|
+
createThread(thread: Thread, firstMessage: StoredMessage): Promise<"created" | "duplicate">;
|
|
240
|
+
appendMessage(message: StoredMessage, update: ThreadUpdate): Promise<"appended" | "duplicate" | "stale">;
|
|
241
|
+
updateThread(update: ThreadUpdate): Promise<boolean>;
|
|
242
|
+
createDraftRunIfAbsent(run: DraftRun): Promise<{
|
|
243
|
+
created: boolean;
|
|
244
|
+
run: DraftRun;
|
|
245
|
+
}>;
|
|
246
|
+
getLatestDraftRun(mailbox: string, inboundMessageId: string): Promise<DraftRun | null>;
|
|
247
|
+
getLatestDraftRunForThread(threadId: string): Promise<DraftRun | null>;
|
|
248
|
+
listInFlightDraftRuns(mailbox: string, olderThan: string): Promise<DraftRun[]>;
|
|
249
|
+
updateDraftRun(key: string, patch: Partial<Omit<DraftRun, "idempotencyKey">>): Promise<DraftRun>;
|
|
250
|
+
listSendIntentsByStatus?(mailbox: string, status: SendStatus): Promise<SendIntent[]>;
|
|
251
|
+
createSendIntentIfAbsent(intent: SendIntent): Promise<{
|
|
252
|
+
created: boolean;
|
|
253
|
+
intent: SendIntent;
|
|
254
|
+
}>;
|
|
255
|
+
getSendIntent(id: string): Promise<SendIntent | null>;
|
|
256
|
+
transitionSendIntent(id: string, from: SendStatus[], patch: SendIntentPatch): Promise<SendIntent | null>;
|
|
257
|
+
}
|
|
258
|
+
export interface DraftRequest {
|
|
259
|
+
idempotencyKey: string;
|
|
260
|
+
mailbox: string;
|
|
261
|
+
threadId: string;
|
|
262
|
+
messageId: string;
|
|
263
|
+
conversationId: string | null;
|
|
264
|
+
subject: string;
|
|
265
|
+
from: string;
|
|
266
|
+
}
|
|
267
|
+
export type DraftResult = {
|
|
268
|
+
ok: true;
|
|
269
|
+
text: string;
|
|
270
|
+
confidence: number;
|
|
271
|
+
reason: string;
|
|
272
|
+
category?: string;
|
|
273
|
+
} | {
|
|
274
|
+
ok: false;
|
|
275
|
+
error: string;
|
|
276
|
+
};
|
|
277
|
+
export interface Drafter {
|
|
278
|
+
start(request: DraftRequest): Promise<{
|
|
279
|
+
runId: string;
|
|
280
|
+
}>;
|
|
281
|
+
}
|
|
282
|
+
export type HelpdeskNotification = {
|
|
283
|
+
type: "review_needed";
|
|
284
|
+
threadId: string;
|
|
285
|
+
mailbox: string;
|
|
286
|
+
subject: string;
|
|
287
|
+
reason: ReviewReason;
|
|
288
|
+
} | {
|
|
289
|
+
type: "send_outcome_unknown";
|
|
290
|
+
threadId: string;
|
|
291
|
+
mailbox: string;
|
|
292
|
+
subject: string;
|
|
293
|
+
draftId: string | null;
|
|
294
|
+
} | {
|
|
295
|
+
type: "sync_failing";
|
|
296
|
+
mailbox: string;
|
|
297
|
+
since: string;
|
|
298
|
+
} | {
|
|
299
|
+
type: "sync_recovered";
|
|
300
|
+
mailbox: string;
|
|
301
|
+
since: string;
|
|
302
|
+
};
|
|
303
|
+
export interface Notifier {
|
|
304
|
+
notify(event: HelpdeskNotification): Promise<void>;
|
|
305
|
+
}
|
|
306
|
+
export interface HelpdeskConfig {
|
|
307
|
+
ownAddresses: string[];
|
|
308
|
+
connectedMailboxes: string[];
|
|
309
|
+
subjectWindowMs?: number;
|
|
310
|
+
restartOverlapMs?: number;
|
|
311
|
+
resumeDraftAfterMs?: number;
|
|
312
|
+
draftTimeoutMs?: number;
|
|
313
|
+
minConfidence?: number;
|
|
314
|
+
pageSize?: number;
|
|
315
|
+
}
|
|
316
|
+
export type MailboxWriteMode = "read_only" | "approve_and_send";
|
|
317
|
+
export interface HelpdeskControls {
|
|
318
|
+
paused: boolean;
|
|
319
|
+
writeMode?: MailboxWriteMode;
|
|
320
|
+
writeModes?: Record<string, MailboxWriteMode>;
|
|
321
|
+
}
|
|
322
|
+
export interface Paused {
|
|
323
|
+
status: "paused";
|
|
324
|
+
}
|
|
325
|
+
export interface WriteBlocked {
|
|
326
|
+
status: "write_blocked";
|
|
327
|
+
mode: MailboxWriteMode;
|
|
328
|
+
}
|
|
329
|
+
export interface HelpdeskDeps {
|
|
330
|
+
store: HelpdeskStore;
|
|
331
|
+
gatewayFor(mailbox: string): MailboxGateway;
|
|
332
|
+
drafter?: Drafter;
|
|
333
|
+
clock: Clock;
|
|
334
|
+
config: HelpdeskConfig;
|
|
335
|
+
controls?: HelpdeskControls | (() => HelpdeskControls | Promise<HelpdeskControls>);
|
|
336
|
+
notifier?: Notifier;
|
|
337
|
+
attachments?: ReplyAttachmentsDeps;
|
|
338
|
+
newId?: () => string;
|
|
339
|
+
}
|
package/dist/types.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,73 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxv-systems/helpdesk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Mailbox helpdesk domain logic for DXV apps — idempotent delta ingestion, conservative threading, ticket state machine, noise detection, and replies that a person approves in the helpdesk before they are sent from the mailbox (quoted history kept, retry-safe, never sent twice). Pure logic behind interfaces, plus the staff inbox as a route handler (./server) and a React component (./react). Zero runtime dependencies.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"dxv",
|
|
7
|
+
"helpdesk",
|
|
8
|
+
"mail",
|
|
9
|
+
"microsoft-graph",
|
|
10
|
+
"threading",
|
|
11
|
+
"tickets"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./testing": {
|
|
22
|
+
"types": "./dist/testing/index.d.ts",
|
|
23
|
+
"default": "./dist/testing/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./server": {
|
|
26
|
+
"types": "./dist/server.d.ts",
|
|
27
|
+
"default": "./dist/server.js"
|
|
28
|
+
},
|
|
29
|
+
"./react": {
|
|
30
|
+
"types": "./dist/react.d.ts",
|
|
31
|
+
"default": "./dist/react.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"main": "./dist/index.js",
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"dist"
|
|
38
|
+
],
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"test": "vitest run",
|
|
43
|
+
"test:watch": "vitest",
|
|
44
|
+
"prepublishOnly": "npm run build"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"react": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"react": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@testing-library/react": "^16.0.1",
|
|
56
|
+
"@types/node": "^20.19.0",
|
|
57
|
+
"@types/react": "^18.3.12",
|
|
58
|
+
"jsdom": "^25.0.1",
|
|
59
|
+
"react": "^18.3.1",
|
|
60
|
+
"react-dom": "^18.3.1",
|
|
61
|
+
"typescript": "^5.6.0",
|
|
62
|
+
"vitest": "^4.1.11"
|
|
63
|
+
},
|
|
64
|
+
"repository": {
|
|
65
|
+
"type": "git",
|
|
66
|
+
"url": "git+https://github.com/dxv-systems/dxv-packages.git",
|
|
67
|
+
"directory": "packages/helpdesk"
|
|
68
|
+
},
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
},
|
|
72
|
+
"license": "MIT"
|
|
73
|
+
}
|