@consilioweb/payload-support 4.0.0 → 6.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 (159) hide show
  1. package/README.md +272 -35
  2. package/dist/components/RichTextEditor/index.js +1 -1
  3. package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
  4. package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
  5. package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
  6. package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
  7. package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
  8. package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
  9. package/dist/components/TicketConversation/components/QuickActions.js +10 -8
  10. package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
  11. package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
  12. package/dist/components/TicketConversation/index.d.ts +10 -0
  13. package/dist/components/TicketConversation/index.js +25 -7
  14. package/dist/index.cjs +421 -73
  15. package/dist/index.d.cts +127 -38
  16. package/dist/index.d.ts +127 -38
  17. package/dist/index.js +415 -72
  18. package/dist/styles/BillingView.module.scss +11 -11
  19. package/dist/styles/ChatView.module.scss +7 -7
  20. package/dist/styles/CommandPalette.module.scss +1 -1
  21. package/dist/styles/CrmView.module.scss +9 -9
  22. package/dist/styles/EmailTracking.module.scss +6 -6
  23. package/dist/styles/ImportConversation.module.scss +5 -5
  24. package/dist/styles/Logs.module.scss +5 -5
  25. package/dist/styles/NewTicket.module.scss +2 -2
  26. package/dist/styles/PendingEmails.module.scss +13 -13
  27. package/dist/styles/SupportDashboard.module.scss +24 -8
  28. package/dist/styles/TicketDetail.module.scss +20 -20
  29. package/dist/styles/TicketInbox.module.scss +6 -6
  30. package/dist/styles/TicketingSettings.module.scss +10 -10
  31. package/dist/styles/TimeDashboard.module.scss +6 -6
  32. package/dist/styles/_tokens.scss +3 -1
  33. package/dist/utils/db.d.ts +34 -0
  34. package/dist/utils/readSettings.d.ts +90 -0
  35. package/dist/views/BillingView/client.js +15 -8
  36. package/dist/views/BillingView/index.js +4 -4
  37. package/dist/views/ChatView/client.js +2 -0
  38. package/dist/views/ChatView/index.js +4 -4
  39. package/dist/views/CrmView/client.js +2 -0
  40. package/dist/views/CrmView/index.js +4 -4
  41. package/dist/views/EmailTrackingView/client.js +23 -11
  42. package/dist/views/EmailTrackingView/index.js +4 -4
  43. package/dist/views/ImportConversationView/client.js +1 -0
  44. package/dist/views/ImportConversationView/index.js +4 -4
  45. package/dist/views/LogsView/client.js +1 -1
  46. package/dist/views/LogsView/index.js +4 -2
  47. package/dist/views/NewTicketView/client.js +20 -13
  48. package/dist/views/NewTicketView/index.js +4 -2
  49. package/dist/views/PendingEmailsView/client.js +9 -3
  50. package/dist/views/PendingEmailsView/index.js +4 -4
  51. package/dist/views/SupportDashboardView/client.js +17 -19
  52. package/dist/views/SupportDashboardView/index.js +4 -4
  53. package/dist/views/TicketDetailView/client.js +23 -11
  54. package/dist/views/TicketDetailView/index.js +4 -4
  55. package/dist/views/TicketInboxView/client.js +52 -26
  56. package/dist/views/TicketInboxView/index.js +4 -2
  57. package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  58. package/dist/views/TicketingSettingsView/client.js +44 -26
  59. package/dist/views/TicketingSettingsView/index.js +4 -4
  60. package/dist/views/TimeDashboardView/client.js +10 -7
  61. package/dist/views/TimeDashboardView/index.js +4 -4
  62. package/dist/views/shared/ErrorBoundary.d.ts +36 -1
  63. package/dist/views/shared/ErrorBoundary.js +59 -28
  64. package/dist/views/shared/adminTokens.d.ts +15 -5
  65. package/dist/views/shared/adminTokens.js +23 -7
  66. package/dist/views/shared/icons.d.ts +49 -0
  67. package/dist/views/shared/icons.js +92 -0
  68. package/dist/views/shared/locales/en.json +15 -1
  69. package/dist/views/shared/locales/fr.json +15 -1
  70. package/dist/views/shared/viewAccess.d.ts +29 -0
  71. package/dist/views/shared/viewAccess.js +24 -0
  72. package/package.json +31 -21
  73. package/scripts/uninstall-data.mjs +178 -0
  74. package/scripts/uninstall.mjs +184 -0
  75. package/src/collections/Tickets.ts +7 -0
  76. package/src/collections/TimeEntries.ts +11 -3
  77. package/src/components/RichTextEditor/index.tsx +1 -1
  78. package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
  79. package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
  80. package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
  81. package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
  82. package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
  83. package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
  84. package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
  85. package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
  86. package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
  87. package/src/components/TicketConversation/index.tsx +41 -7
  88. package/src/endpoints/auth-2fa.ts +53 -8
  89. package/src/endpoints/capabilities.ts +2 -4
  90. package/src/endpoints/chatbot.ts +2 -2
  91. package/src/endpoints/delete-account.ts +191 -47
  92. package/src/endpoints/export-data.ts +104 -4
  93. package/src/endpoints/import-conversation.ts +2 -2
  94. package/src/endpoints/login.ts +13 -3
  95. package/src/endpoints/purge-logs.ts +6 -15
  96. package/src/endpoints/push.ts +14 -1
  97. package/src/endpoints/statuses.ts +17 -0
  98. package/src/index.ts +2 -0
  99. package/src/plugin.ts +51 -1
  100. package/src/portal/LiveChat.tsx +1 -1
  101. package/src/portal/auth/profile/page.tsx +3 -2
  102. package/src/portal/icons.tsx +91 -0
  103. package/src/portal/login/page.tsx +14 -4
  104. package/src/styles/BillingView.module.scss +11 -11
  105. package/src/styles/ChatView.module.scss +7 -7
  106. package/src/styles/CommandPalette.module.scss +1 -1
  107. package/src/styles/CrmView.module.scss +9 -9
  108. package/src/styles/EmailTracking.module.scss +6 -6
  109. package/src/styles/ImportConversation.module.scss +5 -5
  110. package/src/styles/Logs.module.scss +5 -5
  111. package/src/styles/NewTicket.module.scss +2 -2
  112. package/src/styles/PendingEmails.module.scss +13 -13
  113. package/src/styles/SupportDashboard.module.scss +24 -8
  114. package/src/styles/TicketDetail.module.scss +20 -20
  115. package/src/styles/TicketInbox.module.scss +6 -6
  116. package/src/styles/TicketingSettings.module.scss +10 -10
  117. package/src/styles/TimeDashboard.module.scss +6 -6
  118. package/src/styles/_tokens.scss +3 -1
  119. package/src/types.ts +20 -0
  120. package/src/utils/push.ts +22 -0
  121. package/src/utils/rateLimiter.ts +98 -0
  122. package/src/utils/retention.ts +111 -0
  123. package/src/utils/slugs.ts +15 -0
  124. package/src/utils/twoFactorChallenge.ts +6 -1
  125. package/src/utils/urlSafety.ts +36 -1
  126. package/src/views/BillingView/client.tsx +17 -8
  127. package/src/views/BillingView/index.tsx +4 -4
  128. package/src/views/ChatView/client.tsx +2 -0
  129. package/src/views/ChatView/index.tsx +4 -4
  130. package/src/views/CrmView/client.tsx +2 -0
  131. package/src/views/CrmView/index.tsx +4 -4
  132. package/src/views/EmailTrackingView/client.tsx +18 -7
  133. package/src/views/EmailTrackingView/index.tsx +4 -4
  134. package/src/views/ImportConversationView/client.tsx +1 -0
  135. package/src/views/ImportConversationView/index.tsx +4 -4
  136. package/src/views/LogsView/client.tsx +1 -1
  137. package/src/views/LogsView/index.tsx +4 -2
  138. package/src/views/NewTicketView/client.tsx +22 -13
  139. package/src/views/NewTicketView/index.tsx +4 -2
  140. package/src/views/PendingEmailsView/client.tsx +9 -3
  141. package/src/views/PendingEmailsView/index.tsx +4 -4
  142. package/src/views/SupportDashboardView/client.tsx +11 -5
  143. package/src/views/SupportDashboardView/index.tsx +4 -4
  144. package/src/views/TicketDetailView/client.tsx +25 -11
  145. package/src/views/TicketDetailView/index.tsx +4 -4
  146. package/src/views/TicketInboxView/client.tsx +47 -7
  147. package/src/views/TicketInboxView/index.tsx +4 -2
  148. package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
  149. package/src/views/TicketingSettingsView/client.tsx +201 -144
  150. package/src/views/TicketingSettingsView/index.tsx +4 -4
  151. package/src/views/TimeDashboardView/client.tsx +10 -7
  152. package/src/views/TimeDashboardView/index.tsx +4 -4
  153. package/src/views/shared/ErrorBoundary.tsx +95 -31
  154. package/src/views/shared/adminTokens.ts +28 -11
  155. package/src/views/shared/icons.tsx +146 -0
  156. package/src/views/shared/locales/en.json +15 -1
  157. package/src/views/shared/locales/fr.json +15 -1
  158. package/src/views/shared/viewAccess.ts +73 -0
  159. package/src/types/lucide-react.d.ts +0 -42
