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