@consilioweb/payload-support 1.1.1 → 2.0.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 +4 -0
- package/dist/components/TicketConversation/components/AISummaryPanel.cjs +22 -14
- package/dist/components/TicketConversation/components/AISummaryPanel.js +23 -16
- package/dist/components/TicketConversation/components/ActionPanels.cjs +5 -5
- package/dist/components/TicketConversation/components/ActionPanels.js +5 -5
- package/dist/components/TicketConversation/components/ClientBar.cjs +1 -1
- package/dist/components/TicketConversation/components/ClientBar.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.cjs +2 -2
- package/dist/components/TicketConversation/components/QuickActions.js +2 -2
- package/dist/components/TicketConversation/components/TicketHeader.cjs +1 -1
- package/dist/components/TicketConversation/components/TicketHeader.js +1 -1
- package/dist/components/TicketConversation/index.cjs +10 -10
- package/dist/components/TicketConversation/index.js +10 -10
- package/dist/index.cjs +709 -173
- package/dist/index.d.cts +165 -3
- package/dist/index.d.ts +165 -3
- package/dist/index.js +702 -175
- package/dist/styles/ChatView.module.scss +1 -1
- package/dist/styles/TicketDetail.module.scss +3 -3
- package/dist/styles/_tokens.scss +1 -1
- package/dist/styles/theme.css +6 -6
- package/dist/views/TicketingSettingsView/client.cjs +3 -3
- package/dist/views/TicketingSettingsView/client.js +3 -3
- package/package.json +9 -9
- package/src/__tests__/aiSummaryRendering.test.ts +21 -0
- package/src/__tests__/authResponses.test.ts +69 -0
- package/src/__tests__/capabilities.test.ts +71 -0
- package/src/__tests__/generateTrackingToken.test.ts +18 -4
- package/src/__tests__/integration/core-behaviors.test.ts +23 -0
- package/src/__tests__/rateLimiter.test.ts +60 -66
- package/src/__tests__/trackOpenEndpoint.test.ts +54 -0
- package/src/__tests__/webhookSecurity.test.ts +58 -0
- package/src/collections/PendingEmails.ts +2 -1
- package/src/collections/SupportClients.ts +43 -1
- package/src/collections/SupportCounters.ts +14 -0
- package/src/collections/SupportRateLimits.ts +20 -0
- package/src/collections/TicketMessages.ts +76 -0
- package/src/collections/Tickets.ts +179 -39
- package/src/collections/index.ts +2 -0
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +26 -13
- package/src/components/TicketConversation/components/ActionPanels.tsx +5 -5
- package/src/components/TicketConversation/components/ClientBar.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +2 -2
- package/src/components/TicketConversation/components/TicketHeader.tsx +1 -1
- package/src/components/TicketConversation/index.tsx +10 -10
- package/src/endpoints/admin-chat.ts +4 -4
- package/src/endpoints/ai-agent.ts +6 -1
- package/src/endpoints/ai.ts +10 -5
- package/src/endpoints/auth-2fa.ts +6 -7
- package/src/endpoints/auto-close.ts +2 -1
- package/src/endpoints/capabilities.ts +144 -0
- package/src/endpoints/channels.ts +2 -1
- package/src/endpoints/chat.ts +6 -6
- package/src/endpoints/chatbot.ts +4 -4
- package/src/endpoints/client-intelligence.ts +13 -4
- package/src/endpoints/import-conversation.ts +6 -5
- package/src/endpoints/index.ts +35 -14
- package/src/endpoints/invite-collaborator.ts +4 -4
- package/src/endpoints/login.ts +4 -5
- package/src/endpoints/oauth-google.ts +1 -1
- package/src/endpoints/process-digests.ts +2 -1
- package/src/endpoints/process-scheduled.ts +2 -1
- package/src/endpoints/process-snooze.ts +2 -1
- package/src/endpoints/resend-notification.ts +4 -4
- package/src/endpoints/send-reminder.ts +4 -4
- package/src/endpoints/ticket-synthesis.ts +17 -2
- package/src/endpoints/track-open.ts +44 -21
- package/src/endpoints/transfer-ticket.ts +4 -4
- package/src/index.ts +7 -0
- package/src/plugin.ts +16 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/ChatWidget.tsx +4 -4
- package/src/portal/auth/ChatbotWidget.tsx +4 -4
- package/src/portal/auth/dashboard/DashboardClient.tsx +2 -2
- package/src/portal/auth/faq/FAQSearch.tsx +1 -1
- package/src/portal/auth/faq/page.tsx +2 -2
- package/src/portal/auth/profile/page.tsx +17 -17
- package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +1 -1
- package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +1 -1
- package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +1 -1
- package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +3 -3
- package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +3 -3
- package/src/portal/auth/tickets/detail/page.tsx +5 -5
- package/src/portal/auth/tickets/new/page.tsx +8 -8
- package/src/portal/forgot-password/page.tsx +3 -3
- package/src/portal/layout.tsx +5 -2
- package/src/portal/login/page.tsx +7 -7
- package/src/portal/page.tsx +9 -9
- package/src/portal/register/page.tsx +4 -4
- package/src/portal/reset-password/page.tsx +3 -3
- package/src/styles/ChatView.module.scss +1 -1
- package/src/styles/TicketDetail.module.scss +3 -3
- package/src/styles/_tokens.scss +1 -1
- package/src/styles/theme.css +6 -6
- package/src/types.ts +71 -0
- package/src/utils/rateLimiter.ts +131 -38
- package/src/utils/slugs.ts +4 -0
- package/src/utils/webhookSecurity.ts +81 -0
- package/src/views/TicketingSettingsView/client.tsx +3 -3
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { initTransaction, commitTransaction, killTransaction, getFieldsToSign, jwtSign, APIError } from 'payload';
|
|
2
|
+
import crypto3, { createHash, timingSafeEqual, createHmac, randomBytes } from 'crypto';
|
|
2
3
|
import PDFDocument from 'pdfkit';
|
|
3
|
-
import { APIError, getFieldsToSign, jwtSign } from 'payload';
|
|
4
4
|
import webpush from 'web-push';
|
|
5
5
|
import sanitizeHtml from 'sanitize-html';
|
|
6
6
|
|
|
@@ -66,12 +66,263 @@ var DEFAULT_SLUGS = {
|
|
|
66
66
|
automationRules: "automation-rules",
|
|
67
67
|
supportTeams: "support-teams",
|
|
68
68
|
pushSubscriptions: "push-subscriptions",
|
|
69
|
+
rateLimits: "support-rate-limits",
|
|
70
|
+
counters: "support-counters",
|
|
69
71
|
users: "users",
|
|
70
72
|
media: "media"
|
|
71
73
|
};
|
|
72
74
|
function resolveSlugs(overrides) {
|
|
73
75
|
return { ...DEFAULT_SLUGS, ...overrides };
|
|
74
76
|
}
|
|
77
|
+
var MemoryRateLimitStore = class {
|
|
78
|
+
entries = /* @__PURE__ */ new Map();
|
|
79
|
+
async increment(key, windowMs) {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const current = this.entries.get(key);
|
|
82
|
+
const next = !current || now > current.resetAt ? { count: 1, resetAt: now + windowMs } : { ...current, count: current.count + 1 };
|
|
83
|
+
this.entries.set(key, next);
|
|
84
|
+
return next;
|
|
85
|
+
}
|
|
86
|
+
async reset(key) {
|
|
87
|
+
this.entries.delete(key);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
var PayloadRateLimitStore = class {
|
|
91
|
+
constructor(collectionSlug = "support-rate-limits") {
|
|
92
|
+
this.collectionSlug = collectionSlug;
|
|
93
|
+
}
|
|
94
|
+
collectionSlug;
|
|
95
|
+
async increment(key, windowMs, context) {
|
|
96
|
+
const { payload, req } = this.resolveContext(context);
|
|
97
|
+
const ownsTransaction = req ? await initTransaction(req) : false;
|
|
98
|
+
try {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
const result = await payload.find({
|
|
101
|
+
collection: this.collectionSlug,
|
|
102
|
+
where: { key: { equals: key } },
|
|
103
|
+
limit: 1,
|
|
104
|
+
depth: 0,
|
|
105
|
+
overrideAccess: true,
|
|
106
|
+
...req ? { req } : {}
|
|
107
|
+
});
|
|
108
|
+
const current = result.docs[0];
|
|
109
|
+
const currentResetAt = current?.resetAt ? new Date(String(current.resetAt)).getTime() : 0;
|
|
110
|
+
const next = !current || now > currentResetAt ? { count: 1, resetAt: now + windowMs } : { count: Number(current.count || 0) + 1, resetAt: currentResetAt };
|
|
111
|
+
if (current?.id != null) {
|
|
112
|
+
await payload.update({
|
|
113
|
+
collection: this.collectionSlug,
|
|
114
|
+
id: current.id,
|
|
115
|
+
data: { count: next.count, resetAt: new Date(next.resetAt).toISOString() },
|
|
116
|
+
overrideAccess: true,
|
|
117
|
+
...req ? { req } : {}
|
|
118
|
+
});
|
|
119
|
+
} else {
|
|
120
|
+
await payload.create({
|
|
121
|
+
collection: this.collectionSlug,
|
|
122
|
+
data: { key, count: next.count, resetAt: new Date(next.resetAt).toISOString() },
|
|
123
|
+
overrideAccess: true,
|
|
124
|
+
...req ? { req } : {}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (ownsTransaction && req) await commitTransaction(req);
|
|
128
|
+
return next;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
if (ownsTransaction && req) await killTransaction(req);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async reset(key, context) {
|
|
135
|
+
const { payload, req } = this.resolveContext(context);
|
|
136
|
+
const ownsTransaction = req ? await initTransaction(req) : false;
|
|
137
|
+
try {
|
|
138
|
+
await payload.delete({
|
|
139
|
+
collection: this.collectionSlug,
|
|
140
|
+
where: { key: { equals: key } },
|
|
141
|
+
overrideAccess: true,
|
|
142
|
+
...req ? { req } : {}
|
|
143
|
+
});
|
|
144
|
+
if (ownsTransaction && req) await commitTransaction(req);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
if (ownsTransaction && req) await killTransaction(req);
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
resolveContext(context) {
|
|
151
|
+
if (!context || typeof context !== "object") {
|
|
152
|
+
throw new Error("PayloadRateLimitStore requires the current Payload request or instance");
|
|
153
|
+
}
|
|
154
|
+
if ("payload" in context) {
|
|
155
|
+
const req = context;
|
|
156
|
+
return { payload: req.payload, req };
|
|
157
|
+
}
|
|
158
|
+
return { payload: context };
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
var RateLimiter = class {
|
|
162
|
+
constructor(windowMs, maxRequests, store) {
|
|
163
|
+
this.windowMs = windowMs;
|
|
164
|
+
this.maxRequests = maxRequests;
|
|
165
|
+
this.store = store ?? new MemoryRateLimitStore();
|
|
166
|
+
}
|
|
167
|
+
windowMs;
|
|
168
|
+
maxRequests;
|
|
169
|
+
store;
|
|
170
|
+
async check(key, context) {
|
|
171
|
+
const entry = context === void 0 ? await this.store.increment(key, this.windowMs) : await this.store.increment(key, this.windowMs, context);
|
|
172
|
+
return entry.count > this.maxRequests;
|
|
173
|
+
}
|
|
174
|
+
async reset(key, context) {
|
|
175
|
+
if (context === void 0) await this.store.reset(key);
|
|
176
|
+
else await this.store.reset(key, context);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var DEFAULT_INBOUND_EMAIL_LIMITS = {
|
|
180
|
+
maxRequestBytes: 25 * 1024 * 1024,
|
|
181
|
+
maxAttachmentBytes: 10 * 1024 * 1024,
|
|
182
|
+
maxAttachments: 10,
|
|
183
|
+
maxSubjectLength: 1e3,
|
|
184
|
+
maxNameLength: 500,
|
|
185
|
+
maxEmailLength: 320,
|
|
186
|
+
maxBodyLength: 38e3
|
|
187
|
+
};
|
|
188
|
+
function verifySecret(provided, expected) {
|
|
189
|
+
if (!provided || !expected) return false;
|
|
190
|
+
const providedDigest = createHash("sha256").update(provided).digest();
|
|
191
|
+
const expectedDigest = createHash("sha256").update(expected).digest();
|
|
192
|
+
return timingSafeEqual(providedDigest, expectedDigest);
|
|
193
|
+
}
|
|
194
|
+
function validateInboundEmailPayload(input, contentLength, limits = DEFAULT_INBOUND_EMAIL_LIMITS) {
|
|
195
|
+
if (contentLength != null && contentLength > limits.maxRequestBytes) {
|
|
196
|
+
return { code: "request_too_large", status: 413 };
|
|
197
|
+
}
|
|
198
|
+
const attachments = Array.isArray(input.attachments) ? input.attachments : [];
|
|
199
|
+
if (attachments.length > limits.maxAttachments) {
|
|
200
|
+
return { code: "too_many_attachments", status: 413 };
|
|
201
|
+
}
|
|
202
|
+
for (const attachment of attachments) {
|
|
203
|
+
const decodedSize = attachment.content ? Math.ceil(attachment.content.length * 0.75) : 0;
|
|
204
|
+
if (Math.max(Number(attachment.size || 0), decodedSize) > limits.maxAttachmentBytes) {
|
|
205
|
+
return { code: "attachment_too_large", status: 413 };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const tooLong = (input.subject?.length || 0) > limits.maxSubjectLength || (input.senderName?.length || 0) > limits.maxNameLength || (input.senderEmail?.length || 0) > limits.maxEmailLength || (input.recipientEmail?.length || 0) > limits.maxEmailLength || (input.cc?.length || 0) > limits.maxEmailLength || (input.body?.length || 0) > limits.maxBodyLength || (input.htmlBody?.length || 0) > limits.maxBodyLength;
|
|
209
|
+
return tooLong ? { code: "text_field_too_large", status: 413 } : null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// src/endpoints/capabilities.ts
|
|
213
|
+
function createInboundEmailEndpoint(capability, store) {
|
|
214
|
+
const limiter = new RateLimiter(6e4, 60, store);
|
|
215
|
+
return {
|
|
216
|
+
path: "/support-webhook/inbound-email",
|
|
217
|
+
method: "post",
|
|
218
|
+
handler: async (req) => {
|
|
219
|
+
const secretHeader = capability.secretHeader || "x-webhook-secret";
|
|
220
|
+
if (!verifySecret(req.headers.get(secretHeader), capability.secret)) {
|
|
221
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
222
|
+
}
|
|
223
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
|
|
224
|
+
if (await limiter.check(ip, req)) {
|
|
225
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
226
|
+
}
|
|
227
|
+
const declaredLength = Number(req.headers.get("content-length") || 0) || void 0;
|
|
228
|
+
let input;
|
|
229
|
+
try {
|
|
230
|
+
input = await req.json();
|
|
231
|
+
} catch {
|
|
232
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
233
|
+
}
|
|
234
|
+
const measuredLength = new TextEncoder().encode(JSON.stringify(input)).byteLength;
|
|
235
|
+
const validation = validateInboundEmailPayload(
|
|
236
|
+
input,
|
|
237
|
+
Math.max(declaredLength || 0, measuredLength)
|
|
238
|
+
);
|
|
239
|
+
if (validation) return Response.json({ error: validation.code }, { status: validation.status });
|
|
240
|
+
return capability.handle(req, input);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function createProjectSuggestionsEndpoint(slugs, capability, store) {
|
|
245
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
246
|
+
return {
|
|
247
|
+
path: "/support/suggest-projects",
|
|
248
|
+
method: "post",
|
|
249
|
+
handler: async (req) => {
|
|
250
|
+
if (!req.user || req.user.collection !== slugs.users) {
|
|
251
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
252
|
+
}
|
|
253
|
+
const key = req.user?.id ? String(req.user.id) : "anonymous";
|
|
254
|
+
if (await limiter.check(key, req)) {
|
|
255
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
256
|
+
}
|
|
257
|
+
return capability.suggest(req);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
function createTicketTitleEndpoint(slugs, capability, store) {
|
|
262
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
263
|
+
return {
|
|
264
|
+
path: "/support/ticket-title",
|
|
265
|
+
method: "post",
|
|
266
|
+
handler: async (req) => {
|
|
267
|
+
if (!req.user || req.user.collection !== slugs.users) {
|
|
268
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
269
|
+
}
|
|
270
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
271
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
272
|
+
}
|
|
273
|
+
const ticketId = Number(new URL(req.url || "", "http://localhost").searchParams.get("ticketId"));
|
|
274
|
+
if (!Number.isFinite(ticketId) || ticketId <= 0) {
|
|
275
|
+
return Response.json({ error: "ticketId required" }, { status: 400 });
|
|
276
|
+
}
|
|
277
|
+
const title = await capability.generate(req.payload, ticketId);
|
|
278
|
+
if (!title) return Response.json({ error: "Generation unavailable" }, { status: 502 });
|
|
279
|
+
return Response.json({ title, status: "suggested" });
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
function createGenerateMissingTitlesEndpoint(slugs, capability, store) {
|
|
284
|
+
const limiter = new RateLimiter(6e4, 5, store);
|
|
285
|
+
return {
|
|
286
|
+
path: "/support/generate-missing-titles",
|
|
287
|
+
method: "post",
|
|
288
|
+
handler: async (req) => {
|
|
289
|
+
if (!req.user || req.user.collection !== slugs.users) {
|
|
290
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
291
|
+
}
|
|
292
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
293
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
294
|
+
}
|
|
295
|
+
const limit = Math.min(
|
|
296
|
+
Math.max(Number(new URL(req.url || "", "http://localhost").searchParams.get("limit")) || 8, 1),
|
|
297
|
+
20
|
|
298
|
+
);
|
|
299
|
+
const where = {
|
|
300
|
+
and: [
|
|
301
|
+
{ displayTitle: { exists: false } },
|
|
302
|
+
{ displayTitleStatus: { not_equals: "error" } }
|
|
303
|
+
]
|
|
304
|
+
};
|
|
305
|
+
const batch = await req.payload.find({
|
|
306
|
+
collection: slugs.tickets,
|
|
307
|
+
where,
|
|
308
|
+
limit,
|
|
309
|
+
depth: 0,
|
|
310
|
+
overrideAccess: true,
|
|
311
|
+
sort: "-createdAt"
|
|
312
|
+
});
|
|
313
|
+
let generated = 0;
|
|
314
|
+
for (const ticket of batch.docs) {
|
|
315
|
+
if (await capability.generate(req.payload, ticket.id)) generated++;
|
|
316
|
+
}
|
|
317
|
+
const remaining = await req.payload.count({
|
|
318
|
+
collection: slugs.tickets,
|
|
319
|
+
where,
|
|
320
|
+
overrideAccess: true
|
|
321
|
+
});
|
|
322
|
+
return Response.json({ generated, remaining: remaining.totalDocs });
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
75
326
|
|
|
76
327
|
// src/utils/auth.ts
|
|
77
328
|
var AuthError = class extends Error {
|
|
@@ -183,8 +434,9 @@ async function readUserPrefs(payload, userId) {
|
|
|
183
434
|
}
|
|
184
435
|
|
|
185
436
|
// src/endpoints/ai.ts
|
|
186
|
-
function getClient(aiSettings) {
|
|
187
|
-
const
|
|
437
|
+
async function getClient(aiSettings) {
|
|
438
|
+
const moduleName = "@anthropic-ai/sdk";
|
|
439
|
+
const { default: Anthropic } = await import(moduleName);
|
|
188
440
|
if (aiSettings.provider === "ollama") {
|
|
189
441
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
190
442
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
@@ -194,7 +446,8 @@ function getClient(aiSettings) {
|
|
|
194
446
|
function getModel(aiSettings) {
|
|
195
447
|
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
196
448
|
}
|
|
197
|
-
function createAiEndpoint(slugs) {
|
|
449
|
+
function createAiEndpoint(slugs, store) {
|
|
450
|
+
const limiter = new RateLimiter(6e4, 30, store);
|
|
198
451
|
return {
|
|
199
452
|
path: "/support/ai",
|
|
200
453
|
method: "post",
|
|
@@ -202,6 +455,9 @@ function createAiEndpoint(slugs) {
|
|
|
202
455
|
try {
|
|
203
456
|
const payload = req.payload;
|
|
204
457
|
requireAdmin(req, slugs);
|
|
458
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
459
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
460
|
+
}
|
|
205
461
|
const settings = await readSupportSettings(payload);
|
|
206
462
|
const aiSettings = settings.ai;
|
|
207
463
|
let body;
|
|
@@ -211,7 +467,7 @@ function createAiEndpoint(slugs) {
|
|
|
211
467
|
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
212
468
|
}
|
|
213
469
|
const { action } = body;
|
|
214
|
-
const anthropic = getClient(aiSettings);
|
|
470
|
+
const anthropic = await getClient(aiSettings);
|
|
215
471
|
const model = getModel(aiSettings);
|
|
216
472
|
if (action === "sentiment") {
|
|
217
473
|
if (!aiSettings.enableSentiment) {
|
|
@@ -443,13 +699,17 @@ ${kbText || "(vide)"}`;
|
|
|
443
699
|
}
|
|
444
700
|
|
|
445
701
|
// src/endpoints/ai-agent.ts
|
|
446
|
-
function createAiAgentEndpoint(slugs) {
|
|
702
|
+
function createAiAgentEndpoint(slugs, store) {
|
|
703
|
+
const limiter = new RateLimiter(6e4, 10, store);
|
|
447
704
|
return {
|
|
448
705
|
path: "/support/ai-agent",
|
|
449
706
|
method: "post",
|
|
450
707
|
handler: async (req) => {
|
|
451
708
|
try {
|
|
452
709
|
requireAdmin(req, slugs);
|
|
710
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
711
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
712
|
+
}
|
|
453
713
|
let body = {};
|
|
454
714
|
try {
|
|
455
715
|
body = await req.json();
|
|
@@ -473,8 +733,9 @@ function createAiAgentEndpoint(slugs) {
|
|
|
473
733
|
}
|
|
474
734
|
|
|
475
735
|
// src/endpoints/client-intelligence.ts
|
|
476
|
-
function getClient3(aiSettings) {
|
|
477
|
-
const
|
|
736
|
+
async function getClient3(aiSettings) {
|
|
737
|
+
const moduleName = "@anthropic-ai/sdk";
|
|
738
|
+
const { default: Anthropic } = await import(moduleName);
|
|
478
739
|
if (aiSettings.provider === "ollama") {
|
|
479
740
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
480
741
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
@@ -485,10 +746,14 @@ function getModel2(aiSettings) {
|
|
|
485
746
|
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
486
747
|
}
|
|
487
748
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
488
|
-
function createClientIntelligenceEndpoint(slugs) {
|
|
749
|
+
function createClientIntelligenceEndpoint(slugs, store) {
|
|
750
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
489
751
|
const getHandler = async (req) => {
|
|
490
752
|
try {
|
|
491
753
|
requireAdmin(req, slugs);
|
|
754
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
755
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
756
|
+
}
|
|
492
757
|
const payload = req.payload;
|
|
493
758
|
const url = new URL(req.url || "", "http://localhost");
|
|
494
759
|
const clientId = url.searchParams.get("clientId");
|
|
@@ -518,6 +783,9 @@ function createClientIntelligenceEndpoint(slugs) {
|
|
|
518
783
|
const postHandler = async (req) => {
|
|
519
784
|
try {
|
|
520
785
|
requireAdmin(req, slugs);
|
|
786
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
787
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
788
|
+
}
|
|
521
789
|
const payload = req.payload;
|
|
522
790
|
const body = await req.json?.() || {};
|
|
523
791
|
const clientId = body.clientId;
|
|
@@ -627,7 +895,7 @@ R\xE9ponds en JSON strict (pas de markdown, pas de commentaires) avec cette stru
|
|
|
627
895
|
}
|
|
628
896
|
|
|
629
897
|
Sois factuel. Ne d\xE9passe pas 5 items par tableau. R\xE9ponds UNIQUEMENT avec le JSON.`;
|
|
630
|
-
const anthropic = getClient3(aiSettings);
|
|
898
|
+
const anthropic = await getClient3(aiSettings);
|
|
631
899
|
const model = getModel2(aiSettings);
|
|
632
900
|
const res = await anthropic.messages.create({
|
|
633
901
|
model,
|
|
@@ -1650,7 +1918,13 @@ var TRANSPARENT_GIF = Buffer.from(
|
|
|
1650
1918
|
"base64"
|
|
1651
1919
|
);
|
|
1652
1920
|
function generateTrackingToken(ticketId, messageId, secret) {
|
|
1653
|
-
return createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex")
|
|
1921
|
+
return createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex");
|
|
1922
|
+
}
|
|
1923
|
+
function verifyTrackingToken(ticketId, messageId, signature, secret) {
|
|
1924
|
+
if (!/^[0-9a-f]{64}$/i.test(signature)) return false;
|
|
1925
|
+
const expected = Buffer.from(generateTrackingToken(ticketId, messageId, secret), "hex");
|
|
1926
|
+
const received = Buffer.from(signature, "hex");
|
|
1927
|
+
return expected.length === received.length && timingSafeEqual(expected, received);
|
|
1654
1928
|
}
|
|
1655
1929
|
function createTrackOpenEndpoint(slugs) {
|
|
1656
1930
|
return {
|
|
@@ -1664,7 +1938,7 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1664
1938
|
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
1665
1939
|
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
1666
1940
|
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1667
|
-
const validSig = !!secret && !!ticketId && !!messageId && !!sig &&
|
|
1941
|
+
const validSig = !!secret && Number.isInteger(parsedId) && parsedId > 0 && Number.isInteger(parsedMsgId) && parsedMsgId > 0 && !!ticketId && !!messageId && !!sig && verifyTrackingToken(ticketId, messageId, sig, secret);
|
|
1668
1942
|
if (!validSig) {
|
|
1669
1943
|
return new Response(TRANSPARENT_GIF, {
|
|
1670
1944
|
status: 200,
|
|
@@ -1678,6 +1952,16 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1678
1952
|
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
1679
1953
|
try {
|
|
1680
1954
|
const payload = req.payload;
|
|
1955
|
+
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1956
|
+
id: parsedMsgId,
|
|
1957
|
+
depth: 0,
|
|
1958
|
+
overrideAccess: true,
|
|
1959
|
+
select: { ticket: true, emailOpenedAt: true }
|
|
1960
|
+
});
|
|
1961
|
+
const messageTicketId = typeof msg?.ticket === "object" ? msg.ticket?.id : msg?.ticket;
|
|
1962
|
+
if (!msg || String(messageTicketId) !== String(parsedId)) {
|
|
1963
|
+
return transparentGifResponse();
|
|
1964
|
+
}
|
|
1681
1965
|
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
1682
1966
|
id: parsedId,
|
|
1683
1967
|
depth: 0,
|
|
@@ -1696,12 +1980,6 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1696
1980
|
}
|
|
1697
1981
|
}
|
|
1698
1982
|
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
1699
|
-
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1700
|
-
id: parsedMsgId,
|
|
1701
|
-
depth: 0,
|
|
1702
|
-
overrideAccess: true,
|
|
1703
|
-
select: { emailOpenedAt: true }
|
|
1704
|
-
});
|
|
1705
1983
|
if (msg && !msg.emailOpenedAt) {
|
|
1706
1984
|
await dbUpdate(payload, slugs.ticketMessages, {
|
|
1707
1985
|
id: parsedMsgId,
|
|
@@ -1733,19 +2011,22 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1733
2011
|
console.error("[track-open] Error:", err);
|
|
1734
2012
|
}
|
|
1735
2013
|
}
|
|
1736
|
-
return
|
|
1737
|
-
status: 200,
|
|
1738
|
-
headers: {
|
|
1739
|
-
"Content-Type": "image/gif",
|
|
1740
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1741
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
1742
|
-
"Pragma": "no-cache",
|
|
1743
|
-
"Expires": "0"
|
|
1744
|
-
}
|
|
1745
|
-
});
|
|
2014
|
+
return transparentGifResponse();
|
|
1746
2015
|
}
|
|
1747
2016
|
};
|
|
1748
2017
|
}
|
|
2018
|
+
function transparentGifResponse() {
|
|
2019
|
+
return new Response(TRANSPARENT_GIF, {
|
|
2020
|
+
status: 200,
|
|
2021
|
+
headers: {
|
|
2022
|
+
"Content-Type": "image/gif",
|
|
2023
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
2024
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
2025
|
+
"Pragma": "no-cache",
|
|
2026
|
+
"Expires": "0"
|
|
2027
|
+
}
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
1749
2030
|
|
|
1750
2031
|
// src/utils/emailTemplate.ts
|
|
1751
2032
|
var COLORS = {
|
|
@@ -2032,7 +2313,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
2032
2313
|
handler: async (req) => {
|
|
2033
2314
|
const secret = req.headers.get("x-cron-secret");
|
|
2034
2315
|
const expectedSecret = process.env.CRON_SECRET;
|
|
2035
|
-
if (!
|
|
2316
|
+
if (!verifySecret(secret, expectedSecret)) {
|
|
2036
2317
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
2037
2318
|
}
|
|
2038
2319
|
try {
|
|
@@ -2178,52 +2459,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
2178
2459
|
};
|
|
2179
2460
|
}
|
|
2180
2461
|
|
|
2181
|
-
// src/utils/rateLimiter.ts
|
|
2182
|
-
var RateLimiter = class {
|
|
2183
|
-
constructor(windowMs, maxRequests) {
|
|
2184
|
-
this.windowMs = windowMs;
|
|
2185
|
-
this.maxRequests = maxRequests;
|
|
2186
|
-
const timer = setInterval(() => this.cleanup(), windowMs);
|
|
2187
|
-
timer.unref();
|
|
2188
|
-
}
|
|
2189
|
-
windowMs;
|
|
2190
|
-
maxRequests;
|
|
2191
|
-
store = /* @__PURE__ */ new Map();
|
|
2192
|
-
/**
|
|
2193
|
-
* Check if a key has exceeded the rate limit.
|
|
2194
|
-
* Returns true if the request should be blocked.
|
|
2195
|
-
*/
|
|
2196
|
-
check(key) {
|
|
2197
|
-
const now = Date.now();
|
|
2198
|
-
const entry = this.store.get(key);
|
|
2199
|
-
if (!entry || now > entry.resetAt) {
|
|
2200
|
-
this.store.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
2201
|
-
return false;
|
|
2202
|
-
}
|
|
2203
|
-
entry.count++;
|
|
2204
|
-
return entry.count > this.maxRequests;
|
|
2205
|
-
}
|
|
2206
|
-
/**
|
|
2207
|
-
* Reset the counter for a specific key.
|
|
2208
|
-
*/
|
|
2209
|
-
reset(key) {
|
|
2210
|
-
this.store.delete(key);
|
|
2211
|
-
}
|
|
2212
|
-
/**
|
|
2213
|
-
* Remove expired entries from the store.
|
|
2214
|
-
*/
|
|
2215
|
-
cleanup() {
|
|
2216
|
-
const now = Date.now();
|
|
2217
|
-
for (const [key, entry] of this.store) {
|
|
2218
|
-
if (now > entry.resetAt) {
|
|
2219
|
-
this.store.delete(key);
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
}
|
|
2223
|
-
};
|
|
2224
|
-
|
|
2225
2462
|
// src/endpoints/send-reminder.ts
|
|
2226
|
-
var reminderLimiter = new RateLimiter(60 * 60 * 1e3, 30);
|
|
2227
2463
|
var MIN_HOURS = 1;
|
|
2228
2464
|
var MAX_HOURS = 24 * 30;
|
|
2229
2465
|
function formatFr(date, withTime) {
|
|
@@ -2235,7 +2471,8 @@ function formatFr(date, withTime) {
|
|
|
2235
2471
|
timeZone: "Europe/Paris"
|
|
2236
2472
|
});
|
|
2237
2473
|
}
|
|
2238
|
-
function createSendReminderEndpoint(slugs) {
|
|
2474
|
+
function createSendReminderEndpoint(slugs, store) {
|
|
2475
|
+
const reminderLimiter = new RateLimiter(60 * 60 * 1e3, 30, store);
|
|
2239
2476
|
return {
|
|
2240
2477
|
path: "/support/send-reminder",
|
|
2241
2478
|
method: "post",
|
|
@@ -2243,7 +2480,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2243
2480
|
try {
|
|
2244
2481
|
const payload = req.payload;
|
|
2245
2482
|
requireAdmin(req, slugs);
|
|
2246
|
-
if (reminderLimiter.check(String(req.user.id))) {
|
|
2483
|
+
if (await reminderLimiter.check(String(req.user.id), req)) {
|
|
2247
2484
|
return Response.json(
|
|
2248
2485
|
{ error: "Trop de relances. R\xE9essayez dans une heure." },
|
|
2249
2486
|
{ status: 429 }
|
|
@@ -2546,15 +2783,15 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2546
2783
|
}
|
|
2547
2784
|
|
|
2548
2785
|
// src/endpoints/chatbot.ts
|
|
2549
|
-
|
|
2550
|
-
|
|
2786
|
+
function createChatbotEndpoint(slugs, store) {
|
|
2787
|
+
const chatbotLimiter = new RateLimiter(6e4, 10, store);
|
|
2551
2788
|
return {
|
|
2552
2789
|
path: "/support/chatbot",
|
|
2553
2790
|
method: "post",
|
|
2554
2791
|
handler: async (req) => {
|
|
2555
2792
|
try {
|
|
2556
2793
|
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
|
|
2557
|
-
if (chatbotLimiter.check(ip)) {
|
|
2794
|
+
if (await chatbotLimiter.check(ip, req)) {
|
|
2558
2795
|
return Response.json({ error: "Too many requests. Please wait a moment." }, { status: 429 });
|
|
2559
2796
|
}
|
|
2560
2797
|
let body;
|
|
@@ -2635,8 +2872,6 @@ R\xE9ponds en fran\xE7ais, de mani\xE8re concise et utile. Si tu ne trouves pas
|
|
|
2635
2872
|
}
|
|
2636
2873
|
};
|
|
2637
2874
|
}
|
|
2638
|
-
var chatSessionLimiter = new RateLimiter(36e5, 5);
|
|
2639
|
-
var chatMessageLimiter = new RateLimiter(6e4, 15);
|
|
2640
2875
|
function createChatGetEndpoint(slugs) {
|
|
2641
2876
|
return {
|
|
2642
2877
|
path: "/support/chat",
|
|
@@ -2680,7 +2915,9 @@ function createChatGetEndpoint(slugs) {
|
|
|
2680
2915
|
}
|
|
2681
2916
|
};
|
|
2682
2917
|
}
|
|
2683
|
-
function createChatPostEndpoint(slugs) {
|
|
2918
|
+
function createChatPostEndpoint(slugs, store) {
|
|
2919
|
+
const chatSessionLimiter = new RateLimiter(36e5, 5, store);
|
|
2920
|
+
const chatMessageLimiter = new RateLimiter(6e4, 15, store);
|
|
2684
2921
|
return {
|
|
2685
2922
|
path: "/support/chat",
|
|
2686
2923
|
method: "post",
|
|
@@ -2697,7 +2934,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2697
2934
|
const { action, session, message } = body;
|
|
2698
2935
|
const userId = String(req.user.id);
|
|
2699
2936
|
if (action === "start") {
|
|
2700
|
-
if (chatSessionLimiter.check(userId)) {
|
|
2937
|
+
if (await chatSessionLimiter.check(userId, req)) {
|
|
2701
2938
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2702
2939
|
}
|
|
2703
2940
|
const sessionId = `chat_${crypto3.randomBytes(16).toString("hex")}`;
|
|
@@ -2714,7 +2951,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2714
2951
|
return Response.json({ session: sessionId, messages: [systemMsg] });
|
|
2715
2952
|
}
|
|
2716
2953
|
if (action === "send" && session && message) {
|
|
2717
|
-
if (chatMessageLimiter.check(userId)) {
|
|
2954
|
+
if (await chatMessageLimiter.check(userId, req)) {
|
|
2718
2955
|
return Response.json({ error: "Trop de messages. Attendez un moment." }, { status: 429 });
|
|
2719
2956
|
}
|
|
2720
2957
|
const trimmedMessage = String(message).trim();
|
|
@@ -2880,7 +3117,6 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2880
3117
|
}
|
|
2881
3118
|
|
|
2882
3119
|
// src/endpoints/admin-chat.ts
|
|
2883
|
-
var adminChatLimiter = new RateLimiter(6e4, 30);
|
|
2884
3120
|
function createAdminChatGetEndpoint(slugs) {
|
|
2885
3121
|
return {
|
|
2886
3122
|
path: "/support/admin-chat",
|
|
@@ -2954,7 +3190,8 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2954
3190
|
}
|
|
2955
3191
|
};
|
|
2956
3192
|
}
|
|
2957
|
-
function createAdminChatPostEndpoint(slugs) {
|
|
3193
|
+
function createAdminChatPostEndpoint(slugs, store) {
|
|
3194
|
+
const adminChatLimiter = new RateLimiter(6e4, 30, store);
|
|
2958
3195
|
return {
|
|
2959
3196
|
path: "/support/admin-chat",
|
|
2960
3197
|
method: "post",
|
|
@@ -2983,7 +3220,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2983
3220
|
}
|
|
2984
3221
|
const clientId = typeof sessionMsg.docs[0].client === "object" ? sessionMsg.docs[0].client.id : sessionMsg.docs[0].client;
|
|
2985
3222
|
if (action === "send" && message) {
|
|
2986
|
-
if (adminChatLimiter.check(String(req.user.id))) {
|
|
3223
|
+
if (await adminChatLimiter.check(String(req.user.id), req)) {
|
|
2987
3224
|
return Response.json({ error: "Rate limit atteint." }, { status: 429 });
|
|
2988
3225
|
}
|
|
2989
3226
|
const trimmedMessage = String(message).trim();
|
|
@@ -3937,13 +4174,17 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3937
4174
|
}
|
|
3938
4175
|
|
|
3939
4176
|
// src/endpoints/ticket-synthesis.ts
|
|
3940
|
-
function createTicketSynthesisEndpoint(slugs) {
|
|
4177
|
+
function createTicketSynthesisEndpoint(slugs, generator, store) {
|
|
4178
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
3941
4179
|
return {
|
|
3942
4180
|
path: "/support/ticket-synthesis",
|
|
3943
4181
|
method: "post",
|
|
3944
4182
|
handler: async (req) => {
|
|
3945
4183
|
try {
|
|
3946
4184
|
requireAdmin(req, slugs);
|
|
4185
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
4186
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
4187
|
+
}
|
|
3947
4188
|
const payload = req.payload;
|
|
3948
4189
|
const url = new URL(req.url || "", "http://localhost");
|
|
3949
4190
|
const ticketIdRaw = url.searchParams.get("ticketId");
|
|
@@ -3969,7 +4210,10 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3969
4210
|
});
|
|
3970
4211
|
}
|
|
3971
4212
|
}
|
|
3972
|
-
const result = await generateTicketSynthesis({ payload, slugs, ticketId });
|
|
4213
|
+
const result = generator ? await generator.generate(payload, ticketId) : await generateTicketSynthesis({ payload, slugs, ticketId });
|
|
4214
|
+
if (!result) {
|
|
4215
|
+
return Response.json({ error: "Generation unavailable" }, { status: 502 });
|
|
4216
|
+
}
|
|
3973
4217
|
return Response.json(result);
|
|
3974
4218
|
} catch (err) {
|
|
3975
4219
|
const authResponse = handleAuthError(err);
|
|
@@ -4488,8 +4732,8 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4488
4732
|
}
|
|
4489
4733
|
|
|
4490
4734
|
// src/endpoints/resend-notification.ts
|
|
4491
|
-
|
|
4492
|
-
|
|
4735
|
+
function createResendNotificationEndpoint(slugs, store) {
|
|
4736
|
+
const resendLimiter = new RateLimiter(60 * 60 * 1e3, 10, store);
|
|
4493
4737
|
return {
|
|
4494
4738
|
path: "/support/resend-notification",
|
|
4495
4739
|
method: "post",
|
|
@@ -4497,7 +4741,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4497
4741
|
try {
|
|
4498
4742
|
const payload = req.payload;
|
|
4499
4743
|
requireAdmin(req, slugs);
|
|
4500
|
-
if (resendLimiter.check(String(req.user.id))) {
|
|
4744
|
+
if (await resendLimiter.check(String(req.user.id), req)) {
|
|
4501
4745
|
return Response.json(
|
|
4502
4746
|
{ error: "Trop de renvois. R\xE9essayez dans une heure." },
|
|
4503
4747
|
{ status: 429 }
|
|
@@ -4700,14 +4944,14 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4700
4944
|
}
|
|
4701
4945
|
|
|
4702
4946
|
// src/endpoints/login.ts
|
|
4703
|
-
|
|
4704
|
-
|
|
4947
|
+
function createLoginEndpoint(slugs, store) {
|
|
4948
|
+
const loginLimiter = new RateLimiter(15 * 6e4, 10, store);
|
|
4705
4949
|
return {
|
|
4706
4950
|
path: "/support/login",
|
|
4707
4951
|
method: "post",
|
|
4708
4952
|
handler: async (req) => {
|
|
4709
4953
|
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
|
|
4710
|
-
if (loginLimiter.check(ip)) {
|
|
4954
|
+
if (await loginLimiter.check(ip, req)) {
|
|
4711
4955
|
return Response.json(
|
|
4712
4956
|
{ error: "Trop de tentatives. R\xE9essayez dans quelques minutes." },
|
|
4713
4957
|
{ status: 429 }
|
|
@@ -4747,7 +4991,6 @@ function createLoginEndpoint(slugs) {
|
|
|
4747
4991
|
JSON.stringify({
|
|
4748
4992
|
message: "Login successful",
|
|
4749
4993
|
user: result.user,
|
|
4750
|
-
token: result.token,
|
|
4751
4994
|
exp: result.exp
|
|
4752
4995
|
}),
|
|
4753
4996
|
{ status: 200, headers }
|
|
@@ -4774,8 +5017,6 @@ function createLoginEndpoint(slugs) {
|
|
|
4774
5017
|
}
|
|
4775
5018
|
};
|
|
4776
5019
|
}
|
|
4777
|
-
var sendLimiter = new RateLimiter(60 * 60 * 1e3, 3);
|
|
4778
|
-
var verifyLimiter = new RateLimiter(15 * 60 * 1e3, 5);
|
|
4779
5020
|
function generateSecureCode() {
|
|
4780
5021
|
const buf = crypto3.randomBytes(4);
|
|
4781
5022
|
const num = buf.readUInt32BE(0) % 9e5 + 1e5;
|
|
@@ -4788,7 +5029,9 @@ function hashCode(code) {
|
|
|
4788
5029
|
}
|
|
4789
5030
|
return createHmac("sha256", secret).update(code).digest("hex");
|
|
4790
5031
|
}
|
|
4791
|
-
function createAuth2faEndpoint(slugs) {
|
|
5032
|
+
function createAuth2faEndpoint(slugs, store) {
|
|
5033
|
+
const sendLimiter = new RateLimiter(60 * 60 * 1e3, 3, store);
|
|
5034
|
+
const verifyLimiter = new RateLimiter(15 * 60 * 1e3, 5, store);
|
|
4792
5035
|
return {
|
|
4793
5036
|
path: "/support/2fa",
|
|
4794
5037
|
method: "post",
|
|
@@ -4807,7 +5050,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4807
5050
|
}
|
|
4808
5051
|
const genericSendResponse = { success: true, message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9." };
|
|
4809
5052
|
if (action === "send") {
|
|
4810
|
-
if (sendLimiter.check(email)) {
|
|
5053
|
+
if (await sendLimiter.check(email, req)) {
|
|
4811
5054
|
return Response.json(genericSendResponse);
|
|
4812
5055
|
}
|
|
4813
5056
|
const clients = await dbFind(payload, slugs.supportClients, {
|
|
@@ -4848,7 +5091,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4848
5091
|
if (!code) {
|
|
4849
5092
|
return Response.json({ error: "Code manquant" }, { status: 400 });
|
|
4850
5093
|
}
|
|
4851
|
-
if (verifyLimiter.check(email)) {
|
|
5094
|
+
if (await verifyLimiter.check(email, req)) {
|
|
4852
5095
|
return Response.json(
|
|
4853
5096
|
{ error: "Trop de tentatives. R\xE9essayez dans 15 minutes." },
|
|
4854
5097
|
{ status: 429 }
|
|
@@ -5059,7 +5302,7 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
5059
5302
|
"Set-Cookie",
|
|
5060
5303
|
`payload-token=${token}; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=Lax; Path=/; Max-Age=${tokenExpiration}`
|
|
5061
5304
|
);
|
|
5062
|
-
return new Response(JSON.stringify({
|
|
5305
|
+
return new Response(JSON.stringify({ user: clientDoc, exp }), {
|
|
5063
5306
|
status: 200,
|
|
5064
5307
|
headers
|
|
5065
5308
|
});
|
|
@@ -5262,7 +5505,6 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
5262
5505
|
}
|
|
5263
5506
|
};
|
|
5264
5507
|
}
|
|
5265
|
-
var importLimiter = new RateLimiter(36e5, 10);
|
|
5266
5508
|
function parseStructuredMarkdown(markdown) {
|
|
5267
5509
|
const clientMatch = markdown.match(
|
|
5268
5510
|
/\*\*Client\s*:\*\*\s*(.+?)\s*[—–-]\s*(.+?)\s*\(([^)]+@[^)]+)\)/i
|
|
@@ -5365,7 +5607,8 @@ ONLY JSON, nothing else.`
|
|
|
5365
5607
|
return null;
|
|
5366
5608
|
}
|
|
5367
5609
|
}
|
|
5368
|
-
function createImportConversationEndpoint(slugs) {
|
|
5610
|
+
function createImportConversationEndpoint(slugs, store) {
|
|
5611
|
+
const importLimiter = new RateLimiter(36e5, 10, store);
|
|
5369
5612
|
return {
|
|
5370
5613
|
path: "/support/import-conversation",
|
|
5371
5614
|
method: "post",
|
|
@@ -5374,7 +5617,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5374
5617
|
const payload = req.payload;
|
|
5375
5618
|
const webhookSecret = req.headers.get("x-webhook-secret");
|
|
5376
5619
|
let isAuthed = false;
|
|
5377
|
-
if (webhookSecret
|
|
5620
|
+
if (verifySecret(webhookSecret, process.env.SUPPORT_WEBHOOK_SECRET)) {
|
|
5378
5621
|
isAuthed = true;
|
|
5379
5622
|
} else if (req.user && req.user.collection === slugs.users) {
|
|
5380
5623
|
isAuthed = true;
|
|
@@ -5383,7 +5626,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5383
5626
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5384
5627
|
}
|
|
5385
5628
|
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
|
5386
|
-
if (importLimiter.check(ip)) {
|
|
5629
|
+
if (await importLimiter.check(ip, req)) {
|
|
5387
5630
|
return Response.json({ error: "Rate limit exceeded. Maximum 10 imports per hour." }, { status: 429 });
|
|
5388
5631
|
}
|
|
5389
5632
|
let body;
|
|
@@ -5575,7 +5818,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5575
5818
|
handler: async (req) => {
|
|
5576
5819
|
const secret = req.headers.get("x-cron-secret");
|
|
5577
5820
|
const expectedSecret = process.env.CRON_SECRET;
|
|
5578
|
-
if (!
|
|
5821
|
+
if (!verifySecret(secret, expectedSecret)) {
|
|
5579
5822
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5580
5823
|
}
|
|
5581
5824
|
try {
|
|
@@ -5684,7 +5927,7 @@ function createProcessSnoozeEndpoint(slugs) {
|
|
|
5684
5927
|
handler: async (req) => {
|
|
5685
5928
|
const secret = req.headers.get("x-cron-secret");
|
|
5686
5929
|
const expectedSecret = process.env.CRON_SECRET;
|
|
5687
|
-
if (!
|
|
5930
|
+
if (!verifySecret(secret, expectedSecret)) {
|
|
5688
5931
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5689
5932
|
}
|
|
5690
5933
|
try {
|
|
@@ -5744,7 +5987,7 @@ function createProcessDigestsEndpoint(slugs) {
|
|
|
5744
5987
|
method: "post",
|
|
5745
5988
|
handler: async (req) => {
|
|
5746
5989
|
const secret = req.headers.get("x-cron-secret");
|
|
5747
|
-
if (!
|
|
5990
|
+
if (!verifySecret(secret, process.env.CRON_SECRET)) {
|
|
5748
5991
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5749
5992
|
}
|
|
5750
5993
|
try {
|
|
@@ -5817,7 +6060,7 @@ function createChannelsWebhookEndpoint(slugs) {
|
|
|
5817
6060
|
method: "post",
|
|
5818
6061
|
handler: async (req) => {
|
|
5819
6062
|
const secret = req.headers.get("x-channel-secret");
|
|
5820
|
-
if (!
|
|
6063
|
+
if (!verifySecret(secret, process.env.CHANNELS_WEBHOOK_SECRET)) {
|
|
5821
6064
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5822
6065
|
}
|
|
5823
6066
|
let body;
|
|
@@ -6172,8 +6415,8 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
6172
6415
|
// src/endpoints/transfer-ticket.ts
|
|
6173
6416
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6174
6417
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6175
|
-
|
|
6176
|
-
|
|
6418
|
+
function createTransferTicketEndpoint(slugs, store) {
|
|
6419
|
+
const transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY, store);
|
|
6177
6420
|
return {
|
|
6178
6421
|
path: "/support/tickets/:id/transfer",
|
|
6179
6422
|
method: "post",
|
|
@@ -6208,7 +6451,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
6208
6451
|
if (!isAdmin && !isOwner) {
|
|
6209
6452
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
6210
6453
|
}
|
|
6211
|
-
if (transferLimiter.check(`${req.user.id}:${ticketId}
|
|
6454
|
+
if (await transferLimiter.check(`${req.user.id}:${ticketId}`, req)) {
|
|
6212
6455
|
return Response.json(
|
|
6213
6456
|
{ error: `Limite atteinte (${MAX_TRANSFERS_PER_DAY} transferts par 24h sur ce ticket)` },
|
|
6214
6457
|
{ status: 429 }
|
|
@@ -6355,8 +6598,8 @@ function statusToLabel(status) {
|
|
|
6355
6598
|
}
|
|
6356
6599
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6357
6600
|
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6358
|
-
|
|
6359
|
-
|
|
6601
|
+
function createInviteCollaboratorEndpoint(slugs, store) {
|
|
6602
|
+
const inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10, store);
|
|
6360
6603
|
return {
|
|
6361
6604
|
path: "/support/tickets/:id/invite",
|
|
6362
6605
|
method: "post",
|
|
@@ -6391,7 +6634,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
6391
6634
|
if (!isAdmin && !isOwner) {
|
|
6392
6635
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
6393
6636
|
}
|
|
6394
|
-
if (inviteLimiter.check(String(req.user.id))) {
|
|
6637
|
+
if (await inviteLimiter.check(String(req.user.id), req)) {
|
|
6395
6638
|
return Response.json({ error: "Trop d'invitations. R\xE9essayez plus tard." }, { status: 429 });
|
|
6396
6639
|
}
|
|
6397
6640
|
const collabCount = await dbCount(payload, "ticket-collaborators", { where: { ticket: { equals: ticketId } }, overrideAccess: true }).catch(() => ({ totalDocs: 0 }));
|
|
@@ -6522,6 +6765,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
6522
6765
|
// src/endpoints/index.ts
|
|
6523
6766
|
function createSupportEndpoints(slugs, options) {
|
|
6524
6767
|
const f = options?.features;
|
|
6768
|
+
const rateLimitStore = options?.rateLimitStore;
|
|
6525
6769
|
const endpoints = [
|
|
6526
6770
|
createSearchEndpoint(slugs),
|
|
6527
6771
|
createKbSearchEndpoint(slugs),
|
|
@@ -6531,26 +6775,26 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6531
6775
|
createExportCsvEndpoint(slugs),
|
|
6532
6776
|
createExportDataEndpoint(slugs),
|
|
6533
6777
|
createSeedKbEndpoint(slugs),
|
|
6534
|
-
createLoginEndpoint(slugs),
|
|
6535
|
-
createAuth2faEndpoint(slugs),
|
|
6778
|
+
createLoginEndpoint(slugs, rateLimitStore),
|
|
6779
|
+
createAuth2faEndpoint(slugs, rateLimitStore),
|
|
6536
6780
|
createOAuthGoogleEndpoint(slugs, options?.oauth),
|
|
6537
6781
|
createDeleteAccountEndpoint(slugs),
|
|
6538
6782
|
createMergeClientsEndpoint(slugs),
|
|
6539
|
-
createImportConversationEndpoint(slugs),
|
|
6783
|
+
createImportConversationEndpoint(slugs, rateLimitStore),
|
|
6540
6784
|
createPurgeLogsEndpoint(slugs),
|
|
6541
|
-
createResendNotificationEndpoint(slugs),
|
|
6785
|
+
createResendNotificationEndpoint(slugs, rateLimitStore),
|
|
6542
6786
|
createUserPrefsGetEndpoint(slugs),
|
|
6543
6787
|
createUserPrefsPostEndpoint(slugs),
|
|
6544
6788
|
createEscalateEndpoint(slugs),
|
|
6545
6789
|
createTicketFeedbackEndpoint(slugs),
|
|
6546
|
-
createTransferTicketEndpoint(slugs),
|
|
6547
|
-
createInviteCollaboratorEndpoint(slugs)
|
|
6790
|
+
createTransferTicketEndpoint(slugs, rateLimitStore),
|
|
6791
|
+
createInviteCollaboratorEndpoint(slugs, rateLimitStore)
|
|
6548
6792
|
];
|
|
6549
6793
|
if (!f || f.ai !== false) {
|
|
6550
|
-
endpoints.push(createAiEndpoint(slugs));
|
|
6551
|
-
endpoints.push(createAiAgentEndpoint(slugs));
|
|
6552
|
-
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
6553
|
-
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
6794
|
+
endpoints.push(createAiEndpoint(slugs, rateLimitStore));
|
|
6795
|
+
endpoints.push(createAiAgentEndpoint(slugs, rateLimitStore));
|
|
6796
|
+
endpoints.push(...createClientIntelligenceEndpoint(slugs, rateLimitStore));
|
|
6797
|
+
endpoints.push(createTicketSynthesisEndpoint(slugs, options?.capabilities?.aiSummaries, rateLimitStore));
|
|
6554
6798
|
}
|
|
6555
6799
|
if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs));
|
|
6556
6800
|
if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs));
|
|
@@ -6565,18 +6809,18 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6565
6809
|
if (!f || f.sla !== false) endpoints.push(createSlaCheckEndpoint(slugs));
|
|
6566
6810
|
if (!f || f.autoClose !== false) {
|
|
6567
6811
|
endpoints.push(createAutoCloseEndpoint(slugs));
|
|
6568
|
-
endpoints.push(createSendReminderEndpoint(slugs));
|
|
6812
|
+
endpoints.push(createSendReminderEndpoint(slugs, rateLimitStore));
|
|
6569
6813
|
}
|
|
6570
6814
|
if (!f || f.customStatuses !== false) endpoints.push(createStatusesEndpoint(slugs));
|
|
6571
6815
|
if (!f || f.macros !== false) endpoints.push(createApplyMacroEndpoint(slugs));
|
|
6572
6816
|
if (!f || f.roundRobin !== false) {
|
|
6573
6817
|
endpoints.push(createRoundRobinConfigGetEndpoint(slugs), createRoundRobinConfigPostEndpoint(slugs));
|
|
6574
6818
|
}
|
|
6575
|
-
if (!f || f.chatbot !== false) endpoints.push(createChatbotEndpoint(slugs));
|
|
6819
|
+
if (!f || f.chatbot !== false) endpoints.push(createChatbotEndpoint(slugs, rateLimitStore));
|
|
6576
6820
|
if (!f || f.chat !== false) {
|
|
6577
|
-
endpoints.push(createChatGetEndpoint(slugs), createChatPostEndpoint(slugs));
|
|
6821
|
+
endpoints.push(createChatGetEndpoint(slugs), createChatPostEndpoint(slugs, rateLimitStore));
|
|
6578
6822
|
endpoints.push(createChatStreamEndpoint(slugs));
|
|
6579
|
-
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
6823
|
+
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs, rateLimitStore));
|
|
6580
6824
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
6581
6825
|
}
|
|
6582
6826
|
if (!f || f.timeTracking !== false) {
|
|
@@ -6594,6 +6838,16 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6594
6838
|
endpoints.push(createChannelsWebhookEndpoint(slugs));
|
|
6595
6839
|
endpoints.push(createVapidKeyEndpoint());
|
|
6596
6840
|
endpoints.push(createPushSubscribeEndpoint(slugs));
|
|
6841
|
+
if (options?.capabilities?.inboundEmail) {
|
|
6842
|
+
endpoints.push(createInboundEmailEndpoint(options.capabilities.inboundEmail, rateLimitStore));
|
|
6843
|
+
}
|
|
6844
|
+
if (options?.capabilities?.projectSuggestions) {
|
|
6845
|
+
endpoints.push(createProjectSuggestionsEndpoint(slugs, options.capabilities.projectSuggestions, rateLimitStore));
|
|
6846
|
+
}
|
|
6847
|
+
if (options?.capabilities?.aiTitles) {
|
|
6848
|
+
endpoints.push(createTicketTitleEndpoint(slugs, options.capabilities.aiTitles, rateLimitStore));
|
|
6849
|
+
endpoints.push(createGenerateMissingTitlesEndpoint(slugs, options.capabilities.aiTitles, rateLimitStore));
|
|
6850
|
+
}
|
|
6597
6851
|
return endpoints;
|
|
6598
6852
|
}
|
|
6599
6853
|
|
|
@@ -7230,38 +7484,69 @@ async function resolveAgentTeamIds(payload, slugs, userId) {
|
|
|
7230
7484
|
}
|
|
7231
7485
|
|
|
7232
7486
|
// src/collections/Tickets.ts
|
|
7233
|
-
|
|
7487
|
+
var ticketNumberQueues = /* @__PURE__ */ new Map();
|
|
7488
|
+
async function withTicketNumberLock(key, operation) {
|
|
7489
|
+
const previous = ticketNumberQueues.get(key) ?? Promise.resolve();
|
|
7490
|
+
let release;
|
|
7491
|
+
const gate = new Promise((resolve) => {
|
|
7492
|
+
release = resolve;
|
|
7493
|
+
});
|
|
7494
|
+
const queued = previous.then(() => gate);
|
|
7495
|
+
ticketNumberQueues.set(key, queued);
|
|
7496
|
+
await previous;
|
|
7497
|
+
try {
|
|
7498
|
+
return await operation();
|
|
7499
|
+
} finally {
|
|
7500
|
+
release();
|
|
7501
|
+
if (ticketNumberQueues.get(key) === queued) ticketNumberQueues.delete(key);
|
|
7502
|
+
}
|
|
7503
|
+
}
|
|
7504
|
+
function createAssignTicketNumber(slugs, options) {
|
|
7234
7505
|
return async ({ data, operation, req }) => {
|
|
7235
7506
|
if (operation === "create") {
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7507
|
+
const value = await withTicketNumberLock(slugs.tickets, async () => {
|
|
7508
|
+
const counterResult = await req.payload.find({
|
|
7509
|
+
collection: slugs.counters,
|
|
7510
|
+
where: { key: { equals: slugs.tickets } },
|
|
7511
|
+
limit: 1,
|
|
7512
|
+
depth: 0,
|
|
7513
|
+
overrideAccess: true,
|
|
7514
|
+
req
|
|
7515
|
+
});
|
|
7516
|
+
const counter = counterResult.docs[0];
|
|
7517
|
+
if (counter) {
|
|
7518
|
+
const allocated2 = Number(counter.nextValue);
|
|
7519
|
+
await req.payload.update({
|
|
7520
|
+
collection: slugs.counters,
|
|
7521
|
+
id: counter.id,
|
|
7522
|
+
data: { nextValue: allocated2 + 1 },
|
|
7523
|
+
overrideAccess: true,
|
|
7524
|
+
req
|
|
7251
7525
|
});
|
|
7252
|
-
|
|
7253
|
-
const suffix = Date.now() % 1e4;
|
|
7254
|
-
data.ticketNumber = `TK-${String(baseNumber + suffix).padStart(4, "0")}`;
|
|
7255
|
-
}
|
|
7256
|
-
break;
|
|
7257
|
-
} catch (error) {
|
|
7258
|
-
if (retries > 1 && (error?.message?.includes("UNIQUE") || error?.message?.includes("unique") || error?.code === "SQLITE_CONSTRAINT")) {
|
|
7259
|
-
retries--;
|
|
7260
|
-
continue;
|
|
7261
|
-
}
|
|
7262
|
-
throw error;
|
|
7526
|
+
return allocated2;
|
|
7263
7527
|
}
|
|
7264
|
-
|
|
7528
|
+
const tickets = await req.payload.find({
|
|
7529
|
+
collection: slugs.tickets,
|
|
7530
|
+
limit: 0,
|
|
7531
|
+
depth: 0,
|
|
7532
|
+
overrideAccess: true,
|
|
7533
|
+
select: { ticketNumber: true },
|
|
7534
|
+
req
|
|
7535
|
+
});
|
|
7536
|
+
const highest = tickets.docs.reduce((max, ticket) => {
|
|
7537
|
+
const match = String(ticket.ticketNumber || "").match(/(\d+)$/);
|
|
7538
|
+
return match ? Math.max(max, Number(match[1])) : max;
|
|
7539
|
+
}, 0);
|
|
7540
|
+
const allocated = highest + 1;
|
|
7541
|
+
await req.payload.create({
|
|
7542
|
+
collection: slugs.counters,
|
|
7543
|
+
data: { key: slugs.tickets, nextValue: allocated + 1 },
|
|
7544
|
+
overrideAccess: true,
|
|
7545
|
+
req
|
|
7546
|
+
});
|
|
7547
|
+
return allocated;
|
|
7548
|
+
});
|
|
7549
|
+
data.ticketNumber = `${options?.prefix ?? "TK-"}${String(value).padStart(options?.padding ?? 4, "0")}`;
|
|
7265
7550
|
}
|
|
7266
7551
|
return data;
|
|
7267
7552
|
};
|
|
@@ -7354,7 +7639,7 @@ function createTrackSLA(slugs) {
|
|
|
7354
7639
|
return doc;
|
|
7355
7640
|
};
|
|
7356
7641
|
}
|
|
7357
|
-
function createTrackAiSummaryOnResolve(slugs) {
|
|
7642
|
+
function createTrackAiSummaryOnResolve(slugs, generator) {
|
|
7358
7643
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
7359
7644
|
if (operation !== "update" || !previousDoc) return doc;
|
|
7360
7645
|
const wasResolved = previousDoc.status === "resolved";
|
|
@@ -7374,7 +7659,33 @@ function createTrackAiSummaryOnResolve(slugs) {
|
|
|
7374
7659
|
}
|
|
7375
7660
|
if (!wasResolved && isResolved && !doc.aiSummary) {
|
|
7376
7661
|
setImmediate(() => {
|
|
7377
|
-
generateTicketSynthesis({ payload: req.payload, slugs, ticketId: doc.id })
|
|
7662
|
+
const operation2 = generator ? generator.generate(req.payload, doc.id) : generateTicketSynthesis({ payload: req.payload, slugs, ticketId: doc.id });
|
|
7663
|
+
operation2.catch((err) => console.error("[support] Background ai synthesis failed:", err));
|
|
7664
|
+
});
|
|
7665
|
+
}
|
|
7666
|
+
return doc;
|
|
7667
|
+
};
|
|
7668
|
+
}
|
|
7669
|
+
function createTrackWaitingSince() {
|
|
7670
|
+
return ({ data, originalDoc, operation }) => {
|
|
7671
|
+
if (operation !== "update") return data;
|
|
7672
|
+
const previous = originalDoc?.status;
|
|
7673
|
+
const next = data.status ?? previous;
|
|
7674
|
+
if (next === "waiting_client" && previous !== "waiting_client") {
|
|
7675
|
+
data.waitingSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
7676
|
+
data.autoCloseRemindedAt = null;
|
|
7677
|
+
} else if (next !== "waiting_client" && previous === "waiting_client") {
|
|
7678
|
+
data.waitingSince = null;
|
|
7679
|
+
data.autoCloseRemindedAt = null;
|
|
7680
|
+
}
|
|
7681
|
+
return data;
|
|
7682
|
+
};
|
|
7683
|
+
}
|
|
7684
|
+
function createGenerateTitleOnCreate(generator) {
|
|
7685
|
+
return ({ doc, operation, req }) => {
|
|
7686
|
+
if (operation === "create" && doc?.id && !doc.displayTitle) {
|
|
7687
|
+
setImmediate(() => {
|
|
7688
|
+
generator.generate(req.payload, doc.id).catch((err) => console.error("[support] Background title generation failed:", err));
|
|
7378
7689
|
});
|
|
7379
7690
|
}
|
|
7380
7691
|
return doc;
|
|
@@ -7592,6 +7903,7 @@ function createCascadeDelete(slugs) {
|
|
|
7592
7903
|
function createTicketsCollection(slugs, options) {
|
|
7593
7904
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7594
7905
|
const dynamicFields = [];
|
|
7906
|
+
const capabilities = options?.capabilities;
|
|
7595
7907
|
dynamicFields.push({
|
|
7596
7908
|
name: "conversation",
|
|
7597
7909
|
type: "ui",
|
|
@@ -7610,6 +7922,28 @@ function createTicketsCollection(slugs, options) {
|
|
|
7610
7922
|
admin: { position: "sidebar" }
|
|
7611
7923
|
});
|
|
7612
7924
|
}
|
|
7925
|
+
if (capabilities?.aiTitles) {
|
|
7926
|
+
dynamicFields.push(
|
|
7927
|
+
{
|
|
7928
|
+
name: "displayTitle",
|
|
7929
|
+
type: "text",
|
|
7930
|
+
label: "Titre d'affichage",
|
|
7931
|
+
admin: { description: "Titre court sugg\xE9r\xE9 par l'IA, \xE9ditable sans modifier le sujet email." }
|
|
7932
|
+
},
|
|
7933
|
+
{
|
|
7934
|
+
name: "displayTitleStatus",
|
|
7935
|
+
type: "select",
|
|
7936
|
+
defaultValue: "none",
|
|
7937
|
+
options: [
|
|
7938
|
+
{ label: "Aucun", value: "none" },
|
|
7939
|
+
{ label: "G\xE9n\xE9ration en cours", value: "pending" },
|
|
7940
|
+
{ label: "Sugg\xE9r\xE9", value: "suggested" },
|
|
7941
|
+
{ label: "Valid\xE9", value: "validated" },
|
|
7942
|
+
{ label: "\xC9chec", value: "error" }
|
|
7943
|
+
]
|
|
7944
|
+
}
|
|
7945
|
+
);
|
|
7946
|
+
}
|
|
7613
7947
|
const billingFields = [
|
|
7614
7948
|
{
|
|
7615
7949
|
type: "row",
|
|
@@ -7688,6 +8022,39 @@ function createTicketsCollection(slugs, options) {
|
|
|
7688
8022
|
]
|
|
7689
8023
|
}
|
|
7690
8024
|
];
|
|
8025
|
+
if (capabilities?.detailedBilling) {
|
|
8026
|
+
billingFields.push(
|
|
8027
|
+
{
|
|
8028
|
+
name: "billedAt",
|
|
8029
|
+
type: "date",
|
|
8030
|
+
label: "Factur\xE9 le"
|
|
8031
|
+
},
|
|
8032
|
+
{
|
|
8033
|
+
name: "billingLines",
|
|
8034
|
+
type: "array",
|
|
8035
|
+
label: "Lignes de facturation",
|
|
8036
|
+
fields: [
|
|
8037
|
+
{ name: "period", type: "text", label: "P\xE9riode (AAAA-MM)" },
|
|
8038
|
+
{ name: "label", type: "text", label: "Libell\xE9" },
|
|
8039
|
+
{ name: "amount", type: "number", label: "Montant (\u20AC)" },
|
|
8040
|
+
{ name: "billingType", type: "select", options: ["hourly", "flat"], defaultValue: "flat", label: "Type" },
|
|
8041
|
+
{ name: "billed", type: "checkbox", defaultValue: false, label: "Factur\xE9" },
|
|
8042
|
+
{ name: "billedAt", type: "date", label: "Factur\xE9 le" }
|
|
8043
|
+
]
|
|
8044
|
+
}
|
|
8045
|
+
);
|
|
8046
|
+
}
|
|
8047
|
+
if (capabilities?.volunteering) {
|
|
8048
|
+
billingFields.push(
|
|
8049
|
+
{ name: "volunteer", type: "checkbox", defaultValue: false, label: "B\xE9n\xE9volat (non factur\xE9)" },
|
|
8050
|
+
{
|
|
8051
|
+
name: "volunteerValue",
|
|
8052
|
+
type: "number",
|
|
8053
|
+
label: "Valeur offerte (\u20AC)",
|
|
8054
|
+
admin: { condition: (data) => Boolean(data?.volunteer) }
|
|
8055
|
+
}
|
|
8056
|
+
);
|
|
8057
|
+
}
|
|
7691
8058
|
return {
|
|
7692
8059
|
slug: slugs.tickets,
|
|
7693
8060
|
labels: { singular: "Ticket", plural: "Tickets" },
|
|
@@ -7838,7 +8205,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7838
8205
|
},
|
|
7839
8206
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7840
8207
|
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7841
|
-
{ name: "autoCloseScheduledAt", type: "date", index: true, label: "Fermeture auto programmee", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" }, description: "Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client." } }
|
|
8208
|
+
{ name: "autoCloseScheduledAt", type: "date", index: true, label: "Fermeture auto programmee", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" }, description: "Echeance ferme posee par une relance manuelle. Le ticket se ferme apres cette date sans reponse client." } },
|
|
8209
|
+
{ name: "waitingSince", type: "date", index: true, label: "En attente depuis", admin: { readOnly: true } },
|
|
8210
|
+
{
|
|
8211
|
+
name: "relanceDelayDays",
|
|
8212
|
+
type: "select",
|
|
8213
|
+
defaultValue: "2",
|
|
8214
|
+
label: "D\xE9lai de relance client",
|
|
8215
|
+
options: [
|
|
8216
|
+
{ label: "1 jour", value: "1" },
|
|
8217
|
+
{ label: "2 jours", value: "2" },
|
|
8218
|
+
{ label: "3 jours", value: "3" }
|
|
8219
|
+
]
|
|
8220
|
+
},
|
|
8221
|
+
{ name: "satisfactionRemindedAt", type: "date", admin: { hidden: true, readOnly: true } }
|
|
7842
8222
|
]
|
|
7843
8223
|
},
|
|
7844
8224
|
// Sidebar
|
|
@@ -7913,15 +8293,17 @@ function createTicketsCollection(slugs, options) {
|
|
|
7913
8293
|
],
|
|
7914
8294
|
hooks: {
|
|
7915
8295
|
beforeChange: [
|
|
7916
|
-
createAssignTicketNumber(slugs),
|
|
8296
|
+
createAssignTicketNumber(slugs, options?.ticketNumber),
|
|
7917
8297
|
createAssignClientOnCreate(slugs),
|
|
7918
8298
|
createAutoAssignAdmin(slugs),
|
|
7919
8299
|
autoPaidAt,
|
|
8300
|
+
createTrackWaitingSince(),
|
|
7920
8301
|
createRestrictClientUpdates(slugs)
|
|
7921
8302
|
],
|
|
7922
8303
|
afterChange: [
|
|
7923
8304
|
createTrackSLA(slugs),
|
|
7924
|
-
createTrackAiSummaryOnResolve(slugs),
|
|
8305
|
+
createTrackAiSummaryOnResolve(slugs, capabilities?.aiSummaries),
|
|
8306
|
+
...capabilities?.aiTitles ? [createGenerateTitleOnCreate(capabilities.aiTitles)] : [],
|
|
7925
8307
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7926
8308
|
createPauseSlaOnHold(slugs),
|
|
7927
8309
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
@@ -8426,6 +8808,56 @@ function createNotifyMentions(slugs) {
|
|
|
8426
8808
|
return doc;
|
|
8427
8809
|
};
|
|
8428
8810
|
}
|
|
8811
|
+
function createSmsNotification(slugs, capability) {
|
|
8812
|
+
return async ({ doc, operation, req }) => {
|
|
8813
|
+
try {
|
|
8814
|
+
if (operation !== "create" || !doc.notifyBySms) return doc;
|
|
8815
|
+
if (doc.authorType !== "admin" || doc.isInternal) return doc;
|
|
8816
|
+
if (doc.scheduledAt && !doc.scheduledSent) return doc;
|
|
8817
|
+
if (capability.adapter.isConfigured && !capability.adapter.isConfigured()) return doc;
|
|
8818
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8819
|
+
const ticket = await req.payload.findByID({
|
|
8820
|
+
collection: slugs.tickets,
|
|
8821
|
+
id: ticketId,
|
|
8822
|
+
depth: 1,
|
|
8823
|
+
overrideAccess: true
|
|
8824
|
+
});
|
|
8825
|
+
const client = typeof ticket.client === "object" && ticket.client ? ticket.client : null;
|
|
8826
|
+
if (!client) return doc;
|
|
8827
|
+
const target = typeof doc.smsTo === "string" && doc.smsTo.trim() || String(client.phone || "");
|
|
8828
|
+
if (!target) return doc;
|
|
8829
|
+
const fallback = `ConsilioWEB : nouvelle r\xE9ponse au ticket ${String(ticket.ticketNumber || "")}.`;
|
|
8830
|
+
const message = typeof doc.smsMessage === "string" && doc.smsMessage.trim() || (capability.buildMessage ? await capability.buildMessage({ client, ticket }) : fallback);
|
|
8831
|
+
const result = await capability.adapter.send({
|
|
8832
|
+
message,
|
|
8833
|
+
to: target,
|
|
8834
|
+
payload: req.payload,
|
|
8835
|
+
ticket,
|
|
8836
|
+
client
|
|
8837
|
+
});
|
|
8838
|
+
if (!result.sent && !result.skipped) {
|
|
8839
|
+
req.payload.logger?.warn(`[support:sms] Delivery failed: ${result.error || "unknown error"}`);
|
|
8840
|
+
}
|
|
8841
|
+
} catch (error) {
|
|
8842
|
+
req.payload.logger?.warn(`[support:sms] Adapter error: ${error instanceof Error ? error.message : String(error)}`);
|
|
8843
|
+
}
|
|
8844
|
+
return doc;
|
|
8845
|
+
};
|
|
8846
|
+
}
|
|
8847
|
+
function createThreadCleanup(capability) {
|
|
8848
|
+
return async ({ doc, operation, req }) => {
|
|
8849
|
+
if (operation !== "create" || doc.isInternal) return doc;
|
|
8850
|
+
if (doc.authorType !== "client" && doc.authorType !== "email") return doc;
|
|
8851
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8852
|
+
if (!ticketId) return doc;
|
|
8853
|
+
try {
|
|
8854
|
+
await capability.clean(req.payload, ticketId, doc.id);
|
|
8855
|
+
} catch (error) {
|
|
8856
|
+
req.payload.logger?.warn(`[support:thread-cleanup] ${error instanceof Error ? error.message : String(error)}`);
|
|
8857
|
+
}
|
|
8858
|
+
return doc;
|
|
8859
|
+
};
|
|
8860
|
+
}
|
|
8429
8861
|
function createTicketMessagesCollection(slugs, options) {
|
|
8430
8862
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
8431
8863
|
return {
|
|
@@ -8486,11 +8918,23 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8486
8918
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
8487
8919
|
{ name: "emailSentAt", type: "date", label: "Email envoye le", admin: { hidden: true } },
|
|
8488
8920
|
{ name: "emailSentTo", type: "text", label: "Email envoye a", admin: { hidden: true } },
|
|
8489
|
-
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
8921
|
+
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } },
|
|
8922
|
+
...options?.capabilities?.sms ? [
|
|
8923
|
+
{ name: "notifyBySms", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
8924
|
+
{ name: "smsTo", type: "text", admin: { hidden: true } },
|
|
8925
|
+
{ name: "smsMessage", type: "textarea", admin: { hidden: true } }
|
|
8926
|
+
] : [],
|
|
8927
|
+
...options?.capabilities?.threadCleanup ? [
|
|
8928
|
+
{ name: "isMarkedAsNoise", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
8929
|
+
{ name: "noiseReason", type: "text", admin: { hidden: true } },
|
|
8930
|
+
{ name: "emailContext", type: "json", admin: { hidden: true } },
|
|
8931
|
+
{ name: "emailBodyOriginal", type: "textarea", admin: { hidden: true } }
|
|
8932
|
+
] : []
|
|
8490
8933
|
],
|
|
8491
8934
|
hooks: {
|
|
8492
8935
|
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
8493
8936
|
afterChange: [
|
|
8937
|
+
...options?.capabilities?.sms ? [createSmsNotification(slugs, options.capabilities.sms)] : [],
|
|
8494
8938
|
createAutoUpdateStatus(slugs),
|
|
8495
8939
|
createNotifyClient(slugs),
|
|
8496
8940
|
createTrackFirstResponse(slugs),
|
|
@@ -8499,7 +8943,8 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8499
8943
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
8500
8944
|
createFireMessageWebhooks(slugs),
|
|
8501
8945
|
createDispatchWebhookOnReply(slugs),
|
|
8502
|
-
createNotifyMentions(slugs)
|
|
8946
|
+
createNotifyMentions(slugs),
|
|
8947
|
+
...options?.capabilities?.threadCleanup ? [createThreadCleanup(options.capabilities.threadCleanup)] : []
|
|
8503
8948
|
]
|
|
8504
8949
|
},
|
|
8505
8950
|
access: {
|
|
@@ -8589,7 +9034,7 @@ function createEnforce2FA(slugs) {
|
|
|
8589
9034
|
return user;
|
|
8590
9035
|
};
|
|
8591
9036
|
}
|
|
8592
|
-
function createSupportClientsCollection(slugs) {
|
|
9037
|
+
function createSupportClientsCollection(slugs, options) {
|
|
8593
9038
|
return {
|
|
8594
9039
|
slug: slugs.supportClients,
|
|
8595
9040
|
labels: {
|
|
@@ -8851,7 +9296,45 @@ function createSupportClientsCollection(slugs) {
|
|
|
8851
9296
|
description: "Visible uniquement par les admins",
|
|
8852
9297
|
position: "sidebar"
|
|
8853
9298
|
}
|
|
8854
|
-
}
|
|
9299
|
+
},
|
|
9300
|
+
...options?.capabilities?.sms ? [
|
|
9301
|
+
{
|
|
9302
|
+
name: "notifyBySmsChannel",
|
|
9303
|
+
type: "checkbox",
|
|
9304
|
+
defaultValue: false,
|
|
9305
|
+
label: "Canal SMS activ\xE9",
|
|
9306
|
+
admin: { position: "sidebar" }
|
|
9307
|
+
}
|
|
9308
|
+
] : [],
|
|
9309
|
+
...options?.capabilities?.aiTitles || options?.capabilities?.aiSummaries ? [
|
|
9310
|
+
{
|
|
9311
|
+
name: "preferredFormality",
|
|
9312
|
+
type: "select",
|
|
9313
|
+
defaultValue: "auto",
|
|
9314
|
+
label: "Formule IA",
|
|
9315
|
+
options: [
|
|
9316
|
+
{ label: "Auto", value: "auto" },
|
|
9317
|
+
{ label: "Tutoiement", value: "tutoyer" },
|
|
9318
|
+
{ label: "Vouvoiement", value: "vouvoyer" }
|
|
9319
|
+
],
|
|
9320
|
+
admin: { position: "sidebar" }
|
|
9321
|
+
},
|
|
9322
|
+
{
|
|
9323
|
+
name: "preferredTone",
|
|
9324
|
+
type: "select",
|
|
9325
|
+
defaultValue: "auto",
|
|
9326
|
+
label: "Ton IA",
|
|
9327
|
+
options: [
|
|
9328
|
+
{ label: "Auto", value: "auto" },
|
|
9329
|
+
{ label: "Neutre / professionnel", value: "neutre" },
|
|
9330
|
+
{ label: "Amical", value: "amical" },
|
|
9331
|
+
{ label: "Direct / concis", value: "direct" },
|
|
9332
|
+
{ label: "Formel", value: "formel" }
|
|
9333
|
+
],
|
|
9334
|
+
admin: { position: "sidebar" }
|
|
9335
|
+
},
|
|
9336
|
+
{ name: "lastRewriteStyle", type: "text", admin: { hidden: true } }
|
|
9337
|
+
] : []
|
|
8855
9338
|
],
|
|
8856
9339
|
hooks: {
|
|
8857
9340
|
beforeLogin: [createEnforce2FA(slugs)],
|
|
@@ -9466,7 +9949,7 @@ function createPendingEmailsCollection(slugs) {
|
|
|
9466
9949
|
create: ({ req }) => {
|
|
9467
9950
|
if (req.user?.collection === slugs.users) return true;
|
|
9468
9951
|
const webhookSecret = req.headers.get("x-webhook-secret");
|
|
9469
|
-
if (webhookSecret
|
|
9952
|
+
if (verifySecret(webhookSecret, process.env.SUPPORT_WEBHOOK_SECRET)) return true;
|
|
9470
9953
|
return false;
|
|
9471
9954
|
}
|
|
9472
9955
|
},
|
|
@@ -10599,6 +11082,40 @@ function createPushSubscriptionCollection(slugs) {
|
|
|
10599
11082
|
};
|
|
10600
11083
|
}
|
|
10601
11084
|
|
|
11085
|
+
// src/collections/SupportRateLimits.ts
|
|
11086
|
+
function createSupportRateLimitsCollection(slug = "support-rate-limits") {
|
|
11087
|
+
return {
|
|
11088
|
+
slug,
|
|
11089
|
+
admin: { hidden: true },
|
|
11090
|
+
access: {
|
|
11091
|
+
create: () => false,
|
|
11092
|
+
read: () => false,
|
|
11093
|
+
update: () => false,
|
|
11094
|
+
delete: () => false
|
|
11095
|
+
},
|
|
11096
|
+
fields: [
|
|
11097
|
+
{ name: "key", type: "text", required: true, unique: true, index: true },
|
|
11098
|
+
{ name: "count", type: "number", required: true, min: 1 },
|
|
11099
|
+
{ name: "resetAt", type: "date", required: true, index: true }
|
|
11100
|
+
],
|
|
11101
|
+
timestamps: false
|
|
11102
|
+
};
|
|
11103
|
+
}
|
|
11104
|
+
|
|
11105
|
+
// src/collections/SupportCounters.ts
|
|
11106
|
+
function createSupportCountersCollection(slug = "support-counters") {
|
|
11107
|
+
return {
|
|
11108
|
+
slug,
|
|
11109
|
+
admin: { hidden: true },
|
|
11110
|
+
access: { create: () => false, read: () => false, update: () => false, delete: () => false },
|
|
11111
|
+
fields: [
|
|
11112
|
+
{ name: "key", type: "text", required: true, unique: true, index: true },
|
|
11113
|
+
{ name: "nextValue", type: "number", required: true, min: 1 }
|
|
11114
|
+
],
|
|
11115
|
+
timestamps: false
|
|
11116
|
+
};
|
|
11117
|
+
}
|
|
11118
|
+
|
|
10602
11119
|
// src/plugin.ts
|
|
10603
11120
|
function viewConfig(component, path) {
|
|
10604
11121
|
return { Component: component, path };
|
|
@@ -10614,21 +11131,25 @@ function supportPlugin(config) {
|
|
|
10614
11131
|
...config?.collectionSlugs,
|
|
10615
11132
|
users: config?.userCollectionSlug || "users"
|
|
10616
11133
|
});
|
|
11134
|
+
const rateLimitStore = config?.rateLimitStore === "payload" ? new PayloadRateLimitStore(slugs.rateLimits) : config?.rateLimitStore;
|
|
10617
11135
|
return (incomingConfig) => {
|
|
10618
11136
|
const existingCollections = incomingConfig.collections || [];
|
|
10619
11137
|
const ticketOptions = {
|
|
10620
11138
|
conversationComponent: config?.conversationComponent,
|
|
10621
11139
|
projectCollectionSlug: config?.projectCollectionSlug,
|
|
10622
11140
|
documentsCollectionSlug: config?.documentsCollectionSlug,
|
|
10623
|
-
notificationSlug: config?.notificationSlug
|
|
11141
|
+
notificationSlug: config?.notificationSlug,
|
|
11142
|
+
ticketNumber: config?.ticketNumber,
|
|
11143
|
+
capabilities: config?.capabilities
|
|
10624
11144
|
};
|
|
10625
11145
|
const messageOptions = {
|
|
10626
|
-
notificationSlug: config?.notificationSlug
|
|
11146
|
+
notificationSlug: config?.notificationSlug,
|
|
11147
|
+
capabilities: config?.capabilities
|
|
10627
11148
|
};
|
|
10628
11149
|
const supportCollections = [
|
|
10629
11150
|
createTicketsCollection(slugs, ticketOptions),
|
|
10630
11151
|
createTicketMessagesCollection(slugs, messageOptions),
|
|
10631
|
-
createSupportClientsCollection(slugs),
|
|
11152
|
+
createSupportClientsCollection(slugs, { capabilities: config?.capabilities }),
|
|
10632
11153
|
createCannedResponsesCollection(slugs),
|
|
10633
11154
|
createTicketActivityLogCollection(slugs),
|
|
10634
11155
|
createSatisfactionSurveysCollection(slugs),
|
|
@@ -10638,8 +11159,12 @@ function supportPlugin(config) {
|
|
|
10638
11159
|
createNotificationQueueCollection(slugs),
|
|
10639
11160
|
createAutomationRulesCollection(slugs),
|
|
10640
11161
|
createSupportTeamCollection(slugs),
|
|
10641
|
-
createPushSubscriptionCollection(slugs)
|
|
11162
|
+
createPushSubscriptionCollection(slugs),
|
|
11163
|
+
createSupportCountersCollection(slugs.counters)
|
|
10642
11164
|
];
|
|
11165
|
+
if (config?.rateLimitStore === "payload") {
|
|
11166
|
+
supportCollections.push(createSupportRateLimitsCollection(slugs.rateLimits));
|
|
11167
|
+
}
|
|
10643
11168
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
10644
11169
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|
|
10645
11170
|
if (features.emailTracking !== false) supportCollections.push(createEmailLogsCollection(slugs));
|
|
@@ -10677,7 +11202,9 @@ function supportPlugin(config) {
|
|
|
10677
11202
|
const existingEndpoints = incomingConfig.endpoints || [];
|
|
10678
11203
|
const supportEndpoints = createSupportEndpoints(slugs, {
|
|
10679
11204
|
oauth: { allowedEmailDomains: config?.allowedEmailDomains },
|
|
10680
|
-
features
|
|
11205
|
+
features,
|
|
11206
|
+
rateLimitStore,
|
|
11207
|
+
capabilities: config?.capabilities
|
|
10681
11208
|
});
|
|
10682
11209
|
return {
|
|
10683
11210
|
...incomingConfig,
|
|
@@ -10694,4 +11221,4 @@ function supportPlugin(config) {
|
|
|
10694
11221
|
};
|
|
10695
11222
|
}
|
|
10696
11223
|
|
|
10697
|
-
export { DEFAULT_FEATURES, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_USER_PREFS, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createMacrosCollection, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketCollaboratorsCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, readSupportSettings, readUserPrefs, resolveSlugs, supportPlugin };
|
|
11224
|
+
export { DEFAULT_FEATURES, DEFAULT_INBOUND_EMAIL_LIMITS, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_USER_PREFS, MemoryRateLimitStore, PayloadRateLimitStore, RateLimiter, calculateBusinessHoursDeadline, createAdminNotification, createAssignSlaDeadlines, createAuthLogsCollection, createCannedResponsesCollection, createChatMessagesCollection, createCheckSlaOnReply, createCheckSlaOnResolve, createEmailLogsCollection, createKnowledgeBaseCollection, createLoginEndpoint, createMacrosCollection, createOAuthGoogleEndpoint, createPendingEmailsCollection, createSatisfactionSurveysCollection, createSlaPoliciesCollection, createSupportClientsCollection, createTicketActivityLogCollection, createTicketCollaboratorsCollection, createTicketMessagesCollection, createTicketStatusEmail, createTicketStatusesCollection, createTicketsCollection, createTimeEntriesCollection, createTrackOpenEndpoint, createWebhookEndpointsCollection, dispatchWebhook, generateTicketSynthesis, readSupportSettings, readUserPrefs, resolveSlugs, supportPlugin, validateInboundEmailPayload, verifySecret };
|