package/dist/index.cjs CHANGED
@@ -81,15 +81,51 @@ var DEFAULT_SLUGS = {
81
81
  users: "users",
82
82
  media: "media"
83
83
  };
84
+ var FIXED_SLUGS = {
85
+ clientSummaries: "client-summaries",
86
+ ticketCollaborators: "ticket-collaborators",
87
+ ticketFeedback: "ticket-feedback"
88
+ };
84
89
  function resolveSlugs(overrides) {
85
90
  return { ...DEFAULT_SLUGS, ...overrides };
86
91
  }
92
+ var MAX_MEMORY_RATE_LIMIT_KEYS = 1e4;
87
93
  var MemoryRateLimitStore = class {
94
+ constructor(maxKeys = MAX_MEMORY_RATE_LIMIT_KEYS) {
95
+ this.maxKeys = maxKeys;
96
+ }
97
+ maxKeys;
88
98
  entries = /* @__PURE__ */ new Map();
99
+ /** Distinct keys currently held. Exposed so the ceiling can be asserted. */
100
+ get size() {
101
+ return this.entries.size;
102
+ }
103
+ /** Reclaims every window that has already closed. */
104
+ sweepExpired(now) {
105
+ for (const [key, entry] of this.entries) {
106
+ if (now > entry.resetAt) this.entries.delete(key);
107
+ }
108
+ }
109
+ /** Soonest-closing window first, so the ceiling drops the least useful entry. */
110
+ evictOldest() {
111
+ let oldestKey = null;
112
+ let oldestResetAt = Infinity;
113
+ for (const [key, entry] of this.entries) {
114
+ if (entry.resetAt < oldestResetAt) {
115
+ oldestResetAt = entry.resetAt;
116
+ oldestKey = key;
117
+ }
118
+ }
119
+ if (oldestKey !== null) this.entries.delete(oldestKey);
120
+ }
89
121
  async increment(key, windowMs) {
90
122
  const now = Date.now();
91
123
  const current = this.entries.get(key);
92
124
  const next = !current || now > current.resetAt ? { count: 1, resetAt: now + windowMs } : { ...current, count: current.count + 1 };
125
+ if (!current && this.entries.size >= this.maxKeys) {
126
+ this.sweepExpired(now);
127
+ if (this.entries.size >= this.maxKeys) this.evictOldest();
128
+ }
93
129
  this.entries.set(key, next);
94
130
  return next;
95
131
  }
@@ -173,6 +209,23 @@ function principalRateKey(user) {
173
209
  const collection = typeof user.collection === "string" && user.collection ? user.collection : "unknown";
174
210
  return `${collection}:${String(user.id)}`;
175
211
  }
212
+ var MAX_IP_KEY_LENGTH = 45;
213
+ var IPV4_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/;
214
+ var IPV6_PATTERN = /^[0-9a-fA-F:.%]+$/;
215
+ function clientIpRateKey(req) {
216
+ const candidate = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip")?.trim() || "";
217
+ return normalizeIpKey(candidate);
218
+ }
219
+ function normalizeIpKey(candidate) {
220
+ if (!candidate || candidate.length > MAX_IP_KEY_LENGTH) return "unknown";
221
+ const host = candidate.startsWith("[") && candidate.endsWith("]") ? candidate.slice(1, -1) : candidate;
222
+ if (!host) return "unknown";
223
+ if (IPV4_PATTERN.test(host)) {
224
+ return host.split(".").every((octet) => Number(octet) <= 255) ? host : "unknown";
225
+ }
226
+ if (host.includes(":") && IPV6_PATTERN.test(host)) return host.toLowerCase();
227
+ return "unknown";
228
+ }
176
229
  var RateLimiter = class {
177
230
  /**
178
231
  * @param namespace Endpoint-scoped prefix for every key this limiter writes.
@@ -252,7 +305,7 @@ function createInboundEmailEndpoint(capability, store) {
252
305
  if (!verifySecret(req.headers.get(secretHeader), capability.secret)) {
253
306
  return Response.json({ error: "Unauthorized" }, { status: 401 });
254
307
  }
255
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
308
+ const ip = clientIpRateKey(req);
256
309
  if (await limiter.check(ip, req)) {
257
310
  return Response.json({ error: "Rate limit exceeded" }, { status: 429 });
258
311
  }
@@ -2809,6 +2862,10 @@ function createStatusesEndpoint(slugs) {
2809
2862
  if (!req.user) {
2810
2863
  return Response.json({ error: "Unauthorized" }, { status: 401 });
2811
2864
  }
2865
+ const collection = req.user.collection;
2866
+ if (collection !== slugs.users && collection !== slugs.supportClients) {
2867
+ return Response.json({ error: "Forbidden" }, { status: 403 });
2868
+ }
2812
2869
  const { docs } = await dbFind(payload, slugs.ticketStatuses, {
2813
2870
  sort: "sortOrder",
2814
2871
  limit: 100,
@@ -2945,6 +3002,51 @@ function createApplyMacroEndpoint(slugs) {
2945
3002
  };
2946
3003
  }
2947
3004
 
3005
+ // src/utils/retention.ts
3006
+ var DEFAULT_RETENTION = {
3007
+ authLogsDays: 180,
3008
+ emailLogsDays: 365,
3009
+ cron: "0 30 3 * * *",
3010
+ queue: "default"
3011
+ };
3012
+ var PURGE_LOGS_TASK_SLUG = "support-purge-logs";
3013
+ function purgeableCollections(slugs) {
3014
+ return {
3015
+ "email-logs": slugs.emailLogs,
3016
+ "auth-logs": slugs.authLogs
3017
+ };
3018
+ }
3019
+ async function purgeOlderThan(payload, slug, days, req) {
3020
+ const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
3021
+ const result = await dbDelete(payload, slug, {
3022
+ where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
3023
+ overrideAccess: true,
3024
+ ...req ? { req } : {}
3025
+ });
3026
+ return Array.isArray(result?.docs) ? result.docs.length : 0;
3027
+ }
3028
+ async function runScheduledPurge(payload, slugs, retention, req) {
3029
+ if (retention === false) return { purged: {}, skipped: ["auth-logs", "email-logs"] };
3030
+ const plan = [
3031
+ ["auth-logs", slugs.authLogs, retention?.authLogsDays ?? DEFAULT_RETENTION.authLogsDays],
3032
+ ["email-logs", slugs.emailLogs, retention?.emailLogsDays ?? DEFAULT_RETENTION.emailLogsDays]
3033
+ ];
3034
+ const purged = {};
3035
+ const skipped = [];
3036
+ for (const [name, slug, days] of plan) {
3037
+ if (!Number.isFinite(days) || days <= 0) {
3038
+ skipped.push(name);
3039
+ continue;
3040
+ }
3041
+ if (!payload.collections?.[slug]) {
3042
+ skipped.push(name);
3043
+ continue;
3044
+ }
3045
+ purged[name] = await purgeOlderThan(payload, slug, days, req);
3046
+ }
3047
+ return { purged, skipped };
3048
+ }
3049
+
2948
3050
  // src/endpoints/purge-logs.ts
2949
3051
  function createPurgeLogsEndpoint(slugs) {
2950
3052
  return {
@@ -2957,19 +3059,11 @@ function createPurgeLogsEndpoint(slugs) {
2957
3059
  const url = new URL(req.url);
2958
3060
  const collection = url.searchParams.get("collection");
2959
3061
  const days = Number(url.searchParams.get("days") || "0");
2960
- const allowedCollections = {
2961
- "email-logs": slugs.emailLogs,
2962
- "auth-logs": slugs.authLogs
2963
- };
3062
+ const allowedCollections = purgeableCollections(slugs);
2964
3063
  if (!collection || !allowedCollections[collection]) {
2965
3064
  return Response.json({ error: "Invalid collection. Use email-logs or auth-logs." }, { status: 400 });
2966
3065
  }
2967
- const cutoff = days > 0 ? new Date(Date.now() - days * 864e5).toISOString() : null;
2968
- const result = await dbDelete(payload, allowedCollections[collection], {
2969
- where: cutoff ? { createdAt: { less_than: cutoff } } : { id: { exists: true } },
2970
- overrideAccess: true
2971
- });
2972
- const count = Array.isArray(result.docs) ? result.docs.length : 0;
3066
+ const count = await purgeOlderThan(payload, allowedCollections[collection], days, req);
2973
3067
  return Response.json({
2974
3068
  purged: count,
2975
3069
  collection,
@@ -2999,7 +3093,7 @@ function createChatbotEndpoint(slugs, store, maxPerHour) {
2999
3093
  method: "post",
3000
3094
  handler: async (req) => {
3001
3095
  try {
3002
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
3096
+ const ip = clientIpRateKey(req);
3003
3097
  if (await chatbotLimiter.check(ip, req)) {
3004
3098
  return Response.json({ error: "Too many requests. Please wait a moment." }, { status: 429 });
3005
3099
  }
@@ -4692,6 +4786,7 @@ function createExportCsvEndpoint(slugs) {
4692
4786
  }
4693
4787
 
4694
4788
  // src/endpoints/export-data.ts
4789
+ var EMPTY = { docs: [] };
4695
4790
  function createExportDataEndpoint(slugs) {
4696
4791
  return {
4697
4792
  path: "/support/export-data",
@@ -4700,7 +4795,16 @@ function createExportDataEndpoint(slugs) {
4700
4795
  try {
4701
4796
  const payload = req.payload;
4702
4797
  requireClient(req, slugs);
4703
- const [clientData, ticketsResult, messagesResult, surveysResult] = await Promise.all([
4798
+ const isRegistered = (slug) => Boolean(payload.collections?.[slug]);
4799
+ const [
4800
+ clientData,
4801
+ ticketsResult,
4802
+ messagesResult,
4803
+ surveysResult,
4804
+ chatResult,
4805
+ feedbackResult,
4806
+ summariesResult
4807
+ ] = await Promise.all([
4704
4808
  dbFindByID(payload, slugs.supportClients, {
4705
4809
  id: req.user.id,
4706
4810
  depth: 0,
@@ -4726,12 +4830,35 @@ function createExportDataEndpoint(slugs) {
4726
4830
  limit: 500,
4727
4831
  depth: 0,
4728
4832
  overrideAccess: true
4729
- })
4833
+ }),
4834
+ isRegistered(slugs.chatMessages) ? dbFind(payload, slugs.chatMessages, {
4835
+ where: { client: { equals: req.user.id } },
4836
+ limit: 5e3,
4837
+ depth: 0,
4838
+ overrideAccess: true
4839
+ }) : EMPTY,
4840
+ isRegistered(FIXED_SLUGS.ticketFeedback) ? dbFind(payload, FIXED_SLUGS.ticketFeedback, {
4841
+ where: { client: { equals: req.user.id } },
4842
+ limit: 500,
4843
+ depth: 0,
4844
+ overrideAccess: true
4845
+ }) : EMPTY,
4846
+ isRegistered(FIXED_SLUGS.clientSummaries) ? dbFind(payload, FIXED_SLUGS.clientSummaries, {
4847
+ where: { client: { equals: req.user.id } },
4848
+ limit: 100,
4849
+ depth: 0,
4850
+ overrideAccess: true
4851
+ }) : EMPTY
4730
4852
  ]);
4853
+ const ticketIds = ticketsResult.docs.map((t) => t.id);
4854
+ const timeResult = ticketIds.length > 0 && isRegistered(slugs.timeEntries) ? await dbFind(payload, slugs.timeEntries, {
4855
+ where: { ticket: { in: ticketIds } },
4856
+ limit: 5e3,
4857
+ depth: 0,
4858
+ overrideAccess: true
4859
+ }) : EMPTY;
4731
4860
  const c = clientData;
4732
- const exportData = {
4733
- exportDate: (/* @__PURE__ */ new Date()).toISOString(),
4734
- exportType: "RGPD - Export des donn\xE9es personnelles",
4861
+ const providedByYou = {
4735
4862
  profile: {
4736
4863
  email: c.email,
4737
4864
  firstName: c.firstName,
@@ -4760,8 +4887,50 @@ function createExportDataEndpoint(slugs) {
4760
4887
  rating: s.rating,
4761
4888
  comment: s.comment,
4762
4889
  createdAt: s.createdAt
4890
+ })),
4891
+ feedback: feedbackResult.docs.map((f) => ({
4892
+ ticketId: f.ticket,
4893
+ rating: f.rating,
4894
+ comment: f.comment,
4895
+ submittedFrom: f.submittedFrom,
4896
+ createdAt: f.createdAt
4897
+ })),
4898
+ chatMessages: chatResult.docs.map((m) => ({
4899
+ session: m.session,
4900
+ senderType: m.senderType,
4901
+ message: m.message,
4902
+ createdAt: m.createdAt
4903
+ })),
4904
+ timeEntries: timeResult.docs.map((e) => ({
4905
+ ticketId: e.ticket,
4906
+ duration: e.duration,
4907
+ date: e.date,
4908
+ description: e.description,
4909
+ billable: e.billable
4763
4910
  }))
4764
4911
  };
