@consilioweb/payload-support 1.1.0 → 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.
Files changed (99) hide show
  1. package/README.md +4 -0
  2. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +22 -14
  3. package/dist/components/TicketConversation/components/AISummaryPanel.js +23 -16
  4. package/dist/components/TicketConversation/components/ActionPanels.cjs +5 -5
  5. package/dist/components/TicketConversation/components/ActionPanels.js +5 -5
  6. package/dist/components/TicketConversation/components/ClientBar.cjs +1 -1
  7. package/dist/components/TicketConversation/components/ClientBar.js +1 -1
  8. package/dist/components/TicketConversation/components/QuickActions.cjs +2 -2
  9. package/dist/components/TicketConversation/components/QuickActions.js +2 -2
  10. package/dist/components/TicketConversation/components/TicketHeader.cjs +1 -1
  11. package/dist/components/TicketConversation/components/TicketHeader.js +1 -1
  12. package/dist/components/TicketConversation/index.cjs +10 -10
  13. package/dist/components/TicketConversation/index.js +10 -10
  14. package/dist/index.cjs +710 -174
  15. package/dist/index.d.cts +165 -3
  16. package/dist/index.d.ts +165 -3
  17. package/dist/index.js +703 -176
  18. package/dist/styles/ChatView.module.scss +1 -1
  19. package/dist/styles/TicketDetail.module.scss +3 -3
  20. package/dist/styles/_tokens.scss +1 -1
  21. package/dist/styles/theme.css +6 -6
  22. package/dist/views/TicketingSettingsView/client.cjs +3 -3
  23. package/dist/views/TicketingSettingsView/client.js +3 -3
  24. package/package.json +9 -9
  25. package/src/__tests__/aiSummaryRendering.test.ts +21 -0
  26. package/src/__tests__/authResponses.test.ts +69 -0
  27. package/src/__tests__/capabilities.test.ts +71 -0
  28. package/src/__tests__/generateTrackingToken.test.ts +18 -4
  29. package/src/__tests__/integration/core-behaviors.test.ts +23 -0
  30. package/src/__tests__/rateLimiter.test.ts +60 -66
  31. package/src/__tests__/trackOpenEndpoint.test.ts +54 -0
  32. package/src/__tests__/webhookSecurity.test.ts +58 -0
  33. package/src/collections/PendingEmails.ts +2 -1
  34. package/src/collections/SupportClients.ts +43 -1
  35. package/src/collections/SupportCounters.ts +14 -0
  36. package/src/collections/SupportRateLimits.ts +20 -0
  37. package/src/collections/TicketMessages.ts +76 -0
  38. package/src/collections/Tickets.ts +183 -40
  39. package/src/collections/index.ts +2 -0
  40. package/src/components/TicketConversation/components/AISummaryPanel.tsx +26 -13
  41. package/src/components/TicketConversation/components/ActionPanels.tsx +5 -5
  42. package/src/components/TicketConversation/components/ClientBar.tsx +1 -1
  43. package/src/components/TicketConversation/components/QuickActions.tsx +2 -2
  44. package/src/components/TicketConversation/components/TicketHeader.tsx +1 -1
  45. package/src/components/TicketConversation/index.tsx +10 -10
  46. package/src/endpoints/admin-chat.ts +4 -4
  47. package/src/endpoints/ai-agent.ts +6 -1
  48. package/src/endpoints/ai.ts +10 -5
  49. package/src/endpoints/auth-2fa.ts +6 -7
  50. package/src/endpoints/auto-close.ts +2 -1
  51. package/src/endpoints/capabilities.ts +144 -0
  52. package/src/endpoints/channels.ts +2 -1
  53. package/src/endpoints/chat.ts +6 -6
  54. package/src/endpoints/chatbot.ts +4 -4
  55. package/src/endpoints/client-intelligence.ts +13 -4
  56. package/src/endpoints/import-conversation.ts +6 -5
  57. package/src/endpoints/index.ts +35 -14
  58. package/src/endpoints/invite-collaborator.ts +4 -4
  59. package/src/endpoints/login.ts +4 -5
  60. package/src/endpoints/oauth-google.ts +1 -1
  61. package/src/endpoints/process-digests.ts +2 -1
  62. package/src/endpoints/process-scheduled.ts +2 -1
  63. package/src/endpoints/process-snooze.ts +2 -1
  64. package/src/endpoints/resend-notification.ts +4 -4
  65. package/src/endpoints/send-reminder.ts +4 -4
  66. package/src/endpoints/ticket-synthesis.ts +17 -2
  67. package/src/endpoints/track-open.ts +44 -21
  68. package/src/endpoints/transfer-ticket.ts +4 -4
  69. package/src/index.ts +7 -0
  70. package/src/plugin.ts +16 -1
  71. package/src/portal/LiveChat.tsx +1 -1
  72. package/src/portal/auth/ChatWidget.tsx +4 -4
  73. package/src/portal/auth/ChatbotWidget.tsx +4 -4
  74. package/src/portal/auth/dashboard/DashboardClient.tsx +2 -2
  75. package/src/portal/auth/faq/FAQSearch.tsx +1 -1
  76. package/src/portal/auth/faq/page.tsx +2 -2
  77. package/src/portal/auth/profile/page.tsx +17 -17
  78. package/src/portal/auth/tickets/detail/CloseTicketButton.tsx +1 -1
  79. package/src/portal/auth/tickets/detail/MarkSolutionButton.tsx +1 -1
  80. package/src/portal/auth/tickets/detail/ReopenTicketButton.tsx +1 -1
  81. package/src/portal/auth/tickets/detail/SatisfactionForm.tsx +3 -3
  82. package/src/portal/auth/tickets/detail/TicketReplyForm.tsx +3 -3
  83. package/src/portal/auth/tickets/detail/page.tsx +5 -5
  84. package/src/portal/auth/tickets/new/page.tsx +8 -8
  85. package/src/portal/forgot-password/page.tsx +3 -3
  86. package/src/portal/layout.tsx +5 -2
  87. package/src/portal/login/page.tsx +7 -7
  88. package/src/portal/page.tsx +9 -9
  89. package/src/portal/register/page.tsx +4 -4
  90. package/src/portal/reset-password/page.tsx +3 -3
  91. package/src/styles/ChatView.module.scss +1 -1
  92. package/src/styles/TicketDetail.module.scss +3 -3
  93. package/src/styles/_tokens.scss +1 -1
  94. package/src/styles/theme.css +6 -6
  95. package/src/types.ts +71 -0
  96. package/src/utils/rateLimiter.ts +131 -38
  97. package/src/utils/slugs.ts +4 -0
  98. package/src/utils/webhookSecurity.ts +81 -0
  99. 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 Anthropic = __require("@anthropic-ai/sdk").default;
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 Anthropic = __require("@anthropic-ai/sdk").default;
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").substring(0, 16);
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 && sig === generateTrackingToken(ticketId, messageId, secret);
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 new Response(TRANSPARENT_GIF, {
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 (!expectedSecret || secret !== expectedSecret) {
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
- var chatbotLimiter = new RateLimiter(6e4, 10);
2559
- function createChatbotEndpoint(slugs) {
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
- var resendLimiter = new RateLimiter(60 * 60 * 1e3, 10);
4501
- function createResendNotificationEndpoint(slugs) {
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
- var loginLimiter = new RateLimiter(15 * 6e4, 10);
4713
- function createLoginEndpoint(slugs) {
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({ token, user: clientDoc, exp }), {
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 && process.env.SUPPORT_WEBHOOK_SECRET && webhookSecret === process.env.SUPPORT_WEBHOOK_SECRET) {
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 (!expectedSecret || secret !== expectedSecret) {
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 (!expectedSecret || secret !== expectedSecret) {
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 (!process.env.CRON_SECRET || secret !== process.env.CRON_SECRET) {
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 (!process.env.CHANNELS_WEBHOOK_SECRET || secret !== process.env.CHANNELS_WEBHOOK_SECRET) {
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
- var transferLimiter = new RateLimiter(24 * 60 * 60 * 1e3, MAX_TRANSFERS_PER_DAY);
6185
- function createTransferTicketEndpoint(slugs) {
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
- var inviteLimiter = new RateLimiter(60 * 60 * 1e3, 10);
6368
- function createInviteCollaboratorEndpoint(slugs) {
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
- function createAssignTicketNumber(slugs) {
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
- let retries = 3;
7246
- while (retries > 0) {
7247
- try {
7248
- const countResult = await req.payload.count({
7249
- collection: slugs.tickets,
7250
- overrideAccess: true
7251
- });
7252
- const baseNumber = countResult.totalDocs + 1;
7253
- data.ticketNumber = `TK-${String(baseNumber).padStart(4, "0")}`;
7254
- const existing = await req.payload.find({
7255
- collection: slugs.tickets,
7256
- where: { ticketNumber: { equals: data.ticketNumber } },
7257
- limit: 1,
7258
- depth: 0,
7259
- overrideAccess: true
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
- if (existing.docs.length > 0) {
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
  };
@@ -7294,7 +7579,7 @@ function createRestrictClientUpdates(slugs) {
7294
7579
  return async ({ data, operation, req, originalDoc }) => {
7295
7580
  if (operation !== "update") return data;
7296
7581
  if (req.user?.collection !== slugs.supportClients) return data;
7297
- const allowedStatuses = ["open", "resolved"];
7582
+ const allowedStatuses = ["open", "waiting_support", "resolved"];
7298
7583
  const newData = {};
7299
7584
  if (data.status && allowedStatuses.includes(data.status)) {
7300
7585
  newData.status = data.status;
@@ -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 }).catch((err) => console.error("[support] Background ai synthesis failed:", err));
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 && process.env.SUPPORT_WEBHOOK_SECRET && webhookSecret === process.env.SUPPORT_WEBHOOK_SECRET) return true;
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;