@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.cjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var payload = require('payload');
|
|
3
4
|
var crypto3 = require('crypto');
|
|
4
5
|
var PDFDocument = require('pdfkit');
|
|
5
|
-
var payload = require('payload');
|
|
6
6
|
var webpush = require('web-push');
|
|
7
7
|
var sanitizeHtml = require('sanitize-html');
|
|
8
8
|
|
|
@@ -75,12 +75,263 @@ var DEFAULT_SLUGS = {
|
|
|
75
75
|
automationRules: "automation-rules",
|
|
76
76
|
supportTeams: "support-teams",
|
|
77
77
|
pushSubscriptions: "push-subscriptions",
|
|
78
|
+
rateLimits: "support-rate-limits",
|
|
79
|
+
counters: "support-counters",
|
|
78
80
|
users: "users",
|
|
79
81
|
media: "media"
|
|
80
82
|
};
|
|
81
83
|
function resolveSlugs(overrides) {
|
|
82
84
|
return { ...DEFAULT_SLUGS, ...overrides };
|
|
83
85
|
}
|
|
86
|
+
var MemoryRateLimitStore = class {
|
|
87
|
+
entries = /* @__PURE__ */ new Map();
|
|
88
|
+
async increment(key, windowMs) {
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const current = this.entries.get(key);
|
|
91
|
+
const next = !current || now > current.resetAt ? { count: 1, resetAt: now + windowMs } : { ...current, count: current.count + 1 };
|
|
92
|
+
this.entries.set(key, next);
|
|
93
|
+
return next;
|
|
94
|
+
}
|
|
95
|
+
async reset(key) {
|
|
96
|
+
this.entries.delete(key);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
var PayloadRateLimitStore = class {
|
|
100
|
+
constructor(collectionSlug = "support-rate-limits") {
|
|
101
|
+
this.collectionSlug = collectionSlug;
|
|
102
|
+
}
|
|
103
|
+
collectionSlug;
|
|
104
|
+
async increment(key, windowMs, context) {
|
|
105
|
+
const { payload: payload$1, req } = this.resolveContext(context);
|
|
106
|
+
const ownsTransaction = req ? await payload.initTransaction(req) : false;
|
|
107
|
+
try {
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const result = await payload$1.find({
|
|
110
|
+
collection: this.collectionSlug,
|
|
111
|
+
where: { key: { equals: key } },
|
|
112
|
+
limit: 1,
|
|
113
|
+
depth: 0,
|
|
114
|
+
overrideAccess: true,
|
|
115
|
+
...req ? { req } : {}
|
|
116
|
+
});
|
|
117
|
+
const current = result.docs[0];
|
|
118
|
+
const currentResetAt = current?.resetAt ? new Date(String(current.resetAt)).getTime() : 0;
|
|
119
|
+
const next = !current || now > currentResetAt ? { count: 1, resetAt: now + windowMs } : { count: Number(current.count || 0) + 1, resetAt: currentResetAt };
|
|
120
|
+
if (current?.id != null) {
|
|
121
|
+
await payload$1.update({
|
|
122
|
+
collection: this.collectionSlug,
|
|
123
|
+
id: current.id,
|
|
124
|
+
data: { count: next.count, resetAt: new Date(next.resetAt).toISOString() },
|
|
125
|
+
overrideAccess: true,
|
|
126
|
+
...req ? { req } : {}
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
await payload$1.create({
|
|
130
|
+
collection: this.collectionSlug,
|
|
131
|
+
data: { key, count: next.count, resetAt: new Date(next.resetAt).toISOString() },
|
|
132
|
+
overrideAccess: true,
|
|
133
|
+
...req ? { req } : {}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (ownsTransaction && req) await payload.commitTransaction(req);
|
|
137
|
+
return next;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
if (ownsTransaction && req) await payload.killTransaction(req);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async reset(key, context) {
|
|
144
|
+
const { payload: payload$1, req } = this.resolveContext(context);
|
|
145
|
+
const ownsTransaction = req ? await payload.initTransaction(req) : false;
|
|
146
|
+
try {
|
|
147
|
+
await payload$1.delete({
|
|
148
|
+
collection: this.collectionSlug,
|
|
149
|
+
where: { key: { equals: key } },
|
|
150
|
+
overrideAccess: true,
|
|
151
|
+
...req ? { req } : {}
|
|
152
|
+
});
|
|
153
|
+
if (ownsTransaction && req) await payload.commitTransaction(req);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (ownsTransaction && req) await payload.killTransaction(req);
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
resolveContext(context) {
|
|
160
|
+
if (!context || typeof context !== "object") {
|
|
161
|
+
throw new Error("PayloadRateLimitStore requires the current Payload request or instance");
|
|
162
|
+
}
|
|
163
|
+
if ("payload" in context) {
|
|
164
|
+
const req = context;
|
|
165
|
+
return { payload: req.payload, req };
|
|
166
|
+
}
|
|
167
|
+
return { payload: context };
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var RateLimiter = class {
|
|
171
|
+
constructor(windowMs, maxRequests, store) {
|
|
172
|
+
this.windowMs = windowMs;
|
|
173
|
+
this.maxRequests = maxRequests;
|
|
174
|
+
this.store = store ?? new MemoryRateLimitStore();
|
|
175
|
+
}
|
|
176
|
+
windowMs;
|
|
177
|
+
maxRequests;
|
|
178
|
+
store;
|
|
179
|
+
async check(key, context) {
|
|
180
|
+
const entry = context === void 0 ? await this.store.increment(key, this.windowMs) : await this.store.increment(key, this.windowMs, context);
|
|
181
|
+
return entry.count > this.maxRequests;
|
|
182
|
+
}
|
|
183
|
+
async reset(key, context) {
|
|
184
|
+
if (context === void 0) await this.store.reset(key);
|
|
185
|
+
else await this.store.reset(key, context);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
var DEFAULT_INBOUND_EMAIL_LIMITS = {
|
|
189
|
+
maxRequestBytes: 25 * 1024 * 1024,
|
|
190
|
+
maxAttachmentBytes: 10 * 1024 * 1024,
|
|
191
|
+
maxAttachments: 10,
|
|
192
|
+
maxSubjectLength: 1e3,
|
|
193
|
+
maxNameLength: 500,
|
|
194
|
+
maxEmailLength: 320,
|
|
195
|
+
maxBodyLength: 38e3
|
|
196
|
+
};
|
|
197
|
+
function verifySecret(provided, expected) {
|
|
198
|
+
if (!provided || !expected) return false;
|
|
199
|
+
const providedDigest = crypto3.createHash("sha256").update(provided).digest();
|
|
200
|
+
const expectedDigest = crypto3.createHash("sha256").update(expected).digest();
|
|
201
|
+
return crypto3.timingSafeEqual(providedDigest, expectedDigest);
|
|
202
|
+
}
|
|
203
|
+
function validateInboundEmailPayload(input, contentLength, limits = DEFAULT_INBOUND_EMAIL_LIMITS) {
|
|
204
|
+
if (contentLength != null && contentLength > limits.maxRequestBytes) {
|
|
205
|
+
return { code: "request_too_large", status: 413 };
|
|
206
|
+
}
|
|
207
|
+
const attachments = Array.isArray(input.attachments) ? input.attachments : [];
|
|
208
|
+
if (attachments.length > limits.maxAttachments) {
|
|
209
|
+
return { code: "too_many_attachments", status: 413 };
|
|
210
|
+
}
|
|
211
|
+
for (const attachment of attachments) {
|
|
212
|
+
const decodedSize = attachment.content ? Math.ceil(attachment.content.length * 0.75) : 0;
|
|
213
|
+
if (Math.max(Number(attachment.size || 0), decodedSize) > limits.maxAttachmentBytes) {
|
|
214
|
+
return { code: "attachment_too_large", status: 413 };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
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;
|
|
218
|
+
return tooLong ? { code: "text_field_too_large", status: 413 } : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/endpoints/capabilities.ts
|
|
222
|
+
function createInboundEmailEndpoint(capability, store) {
|
|
223
|
+
const limiter = new RateLimiter(6e4, 60, store);
|
|
224
|
+
return {
|
|
225
|
+
path: "/support-webhook/inbound-email",
|
|
226
|
+
method: "post",
|
|
227
|
+
handler: async (req) => {
|
|
228
|
+
const secretHeader = capability.secretHeader || "x-webhook-secret";
|
|
229
|
+
if (!verifySecret(req.headers.get(secretHeader), capability.secret)) {
|
|
230
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
231
|
+
}
|
|
232
|
+
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
|
|
233
|
+
if (await limiter.check(ip, req)) {
|
|
234
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
235
|
+
}
|
|
236
|
+
const declaredLength = Number(req.headers.get("content-length") || 0) || void 0;
|
|
237
|
+
let input;
|
|
238
|
+
try {
|
|
239
|
+
input = await req.json();
|
|
240
|
+
} catch {
|
|
241
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
242
|
+
}
|
|
243
|
+
const measuredLength = new TextEncoder().encode(JSON.stringify(input)).byteLength;
|
|
244
|
+
const validation = validateInboundEmailPayload(
|
|
245
|
+
input,
|
|
246
|
+
Math.max(declaredLength || 0, measuredLength)
|
|
247
|
+
);
|
|
248
|
+
if (validation) return Response.json({ error: validation.code }, { status: validation.status });
|
|
249
|
+
return capability.handle(req, input);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
function createProjectSuggestionsEndpoint(slugs, capability, store) {
|
|
254
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
255
|
+
return {
|
|
256
|
+
path: "/support/suggest-projects",
|
|
257
|
+
method: "post",
|
|
258
|
+
handler: async (req) => {
|
|
259
|
+
if (!req.user || req.user.collection !== slugs.users) {
|
|
260
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
261
|
+
}
|
|
262
|
+
const key = req.user?.id ? String(req.user.id) : "anonymous";
|
|
263
|
+
if (await limiter.check(key, req)) {
|
|
264
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
265
|
+
}
|
|
266
|
+
return capability.suggest(req);
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function createTicketTitleEndpoint(slugs, capability, store) {
|
|
271
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
272
|
+
return {
|
|
273
|
+
path: "/support/ticket-title",
|
|
274
|
+
method: "post",
|
|
275
|
+
handler: async (req) => {
|
|
276
|
+
if (!req.user || req.user.collection !== slugs.users) {
|
|
277
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
278
|
+
}
|
|
279
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
280
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
281
|
+
}
|
|
282
|
+
const ticketId = Number(new URL(req.url || "", "http://localhost").searchParams.get("ticketId"));
|
|
283
|
+
if (!Number.isFinite(ticketId) || ticketId <= 0) {
|
|
284
|
+
return Response.json({ error: "ticketId required" }, { status: 400 });
|
|
285
|
+
}
|
|
286
|
+
const title = await capability.generate(req.payload, ticketId);
|
|
287
|
+
if (!title) return Response.json({ error: "Generation unavailable" }, { status: 502 });
|
|
288
|
+
return Response.json({ title, status: "suggested" });
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function createGenerateMissingTitlesEndpoint(slugs, capability, store) {
|
|
293
|
+
const limiter = new RateLimiter(6e4, 5, store);
|
|
294
|
+
return {
|
|
295
|
+
path: "/support/generate-missing-titles",
|
|
296
|
+
method: "post",
|
|
297
|
+
handler: async (req) => {
|
|
298
|
+
if (!req.user || req.user.collection !== slugs.users) {
|
|
299
|
+
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
300
|
+
}
|
|
301
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
302
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
303
|
+
}
|
|
304
|
+
const limit = Math.min(
|
|
305
|
+
Math.max(Number(new URL(req.url || "", "http://localhost").searchParams.get("limit")) || 8, 1),
|
|
306
|
+
20
|
|
307
|
+
);
|
|
308
|
+
const where = {
|
|
309
|
+
and: [
|
|
310
|
+
{ displayTitle: { exists: false } },
|
|
311
|
+
{ displayTitleStatus: { not_equals: "error" } }
|
|
312
|
+
]
|
|
313
|
+
};
|
|
314
|
+
const batch = await req.payload.find({
|
|
315
|
+
collection: slugs.tickets,
|
|
316
|
+
where,
|
|
317
|
+
limit,
|
|
318
|
+
depth: 0,
|
|
319
|
+
overrideAccess: true,
|
|
320
|
+
sort: "-createdAt"
|
|
321
|
+
});
|
|
322
|
+
let generated = 0;
|
|
323
|
+
for (const ticket of batch.docs) {
|
|
324
|
+
if (await capability.generate(req.payload, ticket.id)) generated++;
|
|
325
|
+
}
|
|
326
|
+
const remaining = await req.payload.count({
|
|
327
|
+
collection: slugs.tickets,
|
|
328
|
+
where,
|
|
329
|
+
overrideAccess: true
|
|
330
|
+
});
|
|
331
|
+
return Response.json({ generated, remaining: remaining.totalDocs });
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
}
|
|
84
335
|
|
|
85
336
|
// src/utils/auth.ts
|
|
86
337
|
var AuthError = class extends Error {
|
|
@@ -192,8 +443,9 @@ async function readUserPrefs(payload, userId) {
|
|
|
192
443
|
}
|
|
193
444
|
|
|
194
445
|
// src/endpoints/ai.ts
|
|
195
|
-
function getClient(aiSettings) {
|
|
196
|
-
const
|
|
446
|
+
async function getClient(aiSettings) {
|
|
447
|
+
const moduleName = "@anthropic-ai/sdk";
|
|
448
|
+
const { default: Anthropic } = await import(moduleName);
|
|
197
449
|
if (aiSettings.provider === "ollama") {
|
|
198
450
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
199
451
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
@@ -203,7 +455,8 @@ function getClient(aiSettings) {
|
|
|
203
455
|
function getModel(aiSettings) {
|
|
204
456
|
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
205
457
|
}
|
|
206
|
-
function createAiEndpoint(slugs) {
|
|
458
|
+
function createAiEndpoint(slugs, store) {
|
|
459
|
+
const limiter = new RateLimiter(6e4, 30, store);
|
|
207
460
|
return {
|
|
208
461
|
path: "/support/ai",
|
|
209
462
|
method: "post",
|
|
@@ -211,6 +464,9 @@ function createAiEndpoint(slugs) {
|
|
|
211
464
|
try {
|
|
212
465
|
const payload = req.payload;
|
|
213
466
|
requireAdmin(req, slugs);
|
|
467
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
468
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
469
|
+
}
|
|
214
470
|
const settings = await readSupportSettings(payload);
|
|
215
471
|
const aiSettings = settings.ai;
|
|
216
472
|
let body;
|
|
@@ -220,7 +476,7 @@ function createAiEndpoint(slugs) {
|
|
|
220
476
|
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
221
477
|
}
|
|
222
478
|
const { action } = body;
|
|
223
|
-
const anthropic = getClient(aiSettings);
|
|
479
|
+
const anthropic = await getClient(aiSettings);
|
|
224
480
|
const model = getModel(aiSettings);
|
|
225
481
|
if (action === "sentiment") {
|
|
226
482
|
if (!aiSettings.enableSentiment) {
|
|
@@ -452,13 +708,17 @@ ${kbText || "(vide)"}`;
|
|
|
452
708
|
}
|
|
453
709
|
|
|
454
710
|
// src/endpoints/ai-agent.ts
|
|
455
|
-
function createAiAgentEndpoint(slugs) {
|
|
711
|
+
function createAiAgentEndpoint(slugs, store) {
|
|
712
|
+
const limiter = new RateLimiter(6e4, 10, store);
|
|
456
713
|
return {
|
|
457
714
|
path: "/support/ai-agent",
|
|
458
715
|
method: "post",
|
|
459
716
|
handler: async (req) => {
|
|
460
717
|
try {
|
|
461
718
|
requireAdmin(req, slugs);
|
|
719
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
720
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
721
|
+
}
|
|
462
722
|
let body = {};
|
|
463
723
|
try {
|
|
464
724
|
body = await req.json();
|
|
@@ -482,8 +742,9 @@ function createAiAgentEndpoint(slugs) {
|
|
|
482
742
|
}
|
|
483
743
|
|
|
484
744
|
// src/endpoints/client-intelligence.ts
|
|
485
|
-
function getClient3(aiSettings) {
|
|
486
|
-
const
|
|
745
|
+
async function getClient3(aiSettings) {
|
|
746
|
+
const moduleName = "@anthropic-ai/sdk";
|
|
747
|
+
const { default: Anthropic } = await import(moduleName);
|
|
487
748
|
if (aiSettings.provider === "ollama") {
|
|
488
749
|
const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
|
|
489
750
|
return new Anthropic({ apiKey: "ollama", baseURL });
|
|
@@ -494,10 +755,14 @@ function getModel2(aiSettings) {
|
|
|
494
755
|
return aiSettings.model || "claude-haiku-4-5-20251001";
|
|
495
756
|
}
|
|
496
757
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
497
|
-
function createClientIntelligenceEndpoint(slugs) {
|
|
758
|
+
function createClientIntelligenceEndpoint(slugs, store) {
|
|
759
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
498
760
|
const getHandler = async (req) => {
|
|
499
761
|
try {
|
|
500
762
|
requireAdmin(req, slugs);
|
|
763
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
764
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
765
|
+
}
|
|
501
766
|
const payload = req.payload;
|
|
502
767
|
const url = new URL(req.url || "", "http://localhost");
|
|
503
768
|
const clientId = url.searchParams.get("clientId");
|
|
@@ -527,6 +792,9 @@ function createClientIntelligenceEndpoint(slugs) {
|
|
|
527
792
|
const postHandler = async (req) => {
|
|
528
793
|
try {
|
|
529
794
|
requireAdmin(req, slugs);
|
|
795
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
796
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
797
|
+
}
|
|
530
798
|
const payload = req.payload;
|
|
531
799
|
const body = await req.json?.() || {};
|
|
532
800
|
const clientId = body.clientId;
|
|
@@ -636,7 +904,7 @@ R\xE9ponds en JSON strict (pas de markdown, pas de commentaires) avec cette stru
|
|
|
636
904
|
}
|
|
637
905
|
|
|
638
906
|
Sois factuel. Ne d\xE9passe pas 5 items par tableau. R\xE9ponds UNIQUEMENT avec le JSON.`;
|
|
639
|
-
const anthropic = getClient3(aiSettings);
|
|
907
|
+
const anthropic = await getClient3(aiSettings);
|
|
640
908
|
const model = getModel2(aiSettings);
|
|
641
909
|
const res = await anthropic.messages.create({
|
|
642
910
|
model,
|
|
@@ -1659,7 +1927,13 @@ var TRANSPARENT_GIF = Buffer.from(
|
|
|
1659
1927
|
"base64"
|
|
1660
1928
|
);
|
|
1661
1929
|
function generateTrackingToken(ticketId, messageId, secret) {
|
|
1662
|
-
return crypto3.createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex")
|
|
1930
|
+
return crypto3.createHmac("sha256", secret).update(`${ticketId}:${messageId}`).digest("hex");
|
|
1931
|
+
}
|
|
1932
|
+
function verifyTrackingToken(ticketId, messageId, signature, secret) {
|
|
1933
|
+
if (!/^[0-9a-f]{64}$/i.test(signature)) return false;
|
|
1934
|
+
const expected = Buffer.from(generateTrackingToken(ticketId, messageId, secret), "hex");
|
|
1935
|
+
const received = Buffer.from(signature, "hex");
|
|
1936
|
+
return expected.length === received.length && crypto3.timingSafeEqual(expected, received);
|
|
1663
1937
|
}
|
|
1664
1938
|
function createTrackOpenEndpoint(slugs) {
|
|
1665
1939
|
return {
|
|
@@ -1673,7 +1947,7 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1673
1947
|
const parsedId = ticketId ? Number(ticketId) : NaN;
|
|
1674
1948
|
const parsedMsgId = messageId ? Number(messageId) : NaN;
|
|
1675
1949
|
const secret = process.env.PAYLOAD_SECRET || "";
|
|
1676
|
-
const validSig = !!secret && !!ticketId && !!messageId && !!sig &&
|
|
1950
|
+
const validSig = !!secret && Number.isInteger(parsedId) && parsedId > 0 && Number.isInteger(parsedMsgId) && parsedMsgId > 0 && !!ticketId && !!messageId && !!sig && verifyTrackingToken(ticketId, messageId, sig, secret);
|
|
1677
1951
|
if (!validSig) {
|
|
1678
1952
|
return new Response(TRANSPARENT_GIF, {
|
|
1679
1953
|
status: 200,
|
|
@@ -1687,6 +1961,16 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1687
1961
|
if (ticketId && Number.isInteger(parsedId) && parsedId > 0) {
|
|
1688
1962
|
try {
|
|
1689
1963
|
const payload = req.payload;
|
|
1964
|
+
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1965
|
+
id: parsedMsgId,
|
|
1966
|
+
depth: 0,
|
|
1967
|
+
overrideAccess: true,
|
|
1968
|
+
select: { ticket: true, emailOpenedAt: true }
|
|
1969
|
+
});
|
|
1970
|
+
const messageTicketId = typeof msg?.ticket === "object" ? msg.ticket?.id : msg?.ticket;
|
|
1971
|
+
if (!msg || String(messageTicketId) !== String(parsedId)) {
|
|
1972
|
+
return transparentGifResponse();
|
|
1973
|
+
}
|
|
1690
1974
|
const ticket = await dbFindByID(payload, slugs.tickets, {
|
|
1691
1975
|
id: parsedId,
|
|
1692
1976
|
depth: 0,
|
|
@@ -1705,12 +1989,6 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1705
1989
|
}
|
|
1706
1990
|
}
|
|
1707
1991
|
if (Number.isInteger(parsedMsgId) && parsedMsgId > 0) {
|
|
1708
|
-
const msg = await dbFindByID(payload, slugs.ticketMessages, {
|
|
1709
|
-
id: parsedMsgId,
|
|
1710
|
-
depth: 0,
|
|
1711
|
-
overrideAccess: true,
|
|
1712
|
-
select: { emailOpenedAt: true }
|
|
1713
|
-
});
|
|
1714
1992
|
if (msg && !msg.emailOpenedAt) {
|
|
1715
1993
|
await dbUpdate(payload, slugs.ticketMessages, {
|
|
1716
1994
|
id: parsedMsgId,
|
|
@@ -1742,19 +2020,22 @@ function createTrackOpenEndpoint(slugs) {
|
|
|
1742
2020
|
console.error("[track-open] Error:", err);
|
|
1743
2021
|
}
|
|
1744
2022
|
}
|
|
1745
|
-
return
|
|
1746
|
-
status: 200,
|
|
1747
|
-
headers: {
|
|
1748
|
-
"Content-Type": "image/gif",
|
|
1749
|
-
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
1750
|
-
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
1751
|
-
"Pragma": "no-cache",
|
|
1752
|
-
"Expires": "0"
|
|
1753
|
-
}
|
|
1754
|
-
});
|
|
2023
|
+
return transparentGifResponse();
|
|
1755
2024
|
}
|
|
1756
2025
|
};
|
|
1757
2026
|
}
|
|
2027
|
+
function transparentGifResponse() {
|
|
2028
|
+
return new Response(TRANSPARENT_GIF, {
|
|
2029
|
+
status: 200,
|
|
2030
|
+
headers: {
|
|
2031
|
+
"Content-Type": "image/gif",
|
|
2032
|
+
"Content-Length": String(TRANSPARENT_GIF.length),
|
|
2033
|
+
"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0",
|
|
2034
|
+
"Pragma": "no-cache",
|
|
2035
|
+
"Expires": "0"
|
|
2036
|
+
}
|
|
2037
|
+
});
|
|
2038
|
+
}
|
|
1758
2039
|
|
|
1759
2040
|
// src/utils/emailTemplate.ts
|
|
1760
2041
|
var COLORS = {
|
|
@@ -2041,7 +2322,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
2041
2322
|
handler: async (req) => {
|
|
2042
2323
|
const secret = req.headers.get("x-cron-secret");
|
|
2043
2324
|
const expectedSecret = process.env.CRON_SECRET;
|
|
2044
|
-
if (!
|
|
2325
|
+
if (!verifySecret(secret, expectedSecret)) {
|
|
2045
2326
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
2046
2327
|
}
|
|
2047
2328
|
try {
|
|
@@ -2187,52 +2468,7 @@ function createAutoCloseEndpoint(slugs) {
|
|
|
2187
2468
|
};
|
|
2188
2469
|
}
|
|
2189
2470
|
|
|
2190
|
-
// src/utils/rateLimiter.ts
|
|
2191
|
-
var RateLimiter = class {
|
|
2192
|
-
constructor(windowMs, maxRequests) {
|
|
2193
|
-
this.windowMs = windowMs;
|
|
2194
|
-
this.maxRequests = maxRequests;
|
|
2195
|
-
const timer = setInterval(() => this.cleanup(), windowMs);
|
|
2196
|
-
timer.unref();
|
|
2197
|
-
}
|
|
2198
|
-
windowMs;
|
|
2199
|
-
maxRequests;
|
|
2200
|
-
store = /* @__PURE__ */ new Map();
|
|
2201
|
-
/**
|
|
2202
|
-
* Check if a key has exceeded the rate limit.
|
|
2203
|
-
* Returns true if the request should be blocked.
|
|
2204
|
-
*/
|
|
2205
|
-
check(key) {
|
|
2206
|
-
const now = Date.now();
|
|
2207
|
-
const entry = this.store.get(key);
|
|
2208
|
-
if (!entry || now > entry.resetAt) {
|
|
2209
|
-
this.store.set(key, { count: 1, resetAt: now + this.windowMs });
|
|
2210
|
-
return false;
|
|
2211
|
-
}
|
|
2212
|
-
entry.count++;
|
|
2213
|
-
return entry.count > this.maxRequests;
|
|
2214
|
-
}
|
|
2215
|
-
/**
|
|
2216
|
-
* Reset the counter for a specific key.
|
|
2217
|
-
*/
|
|
2218
|
-
reset(key) {
|
|
2219
|
-
this.store.delete(key);
|
|
2220
|
-
}
|
|
2221
|
-
/**
|
|
2222
|
-
* Remove expired entries from the store.
|
|
2223
|
-
*/
|
|
2224
|
-
cleanup() {
|
|
2225
|
-
const now = Date.now();
|
|
2226
|
-
for (const [key, entry] of this.store) {
|
|
2227
|
-
if (now > entry.resetAt) {
|
|
2228
|
-
this.store.delete(key);
|
|
2229
|
-
}
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
};
|
|
2233
|
-
|
|
2234
2471
|
// src/endpoints/send-reminder.ts
|
|
2235
|
-
var reminderLimiter = new RateLimiter(60 * 60 * 1e3, 30);
|
|
2236
2472
|
var MIN_HOURS = 1;
|
|
2237
2473
|
var MAX_HOURS = 24 * 30;
|
|
2238
2474
|
function formatFr(date, withTime) {
|
|
@@ -2244,7 +2480,8 @@ function formatFr(date, withTime) {
|
|
|
2244
2480
|
timeZone: "Europe/Paris"
|
|
2245
2481
|
});
|
|
2246
2482
|
}
|
|
2247
|
-
function createSendReminderEndpoint(slugs) {
|
|
2483
|
+
function createSendReminderEndpoint(slugs, store) {
|
|
2484
|
+
const reminderLimiter = new RateLimiter(60 * 60 * 1e3, 30, store);
|
|
2248
2485
|
return {
|
|
2249
2486
|
path: "/support/send-reminder",
|
|
2250
2487
|
method: "post",
|
|
@@ -2252,7 +2489,7 @@ function createSendReminderEndpoint(slugs) {
|
|
|
2252
2489
|
try {
|
|
2253
2490
|
const payload = req.payload;
|
|
2254
2491
|
requireAdmin(req, slugs);
|
|
2255
|
-
if (reminderLimiter.check(String(req.user.id))) {
|
|
2492
|
+
if (await reminderLimiter.check(String(req.user.id), req)) {
|
|
2256
2493
|
return Response.json(
|
|
2257
2494
|
{ error: "Trop de relances. R\xE9essayez dans une heure." },
|
|
2258
2495
|
{ status: 429 }
|
|
@@ -2555,15 +2792,15 @@ function createPurgeLogsEndpoint(slugs) {
|
|
|
2555
2792
|
}
|
|
2556
2793
|
|
|
2557
2794
|
// src/endpoints/chatbot.ts
|
|
2558
|
-
|
|
2559
|
-
|
|
2795
|
+
function createChatbotEndpoint(slugs, store) {
|
|
2796
|
+
const chatbotLimiter = new RateLimiter(6e4, 10, store);
|
|
2560
2797
|
return {
|
|
2561
2798
|
path: "/support/chatbot",
|
|
2562
2799
|
method: "post",
|
|
2563
2800
|
handler: async (req) => {
|
|
2564
2801
|
try {
|
|
2565
2802
|
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
|
|
2566
|
-
if (chatbotLimiter.check(ip)) {
|
|
2803
|
+
if (await chatbotLimiter.check(ip, req)) {
|
|
2567
2804
|
return Response.json({ error: "Too many requests. Please wait a moment." }, { status: 429 });
|
|
2568
2805
|
}
|
|
2569
2806
|
let body;
|
|
@@ -2644,8 +2881,6 @@ R\xE9ponds en fran\xE7ais, de mani\xE8re concise et utile. Si tu ne trouves pas
|
|
|
2644
2881
|
}
|
|
2645
2882
|
};
|
|
2646
2883
|
}
|
|
2647
|
-
var chatSessionLimiter = new RateLimiter(36e5, 5);
|
|
2648
|
-
var chatMessageLimiter = new RateLimiter(6e4, 15);
|
|
2649
2884
|
function createChatGetEndpoint(slugs) {
|
|
2650
2885
|
return {
|
|
2651
2886
|
path: "/support/chat",
|
|
@@ -2689,7 +2924,9 @@ function createChatGetEndpoint(slugs) {
|
|
|
2689
2924
|
}
|
|
2690
2925
|
};
|
|
2691
2926
|
}
|
|
2692
|
-
function createChatPostEndpoint(slugs) {
|
|
2927
|
+
function createChatPostEndpoint(slugs, store) {
|
|
2928
|
+
const chatSessionLimiter = new RateLimiter(36e5, 5, store);
|
|
2929
|
+
const chatMessageLimiter = new RateLimiter(6e4, 15, store);
|
|
2693
2930
|
return {
|
|
2694
2931
|
path: "/support/chat",
|
|
2695
2932
|
method: "post",
|
|
@@ -2706,7 +2943,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2706
2943
|
const { action, session, message } = body;
|
|
2707
2944
|
const userId = String(req.user.id);
|
|
2708
2945
|
if (action === "start") {
|
|
2709
|
-
if (chatSessionLimiter.check(userId)) {
|
|
2946
|
+
if (await chatSessionLimiter.check(userId, req)) {
|
|
2710
2947
|
return Response.json({ error: "Trop de sessions cr\xE9\xE9es. R\xE9essayez plus tard." }, { status: 429 });
|
|
2711
2948
|
}
|
|
2712
2949
|
const sessionId = `chat_${crypto3__default.default.randomBytes(16).toString("hex")}`;
|
|
@@ -2723,7 +2960,7 @@ function createChatPostEndpoint(slugs) {
|
|
|
2723
2960
|
return Response.json({ session: sessionId, messages: [systemMsg] });
|
|
2724
2961
|
}
|
|
2725
2962
|
if (action === "send" && session && message) {
|
|
2726
|
-
if (chatMessageLimiter.check(userId)) {
|
|
2963
|
+
if (await chatMessageLimiter.check(userId, req)) {
|
|
2727
2964
|
return Response.json({ error: "Trop de messages. Attendez un moment." }, { status: 429 });
|
|
2728
2965
|
}
|
|
2729
2966
|
const trimmedMessage = String(message).trim();
|
|
@@ -2889,7 +3126,6 @@ function createChatStreamEndpoint(slugs) {
|
|
|
2889
3126
|
}
|
|
2890
3127
|
|
|
2891
3128
|
// src/endpoints/admin-chat.ts
|
|
2892
|
-
var adminChatLimiter = new RateLimiter(6e4, 30);
|
|
2893
3129
|
function createAdminChatGetEndpoint(slugs) {
|
|
2894
3130
|
return {
|
|
2895
3131
|
path: "/support/admin-chat",
|
|
@@ -2963,7 +3199,8 @@ function createAdminChatGetEndpoint(slugs) {
|
|
|
2963
3199
|
}
|
|
2964
3200
|
};
|
|
2965
3201
|
}
|
|
2966
|
-
function createAdminChatPostEndpoint(slugs) {
|
|
3202
|
+
function createAdminChatPostEndpoint(slugs, store) {
|
|
3203
|
+
const adminChatLimiter = new RateLimiter(6e4, 30, store);
|
|
2967
3204
|
return {
|
|
2968
3205
|
path: "/support/admin-chat",
|
|
2969
3206
|
method: "post",
|
|
@@ -2992,7 +3229,7 @@ function createAdminChatPostEndpoint(slugs) {
|
|
|
2992
3229
|
}
|
|
2993
3230
|
const clientId = typeof sessionMsg.docs[0].client === "object" ? sessionMsg.docs[0].client.id : sessionMsg.docs[0].client;
|
|
2994
3231
|
if (action === "send" && message) {
|
|
2995
|
-
if (adminChatLimiter.check(String(req.user.id))) {
|
|
3232
|
+
if (await adminChatLimiter.check(String(req.user.id), req)) {
|
|
2996
3233
|
return Response.json({ error: "Rate limit atteint." }, { status: 429 });
|
|
2997
3234
|
}
|
|
2998
3235
|
const trimmedMessage = String(message).trim();
|
|
@@ -3946,13 +4183,17 @@ Reponds UNIQUEMENT avec la liste de puces, rien d'autre.`;
|
|
|
3946
4183
|
}
|
|
3947
4184
|
|
|
3948
4185
|
// src/endpoints/ticket-synthesis.ts
|
|
3949
|
-
function createTicketSynthesisEndpoint(slugs) {
|
|
4186
|
+
function createTicketSynthesisEndpoint(slugs, generator, store) {
|
|
4187
|
+
const limiter = new RateLimiter(6e4, 20, store);
|
|
3950
4188
|
return {
|
|
3951
4189
|
path: "/support/ticket-synthesis",
|
|
3952
4190
|
method: "post",
|
|
3953
4191
|
handler: async (req) => {
|
|
3954
4192
|
try {
|
|
3955
4193
|
requireAdmin(req, slugs);
|
|
4194
|
+
if (await limiter.check(String(req.user.id), req)) {
|
|
4195
|
+
return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
|
|
4196
|
+
}
|
|
3956
4197
|
const payload = req.payload;
|
|
3957
4198
|
const url = new URL(req.url || "", "http://localhost");
|
|
3958
4199
|
const ticketIdRaw = url.searchParams.get("ticketId");
|
|
@@ -3978,7 +4219,10 @@ function createTicketSynthesisEndpoint(slugs) {
|
|
|
3978
4219
|
});
|
|
3979
4220
|
}
|
|
3980
4221
|
}
|
|
3981
|
-
const result = await generateTicketSynthesis({ payload, slugs, ticketId });
|
|
4222
|
+
const result = generator ? await generator.generate(payload, ticketId) : await generateTicketSynthesis({ payload, slugs, ticketId });
|
|
4223
|
+
if (!result) {
|
|
4224
|
+
return Response.json({ error: "Generation unavailable" }, { status: 502 });
|
|
4225
|
+
}
|
|
3982
4226
|
return Response.json(result);
|
|
3983
4227
|
} catch (err) {
|
|
3984
4228
|
const authResponse = handleAuthError(err);
|
|
@@ -4497,8 +4741,8 @@ function createPendingEmailsProcessEndpoint(slugs) {
|
|
|
4497
4741
|
}
|
|
4498
4742
|
|
|
4499
4743
|
// src/endpoints/resend-notification.ts
|
|
4500
|
-
|
|
4501
|
-
|
|
4744
|
+
function createResendNotificationEndpoint(slugs, store) {
|
|
4745
|
+
const resendLimiter = new RateLimiter(60 * 60 * 1e3, 10, store);
|
|
4502
4746
|
return {
|
|
4503
4747
|
path: "/support/resend-notification",
|
|
4504
4748
|
method: "post",
|
|
@@ -4506,7 +4750,7 @@ function createResendNotificationEndpoint(slugs) {
|
|
|
4506
4750
|
try {
|
|
4507
4751
|
const payload = req.payload;
|
|
4508
4752
|
requireAdmin(req, slugs);
|
|
4509
|
-
if (resendLimiter.check(String(req.user.id))) {
|
|
4753
|
+
if (await resendLimiter.check(String(req.user.id), req)) {
|
|
4510
4754
|
return Response.json(
|
|
4511
4755
|
{ error: "Trop de renvois. R\xE9essayez dans une heure." },
|
|
4512
4756
|
{ status: 429 }
|
|
@@ -4709,14 +4953,14 @@ function createSeedKbEndpoint(slugs) {
|
|
|
4709
4953
|
}
|
|
4710
4954
|
|
|
4711
4955
|
// src/endpoints/login.ts
|
|
4712
|
-
|
|
4713
|
-
|
|
4956
|
+
function createLoginEndpoint(slugs, store) {
|
|
4957
|
+
const loginLimiter = new RateLimiter(15 * 6e4, 10, store);
|
|
4714
4958
|
return {
|
|
4715
4959
|
path: "/support/login",
|
|
4716
4960
|
method: "post",
|
|
4717
4961
|
handler: async (req) => {
|
|
4718
4962
|
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
|
|
4719
|
-
if (loginLimiter.check(ip)) {
|
|
4963
|
+
if (await loginLimiter.check(ip, req)) {
|
|
4720
4964
|
return Response.json(
|
|
4721
4965
|
{ error: "Trop de tentatives. R\xE9essayez dans quelques minutes." },
|
|
4722
4966
|
{ status: 429 }
|
|
@@ -4756,7 +5000,6 @@ function createLoginEndpoint(slugs) {
|
|
|
4756
5000
|
JSON.stringify({
|
|
4757
5001
|
message: "Login successful",
|
|
4758
5002
|
user: result.user,
|
|
4759
|
-
token: result.token,
|
|
4760
5003
|
exp: result.exp
|
|
4761
5004
|
}),
|
|
4762
5005
|
{ status: 200, headers }
|
|
@@ -4783,8 +5026,6 @@ function createLoginEndpoint(slugs) {
|
|
|
4783
5026
|
}
|
|
4784
5027
|
};
|
|
4785
5028
|
}
|
|
4786
|
-
var sendLimiter = new RateLimiter(60 * 60 * 1e3, 3);
|
|
4787
|
-
var verifyLimiter = new RateLimiter(15 * 60 * 1e3, 5);
|
|
4788
5029
|
function generateSecureCode() {
|
|
4789
5030
|
const buf = crypto3__default.default.randomBytes(4);
|
|
4790
5031
|
const num = buf.readUInt32BE(0) % 9e5 + 1e5;
|
|
@@ -4797,7 +5038,9 @@ function hashCode(code) {
|
|
|
4797
5038
|
}
|
|
4798
5039
|
return crypto3.createHmac("sha256", secret).update(code).digest("hex");
|
|
4799
5040
|
}
|
|
4800
|
-
function createAuth2faEndpoint(slugs) {
|
|
5041
|
+
function createAuth2faEndpoint(slugs, store) {
|
|
5042
|
+
const sendLimiter = new RateLimiter(60 * 60 * 1e3, 3, store);
|
|
5043
|
+
const verifyLimiter = new RateLimiter(15 * 60 * 1e3, 5, store);
|
|
4801
5044
|
return {
|
|
4802
5045
|
path: "/support/2fa",
|
|
4803
5046
|
method: "post",
|
|
@@ -4816,7 +5059,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4816
5059
|
}
|
|
4817
5060
|
const genericSendResponse = { success: true, message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9." };
|
|
4818
5061
|
if (action === "send") {
|
|
4819
|
-
if (sendLimiter.check(email)) {
|
|
5062
|
+
if (await sendLimiter.check(email, req)) {
|
|
4820
5063
|
return Response.json(genericSendResponse);
|
|
4821
5064
|
}
|
|
4822
5065
|
const clients = await dbFind(payload, slugs.supportClients, {
|
|
@@ -4857,7 +5100,7 @@ function createAuth2faEndpoint(slugs) {
|
|
|
4857
5100
|
if (!code) {
|
|
4858
5101
|
return Response.json({ error: "Code manquant" }, { status: 400 });
|
|
4859
5102
|
}
|
|
4860
|
-
if (verifyLimiter.check(email)) {
|
|
5103
|
+
if (await verifyLimiter.check(email, req)) {
|
|
4861
5104
|
return Response.json(
|
|
4862
5105
|
{ error: "Trop de tentatives. R\xE9essayez dans 15 minutes." },
|
|
4863
5106
|
{ status: 429 }
|
|
@@ -5068,7 +5311,7 @@ function createOAuthGoogleEndpoint(slugs, options) {
|
|
|
5068
5311
|
"Set-Cookie",
|
|
5069
5312
|
`payload-token=${token}; HttpOnly; ${cookieSecure ? "Secure; " : ""}SameSite=Lax; Path=/; Max-Age=${tokenExpiration}`
|
|
5070
5313
|
);
|
|
5071
|
-
return new Response(JSON.stringify({
|
|
5314
|
+
return new Response(JSON.stringify({ user: clientDoc, exp }), {
|
|
5072
5315
|
status: 200,
|
|
5073
5316
|
headers
|
|
5074
5317
|
});
|
|
@@ -5271,7 +5514,6 @@ function createMergeClientsEndpoint(slugs) {
|
|
|
5271
5514
|
}
|
|
5272
5515
|
};
|
|
5273
5516
|
}
|
|
5274
|
-
var importLimiter = new RateLimiter(36e5, 10);
|
|
5275
5517
|
function parseStructuredMarkdown(markdown) {
|
|
5276
5518
|
const clientMatch = markdown.match(
|
|
5277
5519
|
/\*\*Client\s*:\*\*\s*(.+?)\s*[—–-]\s*(.+?)\s*\(([^)]+@[^)]+)\)/i
|
|
@@ -5374,7 +5616,8 @@ ONLY JSON, nothing else.`
|
|
|
5374
5616
|
return null;
|
|
5375
5617
|
}
|
|
5376
5618
|
}
|
|
5377
|
-
function createImportConversationEndpoint(slugs) {
|
|
5619
|
+
function createImportConversationEndpoint(slugs, store) {
|
|
5620
|
+
const importLimiter = new RateLimiter(36e5, 10, store);
|
|
5378
5621
|
return {
|
|
5379
5622
|
path: "/support/import-conversation",
|
|
5380
5623
|
method: "post",
|
|
@@ -5383,7 +5626,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5383
5626
|
const payload = req.payload;
|
|
5384
5627
|
const webhookSecret = req.headers.get("x-webhook-secret");
|
|
5385
5628
|
let isAuthed = false;
|
|
5386
|
-
if (webhookSecret
|
|
5629
|
+
if (verifySecret(webhookSecret, process.env.SUPPORT_WEBHOOK_SECRET)) {
|
|
5387
5630
|
isAuthed = true;
|
|
5388
5631
|
} else if (req.user && req.user.collection === slugs.users) {
|
|
5389
5632
|
isAuthed = true;
|
|
@@ -5392,7 +5635,7 @@ function createImportConversationEndpoint(slugs) {
|
|
|
5392
5635
|
return Response.json({ error: "Unauthorized" }, { status: 401 });
|
|
5393
5636
|
}
|
|
5394
5637
|
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
|
5395
|
-
if (importLimiter.check(ip)) {
|
|
5638
|
+
if (await importLimiter.check(ip, req)) {
|
|
5396
5639
|
return Response.json({ error: "Rate limit exceeded. Maximum 10 imports per hour." }, { status: 429 });
|
|
5397
5640
|
}
|
|
5398
5641
|
let body;
|
|
@@ -5584,7 +5827,7 @@ function createProcessScheduledEndpoint(slugs) {
|
|
|
5584
5827
|
handler: async (req) => {
|
|
5585
5828
|
const secret = req.headers.get("x-cron-secret");
|
|
5586
5829
|
const expectedSecret = process.env.CRON_SECRET;
|
|
5587
|
-
if (!
|
|
5830
|
+
if (!verifySecret(secret, expectedSecret)) {
|
|
5588
5831
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5589
5832
|
}
|
|
5590
5833
|
try {
|
|
@@ -5693,7 +5936,7 @@ function createProcessSnoozeEndpoint(slugs) {
|
|
|
5693
5936
|
handler: async (req) => {
|
|
5694
5937
|
const secret = req.headers.get("x-cron-secret");
|
|
5695
5938
|
const expectedSecret = process.env.CRON_SECRET;
|
|
5696
|
-
if (!
|
|
5939
|
+
if (!verifySecret(secret, expectedSecret)) {
|
|
5697
5940
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5698
5941
|
}
|
|
5699
5942
|
try {
|
|
@@ -5753,7 +5996,7 @@ function createProcessDigestsEndpoint(slugs) {
|
|
|
5753
5996
|
method: "post",
|
|
5754
5997
|
handler: async (req) => {
|
|
5755
5998
|
const secret = req.headers.get("x-cron-secret");
|
|
5756
|
-
if (!
|
|
5999
|
+
if (!verifySecret(secret, process.env.CRON_SECRET)) {
|
|
5757
6000
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5758
6001
|
}
|
|
5759
6002
|
try {
|
|
@@ -5826,7 +6069,7 @@ function createChannelsWebhookEndpoint(slugs) {
|
|
|
5826
6069
|
method: "post",
|
|
5827
6070
|
handler: async (req) => {
|
|
5828
6071
|
const secret = req.headers.get("x-channel-secret");
|
|
5829
|
-
if (!
|
|
6072
|
+
if (!verifySecret(secret, process.env.CHANNELS_WEBHOOK_SECRET)) {
|
|
5830
6073
|
return Response.json({ error: "Non autoris\xE9" }, { status: 401 });
|
|
5831
6074
|
}
|
|
5832
6075
|
let body;
|
|
@@ -6181,8 +6424,8 @@ function createTicketFeedbackEndpoint(slugs) {
|
|
|
6181
6424
|
// src/endpoints/transfer-ticket.ts
|
|
6182
6425
|
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6183
6426
|
var MAX_TRANSFERS_PER_DAY = 5;
|
|
6184
|
-
|
|
6185
|
-
|
|
6427
|
+
function createTransferTicketEndpoint(slugs, store) {
|
|
6428
|
+
const transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY, store);
|
|
6186
6429
|
return {
|
|
6187
6430
|
path: "/support/tickets/:id/transfer",
|
|
6188
6431
|
method: "post",
|
|
@@ -6217,7 +6460,7 @@ function createTransferTicketEndpoint(slugs) {
|
|
|
6217
6460
|
if (!isAdmin && !isOwner) {
|
|
6218
6461
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
6219
6462
|
}
|
|
6220
|
-
if (transferLimiter.check(`${req.user.id}:${ticketId}
|
|
6463
|
+
if (await transferLimiter.check(`${req.user.id}:${ticketId}`, req)) {
|
|
6221
6464
|
return Response.json(
|
|
6222
6465
|
{ error: `Limite atteinte (${MAX_TRANSFERS_PER_DAY} transferts par 24h sur ce ticket)` },
|
|
6223
6466
|
{ status: 429 }
|
|
@@ -6364,8 +6607,8 @@ function statusToLabel(status) {
|
|
|
6364
6607
|
}
|
|
6365
6608
|
var EMAIL_RE2 = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
6366
6609
|
var MAX_COLLABORATORS_PER_TICKET = 20;
|
|
6367
|
-
|
|
6368
|
-
|
|
6610
|
+
function createInviteCollaboratorEndpoint(slugs, store) {
|
|
6611
|
+
const inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10, store);
|
|
6369
6612
|
return {
|
|
6370
6613
|
path: "/support/tickets/:id/invite",
|
|
6371
6614
|
method: "post",
|
|
@@ -6400,7 +6643,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
6400
6643
|
if (!isAdmin && !isOwner) {
|
|
6401
6644
|
return Response.json({ error: "Forbidden" }, { status: 403 });
|
|
6402
6645
|
}
|
|
6403
|
-
if (inviteLimiter.check(String(req.user.id))) {
|
|
6646
|
+
if (await inviteLimiter.check(String(req.user.id), req)) {
|
|
6404
6647
|
return Response.json({ error: "Trop d'invitations. R\xE9essayez plus tard." }, { status: 429 });
|
|
6405
6648
|
}
|
|
6406
6649
|
const collabCount = await dbCount(payload, "ticket-collaborators", { where: { ticket: { equals: ticketId } }, overrideAccess: true }).catch(() => ({ totalDocs: 0 }));
|
|
@@ -6531,6 +6774,7 @@ function createInviteCollaboratorEndpoint(slugs) {
|
|
|
6531
6774
|
// src/endpoints/index.ts
|
|
6532
6775
|
function createSupportEndpoints(slugs, options) {
|
|
6533
6776
|
const f = options?.features;
|
|
6777
|
+
const rateLimitStore = options?.rateLimitStore;
|
|
6534
6778
|
const endpoints = [
|
|
6535
6779
|
createSearchEndpoint(slugs),
|
|
6536
6780
|
createKbSearchEndpoint(slugs),
|
|
@@ -6540,26 +6784,26 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6540
6784
|
createExportCsvEndpoint(slugs),
|
|
6541
6785
|
createExportDataEndpoint(slugs),
|
|
6542
6786
|
createSeedKbEndpoint(slugs),
|
|
6543
|
-
createLoginEndpoint(slugs),
|
|
6544
|
-
createAuth2faEndpoint(slugs),
|
|
6787
|
+
createLoginEndpoint(slugs, rateLimitStore),
|
|
6788
|
+
createAuth2faEndpoint(slugs, rateLimitStore),
|
|
6545
6789
|
createOAuthGoogleEndpoint(slugs, options?.oauth),
|
|
6546
6790
|
createDeleteAccountEndpoint(slugs),
|
|
6547
6791
|
createMergeClientsEndpoint(slugs),
|
|
6548
|
-
createImportConversationEndpoint(slugs),
|
|
6792
|
+
createImportConversationEndpoint(slugs, rateLimitStore),
|
|
6549
6793
|
createPurgeLogsEndpoint(slugs),
|
|
6550
|
-
createResendNotificationEndpoint(slugs),
|
|
6794
|
+
createResendNotificationEndpoint(slugs, rateLimitStore),
|
|
6551
6795
|
createUserPrefsGetEndpoint(slugs),
|
|
6552
6796
|
createUserPrefsPostEndpoint(slugs),
|
|
6553
6797
|
createEscalateEndpoint(slugs),
|
|
6554
6798
|
createTicketFeedbackEndpoint(slugs),
|
|
6555
|
-
createTransferTicketEndpoint(slugs),
|
|
6556
|
-
createInviteCollaboratorEndpoint(slugs)
|
|
6799
|
+
createTransferTicketEndpoint(slugs, rateLimitStore),
|
|
6800
|
+
createInviteCollaboratorEndpoint(slugs, rateLimitStore)
|
|
6557
6801
|
];
|
|
6558
6802
|
if (!f || f.ai !== false) {
|
|
6559
|
-
endpoints.push(createAiEndpoint(slugs));
|
|
6560
|
-
endpoints.push(createAiAgentEndpoint(slugs));
|
|
6561
|
-
endpoints.push(...createClientIntelligenceEndpoint(slugs));
|
|
6562
|
-
endpoints.push(createTicketSynthesisEndpoint(slugs));
|
|
6803
|
+
endpoints.push(createAiEndpoint(slugs, rateLimitStore));
|
|
6804
|
+
endpoints.push(createAiAgentEndpoint(slugs, rateLimitStore));
|
|
6805
|
+
endpoints.push(...createClientIntelligenceEndpoint(slugs, rateLimitStore));
|
|
6806
|
+
endpoints.push(createTicketSynthesisEndpoint(slugs, options?.capabilities?.aiSummaries, rateLimitStore));
|
|
6563
6807
|
}
|
|
6564
6808
|
if (!f || f.bulkActions !== false) endpoints.push(createBulkActionEndpoint(slugs));
|
|
6565
6809
|
if (!f || f.merge !== false) endpoints.push(createMergeTicketsEndpoint(slugs));
|
|
@@ -6574,18 +6818,18 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6574
6818
|
if (!f || f.sla !== false) endpoints.push(createSlaCheckEndpoint(slugs));
|
|
6575
6819
|
if (!f || f.autoClose !== false) {
|
|
6576
6820
|
endpoints.push(createAutoCloseEndpoint(slugs));
|
|
6577
|
-
endpoints.push(createSendReminderEndpoint(slugs));
|
|
6821
|
+
endpoints.push(createSendReminderEndpoint(slugs, rateLimitStore));
|
|
6578
6822
|
}
|
|
6579
6823
|
if (!f || f.customStatuses !== false) endpoints.push(createStatusesEndpoint(slugs));
|
|
6580
6824
|
if (!f || f.macros !== false) endpoints.push(createApplyMacroEndpoint(slugs));
|
|
6581
6825
|
if (!f || f.roundRobin !== false) {
|
|
6582
6826
|
endpoints.push(createRoundRobinConfigGetEndpoint(slugs), createRoundRobinConfigPostEndpoint(slugs));
|
|
6583
6827
|
}
|
|
6584
|
-
if (!f || f.chatbot !== false) endpoints.push(createChatbotEndpoint(slugs));
|
|
6828
|
+
if (!f || f.chatbot !== false) endpoints.push(createChatbotEndpoint(slugs, rateLimitStore));
|
|
6585
6829
|
if (!f || f.chat !== false) {
|
|
6586
|
-
endpoints.push(createChatGetEndpoint(slugs), createChatPostEndpoint(slugs));
|
|
6830
|
+
endpoints.push(createChatGetEndpoint(slugs), createChatPostEndpoint(slugs, rateLimitStore));
|
|
6587
6831
|
endpoints.push(createChatStreamEndpoint(slugs));
|
|
6588
|
-
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs));
|
|
6832
|
+
endpoints.push(createAdminChatGetEndpoint(slugs), createAdminChatPostEndpoint(slugs, rateLimitStore));
|
|
6589
6833
|
endpoints.push(createAdminChatStreamEndpoint(slugs));
|
|
6590
6834
|
}
|
|
6591
6835
|
if (!f || f.timeTracking !== false) {
|
|
@@ -6603,6 +6847,16 @@ function createSupportEndpoints(slugs, options) {
|
|
|
6603
6847
|
endpoints.push(createChannelsWebhookEndpoint(slugs));
|
|
6604
6848
|
endpoints.push(createVapidKeyEndpoint());
|
|
6605
6849
|
endpoints.push(createPushSubscribeEndpoint(slugs));
|
|
6850
|
+
if (options?.capabilities?.inboundEmail) {
|
|
6851
|
+
endpoints.push(createInboundEmailEndpoint(options.capabilities.inboundEmail, rateLimitStore));
|
|
6852
|
+
}
|
|
6853
|
+
if (options?.capabilities?.projectSuggestions) {
|
|
6854
|
+
endpoints.push(createProjectSuggestionsEndpoint(slugs, options.capabilities.projectSuggestions, rateLimitStore));
|
|
6855
|
+
}
|
|
6856
|
+
if (options?.capabilities?.aiTitles) {
|
|
6857
|
+
endpoints.push(createTicketTitleEndpoint(slugs, options.capabilities.aiTitles, rateLimitStore));
|
|
6858
|
+
endpoints.push(createGenerateMissingTitlesEndpoint(slugs, options.capabilities.aiTitles, rateLimitStore));
|
|
6859
|
+
}
|
|
6606
6860
|
return endpoints;
|
|
6607
6861
|
}
|
|
6608
6862
|
|
|
@@ -7239,38 +7493,69 @@ async function resolveAgentTeamIds(payload, slugs, userId) {
|
|
|
7239
7493
|
}
|
|
7240
7494
|
|
|
7241
7495
|
// src/collections/Tickets.ts
|
|
7242
|
-
|
|
7496
|
+
var ticketNumberQueues = /* @__PURE__ */ new Map();
|
|
7497
|
+
async function withTicketNumberLock(key, operation) {
|
|
7498
|
+
const previous = ticketNumberQueues.get(key) ?? Promise.resolve();
|
|
7499
|
+
let release;
|
|
7500
|
+
const gate = new Promise((resolve) => {
|
|
7501
|
+
release = resolve;
|
|
7502
|
+
});
|
|
7503
|
+
const queued = previous.then(() => gate);
|
|
7504
|
+
ticketNumberQueues.set(key, queued);
|
|
7505
|
+
await previous;
|
|
7506
|
+
try {
|
|
7507
|
+
return await operation();
|
|
7508
|
+
} finally {
|
|
7509
|
+
release();
|
|
7510
|
+
if (ticketNumberQueues.get(key) === queued) ticketNumberQueues.delete(key);
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
function createAssignTicketNumber(slugs, options) {
|
|
7243
7514
|
return async ({ data, operation, req }) => {
|
|
7244
7515
|
if (operation === "create") {
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
|
|
7250
|
-
|
|
7251
|
-
|
|
7252
|
-
|
|
7253
|
-
|
|
7254
|
-
|
|
7255
|
-
|
|
7256
|
-
|
|
7257
|
-
|
|
7258
|
-
|
|
7259
|
-
|
|
7516
|
+
const value = await withTicketNumberLock(slugs.tickets, async () => {
|
|
7517
|
+
const counterResult = await req.payload.find({
|
|
7518
|
+
collection: slugs.counters,
|
|
7519
|
+
where: { key: { equals: slugs.tickets } },
|
|
7520
|
+
limit: 1,
|
|
7521
|
+
depth: 0,
|
|
7522
|
+
overrideAccess: true,
|
|
7523
|
+
req
|
|
7524
|
+
});
|
|
7525
|
+
const counter = counterResult.docs[0];
|
|
7526
|
+
if (counter) {
|
|
7527
|
+
const allocated2 = Number(counter.nextValue);
|
|
7528
|
+
await req.payload.update({
|
|
7529
|
+
collection: slugs.counters,
|
|
7530
|
+
id: counter.id,
|
|
7531
|
+
data: { nextValue: allocated2 + 1 },
|
|
7532
|
+
overrideAccess: true,
|
|
7533
|
+
req
|
|
7260
7534
|
});
|
|
7261
|
-
|
|
7262
|
-
const suffix = Date.now() % 1e4;
|
|
7263
|
-
data.ticketNumber = `TK-${String(baseNumber + suffix).padStart(4, "0")}`;
|
|
7264
|
-
}
|
|
7265
|
-
break;
|
|
7266
|
-
} catch (error) {
|
|
7267
|
-
if (retries > 1 && (error?.message?.includes("UNIQUE") || error?.message?.includes("unique") || error?.code === "SQLITE_CONSTRAINT")) {
|
|
7268
|
-
retries--;
|
|
7269
|
-
continue;
|
|
7270
|
-
}
|
|
7271
|
-
throw error;
|
|
7535
|
+
return allocated2;
|
|
7272
7536
|
}
|
|
7273
|
-
|
|
7537
|
+
const tickets = await req.payload.find({
|
|
7538
|
+
collection: slugs.tickets,
|
|
7539
|
+
limit: 0,
|
|
7540
|
+
depth: 0,
|
|
7541
|
+
overrideAccess: true,
|
|
7542
|
+
select: { ticketNumber: true },
|
|
7543
|
+
req
|
|
7544
|
+
});
|
|
7545
|
+
const highest = tickets.docs.reduce((max, ticket) => {
|
|
7546
|
+
const match = String(ticket.ticketNumber || "").match(/(\d+)$/);
|
|
7547
|
+
return match ? Math.max(max, Number(match[1])) : max;
|
|
7548
|
+
}, 0);
|
|
7549
|
+
const allocated = highest + 1;
|
|
7550
|
+
await req.payload.create({
|
|
7551
|
+
collection: slugs.counters,
|
|
7552
|
+
data: { key: slugs.tickets, nextValue: allocated + 1 },
|
|
7553
|
+
overrideAccess: true,
|
|
7554
|
+
req
|
|
7555
|
+
});
|
|
7556
|
+
return allocated;
|
|
7557
|
+
});
|
|
7558
|
+
data.ticketNumber = `${options?.prefix ?? "TK-"}${String(value).padStart(options?.padding ?? 4, "0")}`;
|
|
7274
7559
|
}
|
|
7275
7560
|
return data;
|
|
7276
7561
|
};
|
|
@@ -7363,7 +7648,7 @@ function createTrackSLA(slugs) {
|
|
|
7363
7648
|
return doc;
|
|
7364
7649
|
};
|
|
7365
7650
|
}
|
|
7366
|
-
function createTrackAiSummaryOnResolve(slugs) {
|
|
7651
|
+
function createTrackAiSummaryOnResolve(slugs, generator) {
|
|
7367
7652
|
return async ({ doc, previousDoc, operation, req }) => {
|
|
7368
7653
|
if (operation !== "update" || !previousDoc) return doc;
|
|
7369
7654
|
const wasResolved = previousDoc.status === "resolved";
|
|
@@ -7383,7 +7668,33 @@ function createTrackAiSummaryOnResolve(slugs) {
|
|
|
7383
7668
|
}
|
|
7384
7669
|
if (!wasResolved && isResolved && !doc.aiSummary) {
|
|
7385
7670
|
setImmediate(() => {
|
|
7386
|
-
generateTicketSynthesis({ payload: req.payload, slugs, ticketId: doc.id })
|
|
7671
|
+
const operation2 = generator ? generator.generate(req.payload, doc.id) : generateTicketSynthesis({ payload: req.payload, slugs, ticketId: doc.id });
|
|
7672
|
+
operation2.catch((err) => console.error("[support] Background ai synthesis failed:", err));
|
|
7673
|
+
});
|
|
7674
|
+
}
|
|
7675
|
+
return doc;
|
|
7676
|
+
};
|
|
7677
|
+
}
|
|
7678
|
+
function createTrackWaitingSince() {
|
|
7679
|
+
return ({ data, originalDoc, operation }) => {
|
|
7680
|
+
if (operation !== "update") return data;
|
|
7681
|
+
const previous = originalDoc?.status;
|
|
7682
|
+
const next = data.status ?? previous;
|
|
7683
|
+
if (next === "waiting_client" && previous !== "waiting_client") {
|
|
7684
|
+
data.waitingSince = (/* @__PURE__ */ new Date()).toISOString();
|
|
7685
|
+
data.autoCloseRemindedAt = null;
|
|
7686
|
+
} else if (next !== "waiting_client" && previous === "waiting_client") {
|
|
7687
|
+
data.waitingSince = null;
|
|
7688
|
+
data.autoCloseRemindedAt = null;
|
|
7689
|
+
}
|
|
7690
|
+
return data;
|
|
7691
|
+
};
|
|
7692
|
+
}
|
|
7693
|
+
function createGenerateTitleOnCreate(generator) {
|
|
7694
|
+
return ({ doc, operation, req }) => {
|
|
7695
|
+
if (operation === "create" && doc?.id && !doc.displayTitle) {
|
|
7696
|
+
setImmediate(() => {
|
|
7697
|
+
generator.generate(req.payload, doc.id).catch((err) => console.error("[support] Background title generation failed:", err));
|
|
7387
7698
|
});
|
|
7388
7699
|
}
|
|
7389
7700
|
return doc;
|
|
@@ -7601,6 +7912,7 @@ function createCascadeDelete(slugs) {
|
|
|
7601
7912
|
function createTicketsCollection(slugs, options) {
|
|
7602
7913
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
7603
7914
|
const dynamicFields = [];
|
|
7915
|
+
const capabilities = options?.capabilities;
|
|
7604
7916
|
dynamicFields.push({
|
|
7605
7917
|
name: "conversation",
|
|
7606
7918
|
type: "ui",
|
|
@@ -7619,6 +7931,28 @@ function createTicketsCollection(slugs, options) {
|
|
|
7619
7931
|
admin: { position: "sidebar" }
|
|
7620
7932
|
});
|
|
7621
7933
|
}
|
|
7934
|
+
if (capabilities?.aiTitles) {
|
|
7935
|
+
dynamicFields.push(
|
|
7936
|
+
{
|
|
7937
|
+
name: "displayTitle",
|
|
7938
|
+
type: "text",
|
|
7939
|
+
label: "Titre d'affichage",
|
|
7940
|
+
admin: { description: "Titre court sugg\xE9r\xE9 par l'IA, \xE9ditable sans modifier le sujet email." }
|
|
7941
|
+
},
|
|
7942
|
+
{
|
|
7943
|
+
name: "displayTitleStatus",
|
|
7944
|
+
type: "select",
|
|
7945
|
+
defaultValue: "none",
|
|
7946
|
+
options: [
|
|
7947
|
+
{ label: "Aucun", value: "none" },
|
|
7948
|
+
{ label: "G\xE9n\xE9ration en cours", value: "pending" },
|
|
7949
|
+
{ label: "Sugg\xE9r\xE9", value: "suggested" },
|
|
7950
|
+
{ label: "Valid\xE9", value: "validated" },
|
|
7951
|
+
{ label: "\xC9chec", value: "error" }
|
|
7952
|
+
]
|
|
7953
|
+
}
|
|
7954
|
+
);
|
|
7955
|
+
}
|
|
7622
7956
|
const billingFields = [
|
|
7623
7957
|
{
|
|
7624
7958
|
type: "row",
|
|
@@ -7697,6 +8031,39 @@ function createTicketsCollection(slugs, options) {
|
|
|
7697
8031
|
]
|
|
7698
8032
|
}
|
|
7699
8033
|
];
|
|
8034
|
+
if (capabilities?.detailedBilling) {
|
|
8035
|
+
billingFields.push(
|
|
8036
|
+
{
|
|
8037
|
+
name: "billedAt",
|
|
8038
|
+
type: "date",
|
|
8039
|
+
label: "Factur\xE9 le"
|
|
8040
|
+
},
|
|
8041
|
+
{
|
|
8042
|
+
name: "billingLines",
|
|
8043
|
+
type: "array",
|
|
8044
|
+
label: "Lignes de facturation",
|
|
8045
|
+
fields: [
|
|
8046
|
+
{ name: "period", type: "text", label: "P\xE9riode (AAAA-MM)" },
|
|
8047
|
+
{ name: "label", type: "text", label: "Libell\xE9" },
|
|
8048
|
+
{ name: "amount", type: "number", label: "Montant (\u20AC)" },
|
|
8049
|
+
{ name: "billingType", type: "select", options: ["hourly", "flat"], defaultValue: "flat", label: "Type" },
|
|
8050
|
+
{ name: "billed", type: "checkbox", defaultValue: false, label: "Factur\xE9" },
|
|
8051
|
+
{ name: "billedAt", type: "date", label: "Factur\xE9 le" }
|
|
8052
|
+
]
|
|
8053
|
+
}
|
|
8054
|
+
);
|
|
8055
|
+
}
|
|
8056
|
+
if (capabilities?.volunteering) {
|
|
8057
|
+
billingFields.push(
|
|
8058
|
+
{ name: "volunteer", type: "checkbox", defaultValue: false, label: "B\xE9n\xE9volat (non factur\xE9)" },
|
|
8059
|
+
{
|
|
8060
|
+
name: "volunteerValue",
|
|
8061
|
+
type: "number",
|
|
8062
|
+
label: "Valeur offerte (\u20AC)",
|
|
8063
|
+
admin: { condition: (data) => Boolean(data?.volunteer) }
|
|
8064
|
+
}
|
|
8065
|
+
);
|
|
8066
|
+
}
|
|
7700
8067
|
return {
|
|
7701
8068
|
slug: slugs.tickets,
|
|
7702
8069
|
labels: { singular: "Ticket", plural: "Tickets" },
|
|
@@ -7847,7 +8214,20 @@ function createTicketsCollection(slugs, options) {
|
|
|
7847
8214
|
},
|
|
7848
8215
|
{ name: "mergedInto", type: "relationship", relationTo: slugs.tickets, label: "Fusionne dans", admin: { readOnly: true } },
|
|
7849
8216
|
{ name: "autoCloseRemindedAt", type: "date", label: "Rappel auto-close envoye", admin: { readOnly: true, date: { displayFormat: "dd/MM/yyyy HH:mm" } } },
|
|
7850
|
-
{ 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." } }
|
|
8217
|
+
{ 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." } },
|
|
8218
|
+
{ name: "waitingSince", type: "date", index: true, label: "En attente depuis", admin: { readOnly: true } },
|
|
8219
|
+
{
|
|
8220
|
+
name: "relanceDelayDays",
|
|
8221
|
+
type: "select",
|
|
8222
|
+
defaultValue: "2",
|
|
8223
|
+
label: "D\xE9lai de relance client",
|
|
8224
|
+
options: [
|
|
8225
|
+
{ label: "1 jour", value: "1" },
|
|
8226
|
+
{ label: "2 jours", value: "2" },
|
|
8227
|
+
{ label: "3 jours", value: "3" }
|
|
8228
|
+
]
|
|
8229
|
+
},
|
|
8230
|
+
{ name: "satisfactionRemindedAt", type: "date", admin: { hidden: true, readOnly: true } }
|
|
7851
8231
|
]
|
|
7852
8232
|
},
|
|
7853
8233
|
// Sidebar
|
|
@@ -7922,15 +8302,17 @@ function createTicketsCollection(slugs, options) {
|
|
|
7922
8302
|
],
|
|
7923
8303
|
hooks: {
|
|
7924
8304
|
beforeChange: [
|
|
7925
|
-
createAssignTicketNumber(slugs),
|
|
8305
|
+
createAssignTicketNumber(slugs, options?.ticketNumber),
|
|
7926
8306
|
createAssignClientOnCreate(slugs),
|
|
7927
8307
|
createAutoAssignAdmin(slugs),
|
|
7928
8308
|
autoPaidAt,
|
|
8309
|
+
createTrackWaitingSince(),
|
|
7929
8310
|
createRestrictClientUpdates(slugs)
|
|
7930
8311
|
],
|
|
7931
8312
|
afterChange: [
|
|
7932
8313
|
createTrackSLA(slugs),
|
|
7933
|
-
createTrackAiSummaryOnResolve(slugs),
|
|
8314
|
+
createTrackAiSummaryOnResolve(slugs, capabilities?.aiSummaries),
|
|
8315
|
+
...capabilities?.aiTitles ? [createGenerateTitleOnCreate(capabilities.aiTitles)] : [],
|
|
7934
8316
|
createAssignSlaDeadlines(slugs, notificationSlug),
|
|
7935
8317
|
createPauseSlaOnHold(slugs),
|
|
7936
8318
|
createCheckSlaOnResolve(slugs, notificationSlug),
|
|
@@ -8435,6 +8817,56 @@ function createNotifyMentions(slugs) {
|
|
|
8435
8817
|
return doc;
|
|
8436
8818
|
};
|
|
8437
8819
|
}
|
|
8820
|
+
function createSmsNotification(slugs, capability) {
|
|
8821
|
+
return async ({ doc, operation, req }) => {
|
|
8822
|
+
try {
|
|
8823
|
+
if (operation !== "create" || !doc.notifyBySms) return doc;
|
|
8824
|
+
if (doc.authorType !== "admin" || doc.isInternal) return doc;
|
|
8825
|
+
if (doc.scheduledAt && !doc.scheduledSent) return doc;
|
|
8826
|
+
if (capability.adapter.isConfigured && !capability.adapter.isConfigured()) return doc;
|
|
8827
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8828
|
+
const ticket = await req.payload.findByID({
|
|
8829
|
+
collection: slugs.tickets,
|
|
8830
|
+
id: ticketId,
|
|
8831
|
+
depth: 1,
|
|
8832
|
+
overrideAccess: true
|
|
8833
|
+
});
|
|
8834
|
+
const client = typeof ticket.client === "object" && ticket.client ? ticket.client : null;
|
|
8835
|
+
if (!client) return doc;
|
|
8836
|
+
const target = typeof doc.smsTo === "string" && doc.smsTo.trim() || String(client.phone || "");
|
|
8837
|
+
if (!target) return doc;
|
|
8838
|
+
const fallback = `ConsilioWEB : nouvelle r\xE9ponse au ticket ${String(ticket.ticketNumber || "")}.`;
|
|
8839
|
+
const message = typeof doc.smsMessage === "string" && doc.smsMessage.trim() || (capability.buildMessage ? await capability.buildMessage({ client, ticket }) : fallback);
|
|
8840
|
+
const result = await capability.adapter.send({
|
|
8841
|
+
message,
|
|
8842
|
+
to: target,
|
|
8843
|
+
payload: req.payload,
|
|
8844
|
+
ticket,
|
|
8845
|
+
client
|
|
8846
|
+
});
|
|
8847
|
+
if (!result.sent && !result.skipped) {
|
|
8848
|
+
req.payload.logger?.warn(`[support:sms] Delivery failed: ${result.error || "unknown error"}`);
|
|
8849
|
+
}
|
|
8850
|
+
} catch (error) {
|
|
8851
|
+
req.payload.logger?.warn(`[support:sms] Adapter error: ${error instanceof Error ? error.message : String(error)}`);
|
|
8852
|
+
}
|
|
8853
|
+
return doc;
|
|
8854
|
+
};
|
|
8855
|
+
}
|
|
8856
|
+
function createThreadCleanup(capability) {
|
|
8857
|
+
return async ({ doc, operation, req }) => {
|
|
8858
|
+
if (operation !== "create" || doc.isInternal) return doc;
|
|
8859
|
+
if (doc.authorType !== "client" && doc.authorType !== "email") return doc;
|
|
8860
|
+
const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
|
|
8861
|
+
if (!ticketId) return doc;
|
|
8862
|
+
try {
|
|
8863
|
+
await capability.clean(req.payload, ticketId, doc.id);
|
|
8864
|
+
} catch (error) {
|
|
8865
|
+
req.payload.logger?.warn(`[support:thread-cleanup] ${error instanceof Error ? error.message : String(error)}`);
|
|
8866
|
+
}
|
|
8867
|
+
return doc;
|
|
8868
|
+
};
|
|
8869
|
+
}
|
|
8438
8870
|
function createTicketMessagesCollection(slugs, options) {
|
|
8439
8871
|
const notificationSlug = options?.notificationSlug || "admin-notifications";
|
|
8440
8872
|
return {
|
|
@@ -8495,11 +8927,23 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8495
8927
|
{ name: "deletedAt", type: "date", label: "Supprime le", admin: { hidden: true } },
|
|
8496
8928
|
{ name: "emailSentAt", type: "date", label: "Email envoye le", admin: { hidden: true } },
|
|
8497
8929
|
{ name: "emailSentTo", type: "text", label: "Email envoye a", admin: { hidden: true } },
|
|
8498
|
-
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } }
|
|
8930
|
+
{ name: "emailOpenedAt", type: "date", label: "Email ouvert le", admin: { hidden: true } },
|
|
8931
|
+
...options?.capabilities?.sms ? [
|
|
8932
|
+
{ name: "notifyBySms", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
8933
|
+
{ name: "smsTo", type: "text", admin: { hidden: true } },
|
|
8934
|
+
{ name: "smsMessage", type: "textarea", admin: { hidden: true } }
|
|
8935
|
+
] : [],
|
|
8936
|
+
...options?.capabilities?.threadCleanup ? [
|
|
8937
|
+
{ name: "isMarkedAsNoise", type: "checkbox", defaultValue: false, admin: { hidden: true } },
|
|
8938
|
+
{ name: "noiseReason", type: "text", admin: { hidden: true } },
|
|
8939
|
+
{ name: "emailContext", type: "json", admin: { hidden: true } },
|
|
8940
|
+
{ name: "emailBodyOriginal", type: "textarea", admin: { hidden: true } }
|
|
8941
|
+
] : []
|
|
8499
8942
|
],
|
|
8500
8943
|
hooks: {
|
|
8501
8944
|
beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
|
|
8502
8945
|
afterChange: [
|
|
8946
|
+
...options?.capabilities?.sms ? [createSmsNotification(slugs, options.capabilities.sms)] : [],
|
|
8503
8947
|
createAutoUpdateStatus(slugs),
|
|
8504
8948
|
createNotifyClient(slugs),
|
|
8505
8949
|
createTrackFirstResponse(slugs),
|
|
@@ -8508,7 +8952,8 @@ function createTicketMessagesCollection(slugs, options) {
|
|
|
8508
8952
|
createNotifyAdminOnClientMessage(slugs, notificationSlug),
|
|
8509
8953
|
createFireMessageWebhooks(slugs),
|
|
8510
8954
|
createDispatchWebhookOnReply(slugs),
|
|
8511
|
-
createNotifyMentions(slugs)
|
|
8955
|
+
createNotifyMentions(slugs),
|
|
8956
|
+
...options?.capabilities?.threadCleanup ? [createThreadCleanup(options.capabilities.threadCleanup)] : []
|
|
8512
8957
|
]
|
|
8513
8958
|
},
|
|
8514
8959
|
access: {
|
|
@@ -8598,7 +9043,7 @@ function createEnforce2FA(slugs) {
|
|
|
8598
9043
|
return user;
|
|
8599
9044
|
};
|
|
8600
9045
|
}
|
|
8601
|
-
function createSupportClientsCollection(slugs) {
|
|
9046
|
+
function createSupportClientsCollection(slugs, options) {
|
|
8602
9047
|
return {
|
|
8603
9048
|
slug: slugs.supportClients,
|
|
8604
9049
|
labels: {
|
|
@@ -8860,7 +9305,45 @@ function createSupportClientsCollection(slugs) {
|
|
|
8860
9305
|
description: "Visible uniquement par les admins",
|
|
8861
9306
|
position: "sidebar"
|
|
8862
9307
|
}
|
|
8863
|
-
}
|
|
9308
|
+
},
|
|
9309
|
+
...options?.capabilities?.sms ? [
|
|
9310
|
+
{
|
|
9311
|
+
name: "notifyBySmsChannel",
|
|
9312
|
+
type: "checkbox",
|
|
9313
|
+
defaultValue: false,
|
|
9314
|
+
label: "Canal SMS activ\xE9",
|
|
9315
|
+
admin: { position: "sidebar" }
|
|
9316
|
+
}
|
|
9317
|
+
] : [],
|
|
9318
|
+
...options?.capabilities?.aiTitles || options?.capabilities?.aiSummaries ? [
|
|
9319
|
+
{
|
|
9320
|
+
name: "preferredFormality",
|
|
9321
|
+
type: "select",
|
|
9322
|
+
defaultValue: "auto",
|
|
9323
|
+
label: "Formule IA",
|
|
9324
|
+
options: [
|
|
9325
|
+
{ label: "Auto", value: "auto" },
|
|
9326
|
+
{ label: "Tutoiement", value: "tutoyer" },
|
|
9327
|
+
{ label: "Vouvoiement", value: "vouvoyer" }
|
|
9328
|
+
],
|
|
9329
|
+
admin: { position: "sidebar" }
|
|
9330
|
+
},
|
|
9331
|
+
{
|
|
9332
|
+
name: "preferredTone",
|
|
9333
|
+
type: "select",
|
|
9334
|
+
defaultValue: "auto",
|
|
9335
|
+
label: "Ton IA",
|
|
9336
|
+
options: [
|
|
9337
|
+
{ label: "Auto", value: "auto" },
|
|
9338
|
+
{ label: "Neutre / professionnel", value: "neutre" },
|
|
9339
|
+
{ label: "Amical", value: "amical" },
|
|
9340
|
+
{ label: "Direct / concis", value: "direct" },
|
|
9341
|
+
{ label: "Formel", value: "formel" }
|
|
9342
|
+
],
|
|
9343
|
+
admin: { position: "sidebar" }
|
|
9344
|
+
},
|
|
9345
|
+
{ name: "lastRewriteStyle", type: "text", admin: { hidden: true } }
|
|
9346
|
+
] : []
|
|
8864
9347
|
],
|
|
8865
9348
|
hooks: {
|
|
8866
9349
|
beforeLogin: [createEnforce2FA(slugs)],
|
|
@@ -9475,7 +9958,7 @@ function createPendingEmailsCollection(slugs) {
|
|
|
9475
9958
|
create: ({ req }) => {
|
|
9476
9959
|
if (req.user?.collection === slugs.users) return true;
|
|
9477
9960
|
const webhookSecret = req.headers.get("x-webhook-secret");
|
|
9478
|
-
if (webhookSecret
|
|
9961
|
+
if (verifySecret(webhookSecret, process.env.SUPPORT_WEBHOOK_SECRET)) return true;
|
|
9479
9962
|
return false;
|
|
9480
9963
|
}
|
|
9481
9964
|
},
|
|
@@ -10608,6 +11091,40 @@ function createPushSubscriptionCollection(slugs) {
|
|
|
10608
11091
|
};
|
|
10609
11092
|
}
|
|
10610
11093
|
|
|
11094
|
+
// src/collections/SupportRateLimits.ts
|
|
11095
|
+
function createSupportRateLimitsCollection(slug = "support-rate-limits") {
|
|
11096
|
+
return {
|
|
11097
|
+
slug,
|
|
11098
|
+
admin: { hidden: true },
|
|
11099
|
+
access: {
|
|
11100
|
+
create: () => false,
|
|
11101
|
+
read: () => false,
|
|
11102
|
+
update: () => false,
|
|
11103
|
+
delete: () => false
|
|
11104
|
+
},
|
|
11105
|
+
fields: [
|
|
11106
|
+
{ name: "key", type: "text", required: true, unique: true, index: true },
|
|
11107
|
+
{ name: "count", type: "number", required: true, min: 1 },
|
|
11108
|
+
{ name: "resetAt", type: "date", required: true, index: true }
|
|
11109
|
+
],
|
|
11110
|
+
timestamps: false
|
|
11111
|
+
};
|
|
11112
|
+
}
|
|
11113
|
+
|
|
11114
|
+
// src/collections/SupportCounters.ts
|
|
11115
|
+
function createSupportCountersCollection(slug = "support-counters") {
|
|
11116
|
+
return {
|
|
11117
|
+
slug,
|
|
11118
|
+
admin: { hidden: true },
|
|
11119
|
+
access: { create: () => false, read: () => false, update: () => false, delete: () => false },
|
|
11120
|
+
fields: [
|
|
11121
|
+
{ name: "key", type: "text", required: true, unique: true, index: true },
|
|
11122
|
+
{ name: "nextValue", type: "number", required: true, min: 1 }
|
|
11123
|
+
],
|
|
11124
|
+
timestamps: false
|
|
11125
|
+
};
|
|
11126
|
+
}
|
|
11127
|
+
|
|
10611
11128
|
// src/plugin.ts
|
|
10612
11129
|
function viewConfig(component, path) {
|
|
10613
11130
|
return { Component: component, path };
|
|
@@ -10623,21 +11140,25 @@ function supportPlugin(config) {
|
|
|
10623
11140
|
...config?.collectionSlugs,
|
|
10624
11141
|
users: config?.userCollectionSlug || "users"
|
|
10625
11142
|
});
|
|
11143
|
+
const rateLimitStore = config?.rateLimitStore === "payload" ? new PayloadRateLimitStore(slugs.rateLimits) : config?.rateLimitStore;
|
|
10626
11144
|
return (incomingConfig) => {
|
|
10627
11145
|
const existingCollections = incomingConfig.collections || [];
|
|
10628
11146
|
const ticketOptions = {
|
|
10629
11147
|
conversationComponent: config?.conversationComponent,
|
|
10630
11148
|
projectCollectionSlug: config?.projectCollectionSlug,
|
|
10631
11149
|
documentsCollectionSlug: config?.documentsCollectionSlug,
|
|
10632
|
-
notificationSlug: config?.notificationSlug
|
|
11150
|
+
notificationSlug: config?.notificationSlug,
|
|
11151
|
+
ticketNumber: config?.ticketNumber,
|
|
11152
|
+
capabilities: config?.capabilities
|
|
10633
11153
|
};
|
|
10634
11154
|
const messageOptions = {
|
|
10635
|
-
notificationSlug: config?.notificationSlug
|
|
11155
|
+
notificationSlug: config?.notificationSlug,
|
|
11156
|
+
capabilities: config?.capabilities
|
|
10636
11157
|
};
|
|
10637
11158
|
const supportCollections = [
|
|
10638
11159
|
createTicketsCollection(slugs, ticketOptions),
|
|
10639
11160
|
createTicketMessagesCollection(slugs, messageOptions),
|
|
10640
|
-
createSupportClientsCollection(slugs),
|
|
11161
|
+
createSupportClientsCollection(slugs, { capabilities: config?.capabilities }),
|
|
10641
11162
|
createCannedResponsesCollection(slugs),
|
|
10642
11163
|
createTicketActivityLogCollection(slugs),
|
|
10643
11164
|
createSatisfactionSurveysCollection(slugs),
|
|
@@ -10647,8 +11168,12 @@ function supportPlugin(config) {
|
|
|
10647
11168
|
createNotificationQueueCollection(slugs),
|
|
10648
11169
|
createAutomationRulesCollection(slugs),
|
|
10649
11170
|
createSupportTeamCollection(slugs),
|
|
10650
|
-
createPushSubscriptionCollection(slugs)
|
|
11171
|
+
createPushSubscriptionCollection(slugs),
|
|
11172
|
+
createSupportCountersCollection(slugs.counters)
|
|
10651
11173
|
];
|
|
11174
|
+
if (config?.rateLimitStore === "payload") {
|
|
11175
|
+
supportCollections.push(createSupportRateLimitsCollection(slugs.rateLimits));
|
|
11176
|
+
}
|
|
10652
11177
|
if (features.authLogs !== false) supportCollections.push(createAuthLogsCollection(slugs));
|
|
10653
11178
|
if (features.timeTracking !== false) supportCollections.push(createTimeEntriesCollection(slugs));
|
|
10654
11179
|
if (features.emailTracking !== false) supportCollections.push(createEmailLogsCollection(slugs));
|
|
@@ -10686,7 +11211,9 @@ function supportPlugin(config) {
|
|
|
10686
11211
|
const existingEndpoints = incomingConfig.endpoints || [];
|
|
10687
11212
|
const supportEndpoints = createSupportEndpoints(slugs, {
|
|
10688
11213
|
oauth: { allowedEmailDomains: config?.allowedEmailDomains },
|
|
10689
|
-
features
|
|
11214
|
+
features,
|
|
11215
|
+
rateLimitStore,
|
|
11216
|
+
capabilities: config?.capabilities
|
|
10690
11217
|
});
|
|
10691
11218
|
return {
|
|
10692
11219
|
...incomingConfig,
|
|
@@ -10704,9 +11231,13 @@ function supportPlugin(config) {
|
|
|
10704
11231
|
}
|
|
10705
11232
|
|
|
10706
11233
|
exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
|
|
11234
|
+
exports.DEFAULT_INBOUND_EMAIL_LIMITS = DEFAULT_INBOUND_EMAIL_LIMITS;
|
|
10707
11235
|
exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
10708
11236
|
exports.DEFAULT_SLUGS = DEFAULT_SLUGS;
|
|
10709
11237
|
exports.DEFAULT_USER_PREFS = DEFAULT_USER_PREFS;
|
|
11238
|
+
exports.MemoryRateLimitStore = MemoryRateLimitStore;
|
|
11239
|
+
exports.PayloadRateLimitStore = PayloadRateLimitStore;
|
|
11240
|
+
exports.RateLimiter = RateLimiter;
|
|
10710
11241
|
exports.calculateBusinessHoursDeadline = calculateBusinessHoursDeadline;
|
|
10711
11242
|
exports.createAdminNotification = createAdminNotification;
|
|
10712
11243
|
exports.createAssignSlaDeadlines = createAssignSlaDeadlines;
|
|
@@ -10717,7 +11248,9 @@ exports.createCheckSlaOnReply = createCheckSlaOnReply;
|
|
|
10717
11248
|
exports.createCheckSlaOnResolve = createCheckSlaOnResolve;
|
|
10718
11249
|
exports.createEmailLogsCollection = createEmailLogsCollection;
|
|
10719
11250
|
exports.createKnowledgeBaseCollection = createKnowledgeBaseCollection;
|
|
11251
|
+
exports.createLoginEndpoint = createLoginEndpoint;
|
|
10720
11252
|
exports.createMacrosCollection = createMacrosCollection;
|
|
11253
|
+
exports.createOAuthGoogleEndpoint = createOAuthGoogleEndpoint;
|
|
10721
11254
|
exports.createPendingEmailsCollection = createPendingEmailsCollection;
|
|
10722
11255
|
exports.createSatisfactionSurveysCollection = createSatisfactionSurveysCollection;
|
|
10723
11256
|
exports.createSlaPoliciesCollection = createSlaPoliciesCollection;
|
|
@@ -10729,6 +11262,7 @@ exports.createTicketStatusEmail = createTicketStatusEmail;
|
|
|
10729
11262
|
exports.createTicketStatusesCollection = createTicketStatusesCollection;
|
|
10730
11263
|
exports.createTicketsCollection = createTicketsCollection;
|
|
10731
11264
|
exports.createTimeEntriesCollection = createTimeEntriesCollection;
|
|
11265
|
+
exports.createTrackOpenEndpoint = createTrackOpenEndpoint;
|
|
10732
11266
|
exports.createWebhookEndpointsCollection = createWebhookEndpointsCollection;
|
|
10733
11267
|
exports.dispatchWebhook = dispatchWebhook;
|
|
10734
11268
|
exports.generateTicketSynthesis = generateTicketSynthesis;
|
|
@@ -10736,3 +11270,5 @@ exports.readSupportSettings = readSupportSettings;
|
|
|
10736
11270
|
exports.readUserPrefs = readUserPrefs;
|
|
10737
11271
|
exports.resolveSlugs = resolveSlugs;
|
|
10738
11272
|
exports.supportPlugin = supportPlugin;
|
|
11273
|
+
exports.validateInboundEmailPayload = validateInboundEmailPayload;
|
|
11274
|
+
exports.verifySecret = verifySecret;
|