4912
+ const exportData = {
4913
+ exportDate: (/* @__PURE__ */ new Date()).toISOString(),
4914
+ exportType: "RGPD - Export des donn\xE9es personnelles",
4915
+ providedByYou: {
4916
+ legalBasis: "Art. 20 RGPD - droit \xE0 la portabilit\xE9",
4917
+ ...providedByYou
4918
+ },
4919
+ derivedData: {
4920
+ legalBasis: "Art. 15 RGPD - droit d'acc\xE8s (donn\xE9es d\xE9riv\xE9es, hors portabilit\xE9)",
4921
+ clientSummaries: summariesResult.docs.map((s) => ({
4922
+ summary: s.summary,
4923
+ recurringTopics: s.recurringTopics,
4924
+ patterns: s.patterns,
4925
+ keyFacts: s.keyFacts,
4926
+ ticketCount: s.ticketCount,
4927
+ messageCount: s.messageCount,
4928
+ averageSatisfaction: s.averageSatisfaction,
4929
+ generatedAt: s.generatedAt,
4930
+ aiModel: s.aiModel
4931
+ }))
4932
+ }
4933
+ };
4765
4934
  const json = JSON.stringify(exportData, null, 2);
4766
4935
  return new Response(json, {
4767
4936
  status: 200,
@@ -5205,13 +5374,14 @@ function verifyTwoFactorChallenge(email, token, now = Date.now()) {
5205
5374
  }
5206
5375
 
5207
5376
  // src/endpoints/login.ts
5377
+ var MAX_LOGGED_USER_AGENT = 256;
5208
5378
  function createLoginEndpoint(slugs, store) {
5209
5379
  const loginLimiter = new RateLimiter(15 * 6e4, 10, store, "login");
5210
5380
  return {
5211
5381
  path: "/support/login",
5212
5382
  method: "post",
5213
5383
  handler: async (req) => {
5214
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || req.headers.get("x-real-ip") || "unknown";
5384
+ const ip = clientIpRateKey(req);
5215
5385
  if (await loginLimiter.check(ip, req)) {
5216
5386
  return Response.json(
5217
5387
  { error: "Trop de tentatives. R\xE9essayez dans quelques minutes." },
@@ -5226,7 +5396,7 @@ function createLoginEndpoint(slugs, store) {
5226
5396
  return Response.json({ error: "Invalid JSON body" }, { status: 400 });
5227
5397
  }
5228
5398
  const { email, password } = body;
5229
- const userAgent = req.headers.get("user-agent") || "";
5399
+ const userAgent = (req.headers.get("user-agent") || "").slice(0, MAX_LOGGED_USER_AGENT);
5230
5400
  if (!email || !password) {
5231
5401
  return Response.json({ error: "Email et mot de passe requis." }, { status: 400 });
5232
5402
  }
@@ -5314,7 +5484,19 @@ function createAuth2faEndpoint(slugs, store) {
5314
5484
  if (!action || !email) {
5315
5485
  return Response.json({ error: "Param\xE8tres manquants" }, { status: 400 });
5316
5486
  }
5317
- const genericSendResponse = { success: true, message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9." };
5487
+ const limiterKey = normalizeEmail(email);
5488
+ const sendResponse = () => {
5489
+ let refreshed;
5490
+ try {
5491
+ refreshed = issueTwoFactorChallenge(email);
5492
+ } catch {
5493
+ }
5494
+ return Response.json({
5495
+ success: true,
5496
+ message: "Si un compte existe, un code a \xE9t\xE9 envoy\xE9.",
5497
+ ...refreshed ? { challenge: refreshed } : {}
5498
+ });
5499
+ };
5318
5500
  if (action === "send") {
5319
5501
  if (!verifyTwoFactorChallenge(email, challenge)) {
5320
5502
  return Response.json(
@@ -5322,8 +5504,8 @@ function createAuth2faEndpoint(slugs, store) {
5322
5504
  { status: 401 }
5323
5505
  );
5324
5506
  }
5325
- if (await sendLimiter.check(email, req)) {
5326
- return Response.json(genericSendResponse);
5507
+ if (await sendLimiter.check(limiterKey, req)) {
5508
+ return sendResponse();
5327
5509
  }
5328
5510
  const clients = await dbFind(payload, slugs.supportClients, {
5329
5511
  where: { email: { equals: email } },
@@ -5332,7 +5514,7 @@ function createAuth2faEndpoint(slugs, store) {
5332
5514
  overrideAccess: true
5333
5515
  });
5334
5516
  if (clients.docs.length === 0) {
5335
- return Response.json(genericSendResponse);
5517
+ return sendResponse();
5336
5518
  }
5337
5519
  const client = clients.docs[0];
5338
5520
  const plainCode = generateSecureCode();
@@ -5357,13 +5539,19 @@ function createAuth2faEndpoint(slugs, store) {
5357
5539
  <p style="font-size: 13px; color: #6b7280;">Ce code est valable 10 minutes.</p>
5358
5540
  </div>`
5359
5541
  });
5360
- return Response.json(genericSendResponse);
5542
+ return sendResponse();
5361
5543
  }
5362
5544
  if (action === "verify") {
5545
+ if (!verifyTwoFactorChallenge(email, challenge)) {
5546
+ return Response.json(
5547
+ { error: "Authentification requise avant la v\xE9rification d'un code." },
5548
+ { status: 401 }
5549
+ );
5550
+ }
5363
5551
  if (!code) {
5364
5552
  return Response.json({ error: "Code manquant" }, { status: 400 });
5365
5553
  }
5366
- if (await verifyLimiter.check(email, req)) {
5554
+ if (await verifyLimiter.check(limiterKey, req)) {
5367
5555
  return Response.json(
5368
5556
  { error: "Trop de tentatives. R\xE9essayez dans 15 minutes." },
5369
5557
  { status: 429 }
@@ -5404,7 +5592,7 @@ function createAuth2faEndpoint(slugs, store) {
5404
5592
  data: { twoFactorCode: "", twoFactorExpiry: "", twoFactorVerifiedAt: (/* @__PURE__ */ new Date()).toISOString() },
5405
5593
  overrideAccess: true
5406
5594
  });
5407
- verifyLimiter.reset(email);
5595
+ await verifyLimiter.reset(limiterKey, req);
5408
5596
  return Response.json({ success: true, verified: true });
5409
5597
  }
5410
5598
  return Response.json({ error: "Action invalide" }, { status: 400 });
@@ -5656,15 +5844,22 @@ function createOAuthGoogleEndpoint(slugs, options) {
5656
5844
  }
5657
5845
  };
5658
5846
  }
5659
-
5660
- // src/endpoints/delete-account.ts
5847
+ function collectAttachmentIds(docs, into) {
5848
+ for (const doc of docs) {
5849
+ for (const attachment of doc.attachments ?? []) {
5850
+ const file = attachment?.file;
5851
+ const id = file != null && typeof file === "object" ? file.id : file;
5852
+ if (id != null) into.add(id);
5853
+ }
5854
+ }
5855
+ }
5661
5856
  function createDeleteAccountEndpoint(slugs) {
5662
5857
  return {
5663
5858
  path: "/support/delete-account",
5664
5859
  method: "post",
5665
5860
  handler: async (req) => {
5666
5861
  try {
5667
- const payload = req.payload;
5862
+ const payload$1 = req.payload;
5668
5863
  requireClient(req, slugs);
5669
5864
  const body = await req.json();
5670
5865
  const { confirmPassword } = body;
@@ -5675,7 +5870,7 @@ function createDeleteAccountEndpoint(slugs) {
5675
5870
  );
5676
5871
  }
5677
5872
  try {
5678
- await payload.login({
5873
+ await payload$1.login({
5679
5874
  collection: slugs.supportClients,
5680
5875
  data: { email: req.user.email, password: confirmPassword }
5681
5876
  });
@@ -5686,44 +5881,135 @@ function createDeleteAccountEndpoint(slugs) {
5686
5881
  );
5687
5882
  }
5688
5883
  const clientId = req.user.id;
5689
- const tickets = await dbFind(payload, slugs.tickets, {
5690
- where: { client: { equals: clientId } },
5691
- limit: 1e4,
5692
- depth: 0,
5693
- overrideAccess: true,
5694
- select: { id: true }
5695
- });
5696
- const ticketIds = tickets.docs.map((t) => t.id);
5697
- if (ticketIds.length > 0) {
5698
- await dbDelete(payload, slugs.ticketMessages, {
5699
- where: { ticket: { in: ticketIds } },
5700
- overrideAccess: true
5701
- });
5702
- await dbDelete(payload, slugs.ticketActivityLog, {
5703
- where: { ticket: { in: ticketIds } },
5704
- overrideAccess: true
5705
- });
5706
- await dbDelete(payload, slugs.timeEntries, {
5707
- where: { ticket: { in: ticketIds } },
5708
- overrideAccess: true
5709
- });
5710
- await dbDelete(payload, slugs.tickets, {
5884
+ const clientEmail = req.user.email;
5885
+ const isRegistered = (slug) => Boolean(payload$1.collections?.[slug]);
5886
+ const startedTransaction = await payload.initTransaction(req);
5887
+ try {
5888
+ const tickets = await dbFind(payload$1, slugs.tickets, {
5711
5889
  where: { client: { equals: clientId } },
5712
- overrideAccess: true
5890
+ limit: 1e4,
5891
+ depth: 0,
5892
+ overrideAccess: true,
5893
+ select: { id: true },
5894
+ req
5713
5895
  });
5896
+ const ticketIds = tickets.docs.map((t) => t.id);
5897
+ const attachmentIds = /* @__PURE__ */ new Set();
5898
+ if (ticketIds.length > 0) {
5899
+ const messages = await dbFind(
5900
+ payload$1,
5901
+ slugs.ticketMessages,
5902
+ {
5903
+ where: { ticket: { in: ticketIds } },
5904
+ limit: 1e4,
5905
+ depth: 0,
5906
+ overrideAccess: true,
5907
+ req
5908
+ }
5909
+ );
5910
+ collectAttachmentIds(messages.docs, attachmentIds);
5911
+ }
5912
+ if (isRegistered(slugs.pendingEmails)) {
5913
+ const pendingEmails = await dbFind(
5914
+ payload$1,
5915
+ slugs.pendingEmails,
5916
+ {
5917
+ where: { client: { equals: clientId } },
5918
+ limit: 1e4,
5919
+ depth: 0,
5920
+ overrideAccess: true,
5921
+ req
5922
+ }
5923
+ );
5924
+ collectAttachmentIds(pendingEmails.docs, attachmentIds);
5925
+ }
5926
+ if (ticketIds.length > 0) {
5927
+ for (const slug of [FIXED_SLUGS.ticketFeedback, FIXED_SLUGS.ticketCollaborators]) {
5928
+ if (!isRegistered(slug)) continue;
5929
+ await dbDelete(payload$1, slug, {
5930
+ where: {
5931
+ or: [
5932
+ { ticket: { in: ticketIds } },
5933
+ { client: { equals: clientId } }
5934
+ ]
5935
+ },
5936
+ overrideAccess: true,
5937
+ req
5938
+ });
5939
+ }
5940
+ }
5941
+ if (ticketIds.length > 0) {
5942
+ await dbDelete(payload$1, slugs.tickets, {
5943
+ where: { client: { equals: clientId } },
5944
+ overrideAccess: true,
5945
+ req
5946
+ });
5947
+ }
5948
+ for (const slug of [
5949
+ slugs.satisfactionSurveys,
5950
+ slugs.chatMessages,
5951
+ slugs.notificationQueue,
5952
+ slugs.pendingEmails,
5953
+ FIXED_SLUGS.clientSummaries,
5954
+ FIXED_SLUGS.ticketFeedback
5955
+ ]) {
5956
+ if (!isRegistered(slug)) continue;
5957
+ await dbDelete(payload$1, slug, {
5958
+ where: { client: { equals: clientId } },
5959
+ overrideAccess: true,
5960
+ req
5961
+ });
5962
+ }
5963
+ if (isRegistered(FIXED_SLUGS.ticketCollaborators)) {
5964
+ await dbDelete(payload$1, FIXED_SLUGS.ticketCollaborators, {
5965
+ where: {
5966
+ or: [
5967
+ { client: { equals: clientId } },
5968
+ ...clientEmail ? [{ email: { equals: clientEmail } }] : []
5969
+ ]
5970
+ },
5971
+ overrideAccess: true,
5972
+ req
5973
+ });
5974
+ }
5975
+ if (clientEmail) {
5976
+ if (isRegistered(slugs.authLogs)) {
5977
+ await dbDelete(payload$1, slugs.authLogs, {
5978
+ where: { email: { equals: clientEmail } },
5979
+ overrideAccess: true,
5980
+ req
5981
+ });
5982
+ }
5983
+ if (isRegistered(slugs.emailLogs)) {
5984
+ await dbDelete(payload$1, slugs.emailLogs, {
5985
+ where: {
5986
+ or: [
5987
+ { senderEmail: { equals: clientEmail } },
5988
+ { recipientEmail: { equals: clientEmail } }
5989
+ ]
5990
+ },
5991
+ overrideAccess: true,
5992
+ req
5993
+ });
5994
+ }
5995
+ }
5996
+ for (const id of isRegistered(slugs.media) ? attachmentIds : []) {
5997
+ await dbDelete(payload$1, slugs.media, {
5998
+ id,
5999
+ overrideAccess: true,
6000
+ req
6001
+ });
6002
+ }
6003
+ await dbDelete(payload$1, slugs.supportClients, {
6004
+ id: clientId,
6005
+ overrideAccess: true,
6006
+ req
6007
+ });
6008
+ if (startedTransaction) await payload.commitTransaction(req);
6009
+ } catch (err) {
6010
+ if (startedTransaction) await payload.killTransaction(req);
6011
+ throw err;
5714
6012
  }
5715
- await dbDelete(payload, slugs.satisfactionSurveys, {
5716
- where: { client: { equals: clientId } },
5717
- overrideAccess: true
5718
- });
5719
- await dbDelete(payload, slugs.chatMessages, {
5720
- where: { client: { equals: clientId } },
5721
- overrideAccess: true
5722
- });
5723
- await dbDelete(payload, slugs.supportClients, {
5724
- id: clientId,
5725
- overrideAccess: true
5726
- });
5727
6013
  const headers = new Headers({ "Content-Type": "application/json" });
5728
6014
  const secure = process.env.NODE_ENV === "production";
5729
6015
  headers.append(
@@ -5966,7 +6252,7 @@ function createImportConversationEndpoint(slugs, store) {
5966
6252
  if (!isAuthed) {
5967
6253
  return Response.json({ error: "Unauthorized" }, { status: 401 });
5968
6254
  }
5969
- const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
6255
+ const ip = clientIpRateKey(req);
5970
6256
  if (await importLimiter.check(ip, req)) {
5971
6257
  return Response.json({ error: "Rate limit exceeded. Maximum 10 imports per hour." }, { status: 429 });
5972
6258
  }
@@ -6174,6 +6460,21 @@ var WEBHOOK_URL_MESSAGES = {
6174
6460
  scheme_not_allowed: "Seules les URL https:// sont accept\xE9es.",
6175
6461
  private_host: "Les adresses priv\xE9es, loopback et link-local sont interdites (SSRF)."
6176
6462
  };
6463
+ var MAX_PUSH_ENDPOINT_LENGTH = 2048;
6464
+ function validatePushEndpoint(raw) {
6465
+ if (typeof raw !== "string" || !raw.trim() || raw.length > MAX_PUSH_ENDPOINT_LENGTH) {
6466
+ return { ok: false, reason: "invalid_url" };
6467
+ }
6468
+ let url;
6469
+ try {
6470
+ url = new URL(raw.trim());
6471
+ } catch {
6472
+ return { ok: false, reason: "invalid_url" };
6473
+ }
6474
+ if (url.protocol !== "https:") return { ok: false, reason: "scheme_not_allowed" };
6475
+ if (isBlockedHost(url.hostname)) return { ok: false, reason: "private_host" };
6476
+ return { ok: true, url };
6477
+ }
6177
6478
  var LOOKUP_TIMEOUT_MS = 5e3;
6178
6479
  var LOOKUP_TIMED_OUT = /* @__PURE__ */ Symbol("lookup-timed-out");
6179
6480
  var NONEXISTENT_HOST_CODES = /* @__PURE__ */ new Set(["ENOTFOUND", "ENODATA", "NOTFOUND"]);
@@ -6629,6 +6930,15 @@ async function sendPushToUser(payload, slugs, userId, notification) {
6629
6930
  for (const s of subs.docs) {
6630
6931
  const row = s;
6631
6932
  if (!row.endpoint || !row.p256dh || !row.auth) continue;
6933
+ const check = validatePushEndpoint(row.endpoint);
6934
+ if (!check.ok || !check.url) {
6935
+ console.warn("[support] Push endpoint refused (unsafe URL):", check.reason);
6936
+ continue;
6937
+ }
6938
+ if (!await assertPublicHost(check.url.hostname)) {
6939
+ console.warn("[support] Push endpoint refused (host resolves to a private address)");
6940
+ continue;
6941
+ }
6632
6942
  try {
6633
6943
  await webpush__default.default.sendNotification(
6634
6944
  { endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
@@ -6679,7 +6989,14 @@ function createPushSubscribeEndpoint(slugs) {
6679
6989
  if (!endpoint || !p256dh || !auth) {
6680
6990
  return Response.json({ error: "subscription invalide (endpoint + keys requis)." }, { status: 400 });
6681
6991
  }
6682
- const data = { user: req.user.id, endpoint, p256dh, auth, userAgent: req.headers.get("user-agent") || "" };
6992
+ const endpointCheck = validatePushEndpoint(endpoint);
6993
+ if (!endpointCheck.ok) {
6994
+ return Response.json(
6995
+ { error: WEBHOOK_URL_MESSAGES[endpointCheck.reason || "invalid_url"] },
6996
+ { status: 400 }
6997
+ );
6998
+ }
6999
+ const data = { user: req.user.id, endpoint, p256dh, auth, userAgent: (req.headers.get("user-agent") || "").slice(0, 256) };
6683
7000
  const existing = await dbFind(req.payload, slugs.pushSubscriptions, { where: { endpoint: { equals: endpoint } }, limit: 1, depth: 0, overrideAccess: true });
6684
7001
  if (existing.docs.length > 0) {
6685
7002
  await dbUpdate(req.payload, slugs.pushSubscriptions, { id: existing.docs[0].id, data, overrideAccess: true });
@@ -8335,9 +8652,11 @@ function createCascadeDelete(slugs) {
8335
8652
  return async ({ id, req }) => {
8336
8653
  const collections = [slugs.ticketMessages, slugs.ticketActivityLog, slugs.timeEntries, slugs.satisfactionSurveys];
8337
8654
  for (const slug of collections) {
8655
+ if (!req.payload.collections?.[slug]) continue;
8338
8656
  await dbDelete(req.payload, slug, {
8339
8657
  where: { ticket: { equals: id } },
8340
- overrideAccess: true
8658
+ overrideAccess: true,
8659
+ req
8341
8660
  });
8342
8661
  }
8343
8662
  };
@@ -9825,7 +10144,7 @@ function createSupportClientsCollection(slugs, options) {
9825
10144
  }
9826
10145
 
9827
10146
  // src/collections/TimeEntries.ts
9828
- async function recalculateTicketTime(payload, slugs, ticketId) {
10147
+ async function recalculateTicketTime(payload, slugs, ticketId, req) {
9829
10148
  let totalMinutes = 0;
9830
10149
  let page = 1;
9831
10150
  let hasMore = true;
@@ -9837,7 +10156,8 @@ async function recalculateTicketTime(payload, slugs, ticketId) {
9837
10156
  page,
9838
10157
  depth: 0,
9839
10158
  overrideAccess: true,
9840
- select: { duration: true }
10159
+ select: { duration: true },
10160
+ req
9841
10161
  });
9842
10162
  for (const entry of entries.docs) {
9843
10163
  totalMinutes += entry.duration || 0;
@@ -9849,14 +10169,15 @@ async function recalculateTicketTime(payload, slugs, ticketId) {
9849
10169
  collection: slugs.tickets,
9850
10170
  id: ticketId,
9851
10171
  data: { totalTimeMinutes: totalMinutes },
9852
- overrideAccess: true
10172
+ overrideAccess: true,
10173
+ req
9853
10174
  });
9854
10175
  }
9855
10176
  function createRecalculateTicketTime(slugs) {
9856
10177
  return async ({ doc, req }) => {
9857
10178
  if (!doc.ticket) return;
9858
10179
  const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
9859
- await recalculateTicketTime(req.payload, slugs, ticketId);
10180
+ await recalculateTicketTime(req.payload, slugs, ticketId, req);
9860
10181
  };
9861
10182
  }
9862
10183
  function createRecalculateTicketTimeOnDelete(slugs) {
@@ -9864,7 +10185,7 @@ function createRecalculateTicketTimeOnDelete(slugs) {
9864
10185
  if (!doc?.ticket) return;
9865
10186
  const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
9866
10187
  try {
9867
- await recalculateTicketTime(req.payload, slugs, ticketId);
10188
+ await recalculateTicketTime(req.payload, slugs, ticketId, req);
9868
10189
  } catch (err) {
9869
10190
  console.error("[support] Failed to recalculate ticket time after delete:", err);
9870
10191
  }
@@ -11735,8 +12056,30 @@ function supportPlugin(config) {
11735
12056
  rateLimitStore,
11736
12057
  capabilities: config?.capabilities
11737
12058
  });
12059
+ const retention = config?.retention === false ? void 0 : config?.retention;
12060
+ const retentionEnabled = Boolean(retention);
12061
+ const purgeTask = {
12062
+ slug: PURGE_LOGS_TASK_SLUG,
12063
+ label: "Support \u2014 purge des journaux",
12064
+ schedule: [
12065
+ {
12066
+ cron: retention?.cron ?? DEFAULT_RETENTION.cron,
12067
+ queue: retention?.queue ?? DEFAULT_RETENTION.queue
12068
+ }
12069
+ ],
12070
+ handler: async ({ req }) => {
12071
+ const result = await runScheduledPurge(req.payload, slugs, retention, req);
12072
+ return { output: result };
12073
+ }
12074
+ };
12075
+ const existingJobs = incomingConfig.jobs;
12076
+ const jobs = retentionEnabled ? {
12077
+ ...existingJobs,
12078
+ tasks: [...existingJobs?.tasks ?? [], purgeTask]
12079
+ } : existingJobs;
11738
12080
  return {
11739
12081
  ...incomingConfig,
12082
+ jobs,
11740
12083
  // Publish the resolved staff collection so the server-side readers share
11741
12084
  // ONE source of truth with the writers. `requireAdmin` compares against
11742
12085
  // `slugs.users`; the `payload-preferences` reads used to scope themselves
@@ -11763,11 +12106,13 @@ function supportPlugin(config) {
11763
12106
 
11764
12107
  exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
11765
12108
  exports.DEFAULT_INBOUND_EMAIL_LIMITS = DEFAULT_INBOUND_EMAIL_LIMITS;
12109
+ exports.DEFAULT_RETENTION = DEFAULT_RETENTION;
11766
12110
  exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
11767
12111
  exports.DEFAULT_SLUGS = DEFAULT_SLUGS;
11768
12112
  exports.DEFAULT_TICKETING_FEATURES = DEFAULT_TICKETING_FEATURES;
11769
12113
  exports.DEFAULT_USER_PREFS = DEFAULT_USER_PREFS;
11770
12114
  exports.MemoryRateLimitStore = MemoryRateLimitStore;
12115
+ exports.PURGE_LOGS_TASK_SLUG = PURGE_LOGS_TASK_SLUG;
11771
12116
  exports.PayloadRateLimitStore = PayloadRateLimitStore;
11772
12117
  exports.RateLimiter = RateLimiter;
11773
12118
  exports.calculateBusinessHoursDeadline = calculateBusinessHoursDeadline;
@@ -11800,10 +12145,13 @@ exports.dispatchWebhook = dispatchWebhook;
11800
12145
  exports.generateTicketSynthesis = generateTicketSynthesis;
11801
12146
  exports.normalizeFeatures = normalizeFeatures;
11802
12147
  exports.projectAutoClose = projectAutoClose;
12148
+ exports.purgeOlderThan = purgeOlderThan;
12149
+ exports.purgeableCollections = purgeableCollections;
11803
12150
  exports.readSupportSettings = readSupportSettings;
11804
12151
  exports.readSupportSettingsState = readSupportSettingsState;
11805
12152
  exports.readUserPrefs = readUserPrefs;
11806
12153
  exports.resolveSlugs = resolveSlugs;
12154
+ exports.runScheduledPurge = runScheduledPurge;
11807
12155
  exports.stripProjectedFeatures = stripProjectedFeatures;
11808
12156
  exports.supportPlugin = supportPlugin;
11809
12157
  exports.validateInboundEmailPayload = validateInboundEmailPayload;