@consilioweb/payload-support 2.0.0 → 3.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 (222) hide show
  1. package/README.md +656 -355
  2. package/dist/components/RichTextEditor/index.d.ts +19 -0
  3. package/dist/components/TicketConversation/RewriteDropdown.d.ts +6 -0
  4. package/dist/components/TicketConversation/SkeletonText.d.ts +4 -0
  5. package/dist/components/TicketConversation/components/AISummaryPanel.d.ts +14 -0
  6. package/dist/components/TicketConversation/components/ActionPanels.d.ts +47 -0
  7. package/dist/components/TicketConversation/components/ActivityLog.d.ts +5 -0
  8. package/dist/components/TicketConversation/components/ClientBar.d.ts +5 -0
  9. package/dist/components/TicketConversation/components/ClientHistory.d.ts +24 -0
  10. package/dist/components/TicketConversation/components/CodeBlock.d.ts +26 -0
  11. package/dist/components/TicketConversation/components/CodeBlockInserter.d.ts +12 -0
  12. package/dist/components/TicketConversation/components/QuickActions.d.ts +32 -0
  13. package/dist/components/TicketConversation/components/TicketHeader.d.ts +19 -0
  14. package/dist/components/TicketConversation/components/TimeTrackingPanel.d.ts +24 -0
  15. package/dist/components/TicketConversation/config.d.ts +10 -0
  16. package/dist/components/TicketConversation/config.js +1 -40
  17. package/dist/components/TicketConversation/constants.d.ts +56 -0
  18. package/dist/components/TicketConversation/context.d.ts +53 -0
  19. package/dist/components/TicketConversation/hooks/useAI.d.ts +21 -0
  20. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.d.ts +3 -0
  21. package/dist/components/TicketConversation/hooks/useFeatures.d.ts +23 -0
  22. package/dist/components/TicketConversation/hooks/useFeatures.js +61 -0
  23. package/dist/components/TicketConversation/hooks/useMessageActions.d.ts +20 -0
  24. package/dist/components/TicketConversation/hooks/useReply.d.ts +27 -0
  25. package/dist/components/TicketConversation/hooks/useTicketActions.d.ts +58 -0
  26. package/dist/components/TicketConversation/hooks/useTimeTracking.d.ts +18 -0
  27. package/dist/components/TicketConversation/hooks/useTranslation.d.ts +33 -0
  28. package/dist/components/TicketConversation/hooks/useTranslation.js +22 -3
  29. package/dist/components/TicketConversation/index.d.ts +4 -0
  30. package/dist/components/TicketConversation/index.js +2 -2
  31. package/dist/components/TicketConversation/types.d.ts +56 -0
  32. package/dist/components/TicketConversation/utils.d.ts +2 -0
  33. package/dist/index.cjs +392 -320
  34. package/dist/index.d.cts +100 -3
  35. package/dist/index.d.ts +100 -3
  36. package/dist/index.js +388 -321
  37. package/dist/utils/features.d.ts +123 -0
  38. package/dist/utils/features.js +147 -0
  39. package/dist/views/BillingView/client.d.ts +2 -0
  40. package/dist/views/BillingView/index.d.ts +4 -0
  41. package/dist/views/ChatView/client.d.ts +2 -0
  42. package/dist/views/ChatView/client.js +9 -9
  43. package/dist/views/ChatView/index.d.ts +4 -0
  44. package/dist/views/CrmView/client.d.ts +2 -0
  45. package/dist/views/CrmView/client.js +6 -9
  46. package/dist/views/CrmView/index.d.ts +4 -0
  47. package/dist/views/EmailTrackingView/client.d.ts +2 -0
  48. package/dist/views/EmailTrackingView/index.d.ts +4 -0
  49. package/dist/views/ImportConversationView/client.d.ts +2 -0
  50. package/dist/views/ImportConversationView/index.d.ts +4 -0
  51. package/dist/views/LogsView/client.d.ts +2 -0
  52. package/dist/views/LogsView/index.d.ts +4 -0
  53. package/dist/views/NewTicketView/client.d.ts +2 -0
  54. package/dist/views/NewTicketView/index.d.ts +4 -0
  55. package/dist/views/PendingEmailsView/client.d.ts +2 -0
  56. package/dist/views/PendingEmailsView/index.d.ts +4 -0
  57. package/dist/views/SupportDashboardView/client.d.ts +2 -0
  58. package/dist/views/SupportDashboardView/client.js +1 -1
  59. package/dist/views/SupportDashboardView/index.d.ts +4 -0
  60. package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
  61. package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
  62. package/dist/views/TicketDetailView/client.d.ts +2 -0
  63. package/dist/views/TicketDetailView/client.js +2 -2
  64. package/dist/views/TicketDetailView/constants.d.ts +13 -0
  65. package/dist/views/TicketDetailView/helpers.d.ts +2 -0
  66. package/dist/views/TicketDetailView/index.d.ts +4 -0
  67. package/dist/views/TicketDetailView/types.d.ts +40 -0
  68. package/dist/views/TicketInboxView/client.d.ts +2 -0
  69. package/dist/views/TicketInboxView/client.js +5 -5
  70. package/dist/views/TicketInboxView/index.d.ts +4 -0
  71. package/dist/views/TicketingSettingsView/client.d.ts +2 -0
  72. package/dist/views/TicketingSettingsView/client.js +6 -5
  73. package/dist/views/TicketingSettingsView/index.d.ts +4 -0
  74. package/dist/views/TimeDashboardView/client.d.ts +2 -0
  75. package/dist/views/TimeDashboardView/index.d.ts +4 -0
  76. package/dist/views/shared/AdminViewHeader.d.ts +13 -0
  77. package/dist/views/shared/ErrorBoundary.d.ts +17 -0
  78. package/dist/views/shared/Skeleton.d.ts +21 -0
  79. package/dist/views/shared/StatusPill.d.ts +8 -0
  80. package/dist/views/shared/adminTokens.d.ts +19 -0
  81. package/dist/views/shared/config.d.ts +8 -0
  82. package/dist/views/shared/config.js +1 -40
  83. package/dist/views/shared/dateLocale.d.ts +5 -0
  84. package/dist/views/shared/index.d.ts +12 -0
  85. package/dist/views/shared/index.js +2 -1
  86. package/dist/views/shared/locales/en.json +708 -0
  87. package/dist/views/shared/locales/fr.json +708 -0
  88. package/dist/views/shared/sla.d.ts +14 -0
  89. package/dist/views.d.ts +13 -13
  90. package/package.json +26 -41
  91. package/src/collections/SupportClients.ts +29 -0
  92. package/src/collections/TicketCollaborators.ts +6 -1
  93. package/src/collections/TicketMessages.ts +46 -24
  94. package/src/collections/Tickets.ts +117 -79
  95. package/src/collections/TimeEntries.ts +57 -32
  96. package/src/components/TicketConversation/config.ts +16 -80
  97. package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
  98. package/src/components/TicketConversation/hooks/useTranslation.ts +38 -4
  99. package/src/components/TicketConversation/index.tsx +2 -2
  100. package/src/endpoints/ai.ts +2 -1
  101. package/src/endpoints/client-intelligence.ts +2 -1
  102. package/src/endpoints/process-scheduled.ts +13 -8
  103. package/src/endpoints/round-robin-config.ts +30 -20
  104. package/src/endpoints/settings.ts +28 -77
  105. package/src/index.ts +6 -2
  106. package/src/portal/LiveChat.tsx +33 -22
  107. package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
  108. package/src/types/lucide-react.d.ts +10 -2
  109. package/src/utils/aiAgent.ts +2 -1
  110. package/src/utils/aiProvider.ts +21 -0
  111. package/src/utils/features.ts +297 -0
  112. package/src/utils/fireWebhooks.ts +6 -0
  113. package/src/utils/generateTicketSynthesis.ts +2 -1
  114. package/src/utils/index.ts +4 -2
  115. package/src/utils/readSettings.ts +82 -9
  116. package/src/utils/webhookDispatcher.ts +19 -7
  117. package/src/views/ChatView/client.tsx +13 -9
  118. package/src/views/CrmView/client.tsx +10 -11
  119. package/src/views/SupportDashboardView/client.tsx +1 -1
  120. package/src/views/TicketDetailView/client.tsx +2 -2
  121. package/src/views/TicketInboxView/client.tsx +10 -5
  122. package/src/views/TicketingSettingsView/client.tsx +9 -7
  123. package/src/views/shared/config.ts +14 -80
  124. package/src/views/shared/index.ts +3 -1
  125. package/src/views/shared/locales/en.json +2 -2
  126. package/src/views/shared/locales/fr.json +2 -2
  127. package/dist/components/RichTextEditor/index.cjs +0 -279
  128. package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
  129. package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
  130. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
  131. package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
  132. package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
  133. package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
  134. package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
  135. package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
  136. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
  137. package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
  138. package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
  139. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
  140. package/dist/components/TicketConversation/config.cjs +0 -44
  141. package/dist/components/TicketConversation/constants.cjs +0 -127
  142. package/dist/components/TicketConversation/context.cjs +0 -13
  143. package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
  144. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
  145. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
  146. package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
  147. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
  148. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
  149. package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -78
  150. package/dist/components/TicketConversation/index.cjs +0 -1114
  151. package/dist/components/TicketConversation/types.cjs +0 -2
  152. package/dist/components/TicketConversation/utils.cjs +0 -27
  153. package/dist/views/BillingView/client.cjs +0 -469
  154. package/dist/views/BillingView/index.cjs +0 -34
  155. package/dist/views/ChatView/client.cjs +0 -301
  156. package/dist/views/ChatView/index.cjs +0 -34
  157. package/dist/views/CrmView/client.cjs +0 -458
  158. package/dist/views/CrmView/index.cjs +0 -34
  159. package/dist/views/EmailTrackingView/client.cjs +0 -171
  160. package/dist/views/EmailTrackingView/index.cjs +0 -34
  161. package/dist/views/ImportConversationView/client.cjs +0 -238
  162. package/dist/views/ImportConversationView/index.cjs +0 -34
  163. package/dist/views/LogsView/client.cjs +0 -148
  164. package/dist/views/LogsView/index.cjs +0 -32
  165. package/dist/views/NewTicketView/client.cjs +0 -213
  166. package/dist/views/NewTicketView/index.cjs +0 -32
  167. package/dist/views/PendingEmailsView/client.cjs +0 -471
  168. package/dist/views/PendingEmailsView/index.cjs +0 -34
  169. package/dist/views/SupportDashboardView/client.cjs +0 -437
  170. package/dist/views/SupportDashboardView/index.cjs +0 -34
  171. package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
  172. package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
  173. package/dist/views/TicketDetailView/client.cjs +0 -1412
  174. package/dist/views/TicketDetailView/constants.cjs +0 -20
  175. package/dist/views/TicketDetailView/helpers.cjs +0 -23
  176. package/dist/views/TicketDetailView/index.cjs +0 -34
  177. package/dist/views/TicketDetailView/types.cjs +0 -2
  178. package/dist/views/TicketInboxView/client.cjs +0 -359
  179. package/dist/views/TicketInboxView/index.cjs +0 -32
  180. package/dist/views/TicketingSettingsView/client.cjs +0 -740
  181. package/dist/views/TicketingSettingsView/index.cjs +0 -34
  182. package/dist/views/TimeDashboardView/client.cjs +0 -180
  183. package/dist/views/TimeDashboardView/index.cjs +0 -34
  184. package/dist/views/shared/AdminViewHeader.cjs +0 -68
  185. package/dist/views/shared/ErrorBoundary.cjs +0 -55
  186. package/dist/views/shared/Skeleton.cjs +0 -77
  187. package/dist/views/shared/StatusPill.cjs +0 -47
  188. package/dist/views/shared/adminTokens.cjs +0 -34
  189. package/dist/views/shared/config.cjs +0 -44
  190. package/dist/views/shared/dateLocale.cjs +0 -5
  191. package/dist/views/shared/index.cjs +0 -72
  192. package/dist/views/shared/sla.cjs +0 -37
  193. package/dist/views.cjs +0 -86
  194. package/dist/views.d.cts +0 -13
  195. package/src/__tests__/aiSummaryRendering.test.ts +0 -21
  196. package/src/__tests__/authResponses.test.ts +0 -69
  197. package/src/__tests__/capabilities.test.ts +0 -71
  198. package/src/__tests__/generateTrackingToken.test.ts +0 -67
  199. package/src/__tests__/integration/ai-agent.test.ts +0 -46
  200. package/src/__tests__/integration/auth.test.ts +0 -43
  201. package/src/__tests__/integration/automation-rules.test.ts +0 -68
  202. package/src/__tests__/integration/buildTestPayload.ts +0 -52
  203. package/src/__tests__/integration/channels.test.ts +0 -52
  204. package/src/__tests__/integration/core-behaviors.test.ts +0 -81
  205. package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
  206. package/src/__tests__/integration/digest.test.ts +0 -48
  207. package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
  208. package/src/__tests__/integration/isolation.test.ts +0 -81
  209. package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
  210. package/src/__tests__/integration/nps.test.ts +0 -37
  211. package/src/__tests__/integration/per-team-sla.test.ts +0 -44
  212. package/src/__tests__/integration/push.test.ts +0 -75
  213. package/src/__tests__/integration/sanitization.test.ts +0 -35
  214. package/src/__tests__/integration/sla-pause.test.ts +0 -39
  215. package/src/__tests__/integration/smoke.test.ts +0 -26
  216. package/src/__tests__/integration/snooze.test.ts +0 -68
  217. package/src/__tests__/integration/teams.test.ts +0 -38
  218. package/src/__tests__/rateLimiter.test.ts +0 -83
  219. package/src/__tests__/sanitizeHtml.test.ts +0 -165
  220. package/src/__tests__/sla.test.ts +0 -258
  221. package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
  222. package/src/__tests__/webhookSecurity.test.ts +0 -58
package/dist/index.js CHANGED
@@ -368,14 +368,81 @@ function dbDelete(payload, slug, options) {
368
368
  return payload.delete({ collection: slug, ...options });
369
369
  }
370
370
 
371
+ // src/utils/features.ts
372
+ var DEFAULT_TICKETING_FEATURES = {
373
+ timeTracking: true,
374
+ ai: true,
375
+ satisfaction: true,
376
+ chat: true,
377
+ emailTracking: true,
378
+ canned: true,
379
+ merge: true,
380
+ snooze: true,
381
+ externalMessages: true,
382
+ clientHistory: true,
383
+ activityLog: true,
384
+ splitTicket: true,
385
+ scheduledReplies: true,
386
+ autoClose: true,
387
+ autoCloseDays: 7,
388
+ roundRobin: false
389
+ };
390
+ var BOOLEAN_FEATURE_KEYS = [
391
+ "timeTracking",
392
+ "ai",
393
+ "satisfaction",
394
+ "chat",
395
+ "emailTracking",
396
+ "canned",
397
+ "merge",
398
+ "snooze",
399
+ "externalMessages",
400
+ "clientHistory",
401
+ "activityLog",
402
+ "splitTicket",
403
+ "scheduledReplies",
404
+ "autoClose",
405
+ "roundRobin"
406
+ ];
407
+ var PROJECTED_FEATURE_KEYS = ["autoClose", "autoCloseDays"];
408
+ function normalizeFeatures(raw) {
409
+ const out = { ...DEFAULT_TICKETING_FEATURES };
410
+ if (!raw || typeof raw !== "object") return out;
411
+ const src = raw;
412
+ for (const key of BOOLEAN_FEATURE_KEYS) {
413
+ if (typeof src[key] === "boolean") out[key] = src[key];
414
+ }
415
+ const days = src.autoCloseDays;
416
+ if (typeof days === "number" && Number.isFinite(days) && days >= 1) {
417
+ out.autoCloseDays = Math.floor(days);
418
+ }
419
+ return out;
420
+ }
421
+ function projectAutoClose(features, autoClose) {
422
+ if (!autoClose) return features;
423
+ return {
424
+ ...features,
425
+ autoClose: typeof autoClose.enabled === "boolean" ? autoClose.enabled : features.autoClose,
426
+ autoCloseDays: typeof autoClose.daysBeforeClose === "number" && autoClose.daysBeforeClose >= 1 ? Math.floor(autoClose.daysBeforeClose) : features.autoCloseDays
427
+ };
428
+ }
429
+ function stripProjectedFeatures(features) {
430
+ const out = { ...features };
431
+ for (const key of PROJECTED_FEATURE_KEYS) delete out[key];
432
+ return out;
433
+ }
434
+
371
435
  // src/utils/readSettings.ts
372
- var PREF_KEY = "support-settings";
436
+ var SUPPORT_SETTINGS_PREF_KEY = "support-settings";
437
+ var PREF_KEY = SUPPORT_SETTINGS_PREF_KEY;
373
438
  var USER_PREFS_KEY_PREFIX = "support-user-prefs";
439
+ var LEGACY_ROUND_ROBIN_KEY = "support-round-robin";
374
440
  var DEFAULT_SETTINGS = {
375
441
  email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
376
442
  ai: { provider: "anthropic", model: "claude-haiku-4-5-20251001", enableSentiment: true, enableSynthesis: true, enableSuggestion: true, enableRewrite: true },
377
443
  sla: { firstResponseMinutes: 120, resolutionMinutes: 1440, businessHoursOnly: true, escalationEmail: "" },
378
- autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
444
+ autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
445
+ features: { ...DEFAULT_TICKETING_FEATURES }
379
446
  };
380
447
  var DEFAULT_USER_PREFS = {
381
448
  locale: "fr",
@@ -386,32 +453,71 @@ var SETTINGS_TTL_MS = 6e4;
386
453
  function invalidateSupportSettingsCache() {
387
454
  settingsCache = null;
388
455
  }
389
- async function readSupportSettings(payload) {
456
+ function mergeSupportSettings(stored, base = DEFAULT_SETTINGS) {
457
+ const autoClose = { ...base.autoClose, ...stored?.autoClose };
458
+ return {
459
+ email: { ...base.email, ...stored?.email },
460
+ ai: { ...base.ai, ...stored?.ai },
461
+ sla: { ...base.sla, ...stored?.sla },
462
+ autoClose,
463
+ // `autoClose` / `autoCloseDays` are projections of the block above, so they
464
+ // are recomputed here rather than trusted from whatever was persisted.
465
+ features: projectAutoClose(
466
+ normalizeFeatures({ ...base.features, ...stored?.features }),
467
+ autoClose
468
+ )
469
+ };
470
+ }
471
+ async function readSupportSettingsState(payload) {
390
472
  if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
391
473
  return settingsCache.value;
392
474
  }
393
- let value = { ...DEFAULT_SETTINGS };
475
+ let value = {
476
+ settings: mergeSupportSettings(null),
477
+ featuresConfigured: false
478
+ };
394
479
  try {
395
480
  const prefs = await dbFind(payload, "payload-preferences", {
396
481
  where: { key: { equals: PREF_KEY } },
482
+ // The upsert is scoped per admin user, so several rows can share the key.
483
+ // Sorting makes "last write wins" deterministic instead of arbitrary.
484
+ sort: "-updatedAt",
397
485
  limit: 1,
398
486
  depth: 0,
399
487
  overrideAccess: true
400
488
  });
401
489
  if (prefs.docs.length > 0) {
402
490
  const stored = prefs.docs[0].value;
403
- value = {
404
- email: { ...DEFAULT_SETTINGS.email, ...stored.email },
405
- ai: { ...DEFAULT_SETTINGS.ai, ...stored.ai },
406
- sla: { ...DEFAULT_SETTINGS.sla, ...stored.sla },
407
- autoClose: { ...DEFAULT_SETTINGS.autoClose, ...stored.autoClose }
408
- };
491
+ const featuresConfigured = !!stored.features && typeof stored.features === "object";
492
+ const settings = mergeSupportSettings(stored);
493
+ if (!featuresConfigured) {
494
+ settings.features.roundRobin = await readLegacyRoundRobin(payload);
495
+ }
496
+ value = { settings, featuresConfigured };
409
497
  }
410
498
  } catch {
411
499
  }
412
500
  settingsCache = { value, ts: Date.now() };
413
501
  return value;
414
502
  }
503
+ async function readSupportSettings(payload) {
504
+ return (await readSupportSettingsState(payload)).settings;
505
+ }
506
+ async function readLegacyRoundRobin(payload) {
507
+ try {
508
+ const prefs = await dbFind(payload, "payload-preferences", {
509
+ where: { key: { equals: LEGACY_ROUND_ROBIN_KEY } },
510
+ limit: 1,
511
+ depth: 0,
512
+ overrideAccess: true
513
+ });
514
+ if (prefs.docs.length > 0) {
515
+ return prefs.docs[0].value?.enabled === true;
516
+ }
517
+ } catch {
518
+ }
519
+ return DEFAULT_TICKETING_FEATURES.roundRobin;
520
+ }
415
521
  async function readUserPrefs(payload, userId) {
416
522
  try {
417
523
  const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
@@ -432,13 +538,20 @@ async function readUserPrefs(payload, userId) {
432
538
  }
433
539
  return { ...DEFAULT_USER_PREFS };
434
540
  }
541
+ function resolveOllamaBaseUrl() {
542
+ const baseURL = process.env.OLLAMA_API_URL;
543
+ if (!baseURL) {
544
+ throw new APIError("provider 'ollama' requires OLLAMA_API_URL", 500);
545
+ }
546
+ return baseURL;
547
+ }
435
548
 
436
549
  // src/endpoints/ai.ts
437
550
  async function getClient(aiSettings) {
438
551
  const moduleName = "@anthropic-ai/sdk";
439
552
  const { default: Anthropic } = await import(moduleName);
440
553
  if (aiSettings.provider === "ollama") {
441
- const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
554
+ const baseURL = resolveOllamaBaseUrl();
442
555
  return new Anthropic({ apiKey: "ollama", baseURL });
443
556
  }
444
557
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
@@ -637,7 +750,7 @@ async function applyAgentDecision(payload, slugs, ticketId, decision) {
637
750
  function getClient2(aiSettings) {
638
751
  const Anthropic = __require("@anthropic-ai/sdk").default;
639
752
  if (aiSettings.provider === "ollama") {
640
- return new Anthropic({ apiKey: "ollama", baseURL: process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1" });
753
+ return new Anthropic({ apiKey: "ollama", baseURL: resolveOllamaBaseUrl() });
641
754
  }
642
755
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
643
756
  }
@@ -737,7 +850,7 @@ async function getClient3(aiSettings) {
737
850
  const moduleName = "@anthropic-ai/sdk";
738
851
  const { default: Anthropic } = await import(moduleName);
739
852
  if (aiSettings.provider === "ollama") {
740
- const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
853
+ const baseURL = resolveOllamaBaseUrl();
741
854
  return new Anthropic({ apiKey: "ollama", baseURL });
742
855
  }
743
856
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
@@ -1573,25 +1686,6 @@ function createPresenceGetEndpoint(slugs) {
1573
1686
  }
1574
1687
 
1575
1688
  // src/endpoints/settings.ts
1576
- var PREF_KEY2 = "support-settings";
1577
- var DEFAULT_SUPPORT_SETTINGS = {
1578
- email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
1579
- ai: {
1580
- provider: "anthropic",
1581
- model: "claude-haiku-4-5-20251001",
1582
- enableSentiment: true,
1583
- enableSynthesis: true,
1584
- enableSuggestion: true,
1585
- enableRewrite: true
1586
- },
1587
- sla: {
1588
- firstResponseMinutes: 120,
1589
- resolutionMinutes: 1440,
1590
- businessHoursOnly: true,
1591
- escalationEmail: ""
1592
- },
1593
- autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
1594
- };
1595
1689
  function createSettingsGetEndpoint(slugs) {
1596
1690
  return {
1597
1691
  path: "/support/settings",
@@ -1600,23 +1694,8 @@ function createSettingsGetEndpoint(slugs) {
1600
1694
  try {
1601
1695
  const payload = req.payload;
1602
1696
  requireAdmin(req, slugs);
1603
- const prefs = await dbFind(payload, "payload-preferences", {
1604
- where: { key: { equals: PREF_KEY2 } },
1605
- limit: 1,
1606
- depth: 0,
1607
- overrideAccess: true
1608
- });
1609
- let settings = { ...DEFAULT_SUPPORT_SETTINGS };
1610
- if (prefs.docs.length > 0) {
1611
- const stored = prefs.docs[0].value;
1612
- settings = {
1613
- email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...stored.email },
1614
- ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...stored.ai },
1615
- sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...stored.sla },
1616
- autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...stored.autoClose }
1617
- };
1618
- }
1619
- return Response.json(settings);
1697
+ const { settings, featuresConfigured } = await readSupportSettingsState(payload);
1698
+ return Response.json({ ...settings, featuresConfigured });
1620
1699
  } catch (error) {
1621
1700
  const authResponse = handleAuthError(error);
1622
1701
  if (authResponse) return authResponse;
@@ -1635,30 +1714,31 @@ function createSettingsPostEndpoint(slugs) {
1635
1714
  const payload = req.payload;
1636
1715
  requireAdmin(req, slugs);
1637
1716
  const body = await req.json();
1638
- const merged = {
1639
- email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...body.email },
1640
- ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...body.ai },
1641
- sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...body.sla },
1642
- autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...body.autoClose }
1643
- };
1717
+ const { settings: current } = await readSupportSettingsState(payload);
1718
+ const merged = mergeSupportSettings(body, current);
1644
1719
  await payload.db.upsert({
1645
1720
  collection: "payload-preferences",
1646
1721
  data: {
1647
- key: PREF_KEY2,
1722
+ key: SUPPORT_SETTINGS_PREF_KEY,
1648
1723
  user: { relationTo: req.user.collection, value: req.user.id },
1649
- value: merged
1724
+ value: {
1725
+ ...merged,
1726
+ // Persist the flags without the two projected ones, so `features`
1727
+ // and the `autoClose` block can never drift apart.
1728
+ features: stripProjectedFeatures(merged.features)
1729
+ }
1650
1730
  },
1651
1731
  req: { payload, user: req.user },
1652
1732
  where: {
1653
1733
  and: [
1654
- { key: { equals: PREF_KEY2 } },
1734
+ { key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
1655
1735
  { "user.value": { equals: req.user.id } },
1656
1736
  { "user.relationTo": { equals: req.user.collection } }
1657
1737
  ]
1658
1738
  }
1659
1739
  });
1660
1740
  invalidateSupportSettingsCache();
1661
- return Response.json(merged);
1741
+ return Response.json({ ...merged, featuresConfigured: true });
1662
1742
  } catch (error) {
1663
1743
  const authResponse = handleAuthError(error);
1664
1744
  if (authResponse) return authResponse;
@@ -1670,7 +1750,7 @@ function createSettingsPostEndpoint(slugs) {
1670
1750
  }
1671
1751
 
1672
1752
  // src/endpoints/signature.ts
1673
- var PREF_KEY3 = "email-signature";
1753
+ var PREF_KEY2 = "email-signature";
1674
1754
  function createSignatureGetEndpoint(slugs) {
1675
1755
  return {
1676
1756
  path: "/support/signature",
@@ -1680,7 +1760,7 @@ function createSignatureGetEndpoint(slugs) {
1680
1760
  const payload = req.payload;
1681
1761
  requireAdmin(req, slugs);
1682
1762
  const prefs = await dbFind(payload, "payload-preferences", {
1683
- where: { key: { equals: `${PREF_KEY3}-${req.user.id}` } },
1763
+ where: { key: { equals: `${PREF_KEY2}-${req.user.id}` } },
1684
1764
  limit: 1,
1685
1765
  depth: 0,
1686
1766
  overrideAccess: true
@@ -1705,7 +1785,7 @@ function createSignaturePostEndpoint(slugs) {
1705
1785
  const payload = req.payload;
1706
1786
  requireAdmin(req, slugs);
1707
1787
  const { signature } = await req.json();
1708
- const key = `${PREF_KEY3}-${req.user.id}`;
1788
+ const key = `${PREF_KEY2}-${req.user.id}`;
1709
1789
  const existing = await dbFind(payload, "payload-preferences", {
1710
1790
  where: { key: { equals: key } },
1711
1791
  limit: 1,
@@ -1740,7 +1820,6 @@ function createSignaturePostEndpoint(slugs) {
1740
1820
  }
1741
1821
 
1742
1822
  // src/endpoints/round-robin-config.ts
1743
- var PREF_KEY4 = "support-round-robin";
1744
1823
  function createRoundRobinConfigGetEndpoint(slugs) {
1745
1824
  return {
1746
1825
  path: "/support/round-robin-config",
@@ -1749,14 +1828,8 @@ function createRoundRobinConfigGetEndpoint(slugs) {
1749
1828
  try {
1750
1829
  const payload = req.payload;
1751
1830
  requireAdmin(req, slugs);
1752
- const prefs = await dbFind(payload, "payload-preferences", {
1753
- where: { key: { equals: PREF_KEY4 } },
1754
- limit: 1,
1755
- depth: 0,
1756
- overrideAccess: true
1757
- });
1758
- const enabled = prefs.docs.length > 0 ? prefs.docs[0].value?.enabled === true : false;
1759
- return Response.json({ enabled });
1831
+ const { settings } = await readSupportSettingsState(payload);
1832
+ return Response.json({ enabled: settings.features.roundRobin });
1760
1833
  } catch (error) {
1761
1834
  const authResponse = handleAuthError(error);
1762
1835
  if (authResponse) return authResponse;
@@ -1775,28 +1848,31 @@ function createRoundRobinConfigPostEndpoint(slugs) {
1775
1848
  const payload = req.payload;
1776
1849
  requireAdmin(req, slugs);
1777
1850
  const { enabled } = await req.json();
1778
- const existing = await dbFind(payload, "payload-preferences", {
1779
- where: { key: { equals: PREF_KEY4 } },
1780
- limit: 1,
1781
- depth: 0,
1782
- overrideAccess: true
1783
- });
1851
+ const { settings: current } = await readSupportSettingsState(payload);
1852
+ const merged = mergeSupportSettings(
1853
+ { features: { ...current.features, roundRobin: !!enabled } },
1854
+ current
1855
+ );
1784
1856
  await payload.db.upsert({
1785
1857
  collection: "payload-preferences",
1786
1858
  data: {
1787
- key: PREF_KEY4,
1859
+ key: SUPPORT_SETTINGS_PREF_KEY,
1788
1860
  user: { relationTo: req.user.collection, value: req.user.id },
1789
- value: { enabled: !!enabled }
1861
+ value: {
1862
+ ...merged,
1863
+ features: stripProjectedFeatures(merged.features)
1864
+ }
1790
1865
  },
1791
1866
  req: { payload, user: req.user },
1792
1867
  where: {
1793
1868
  and: [
1794
- { key: { equals: PREF_KEY4 } },
1869
+ { key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
1795
1870
  { "user.value": { equals: req.user.id } },
1796
1871
  { "user.relationTo": { equals: req.user.collection } }
1797
1872
  ]
1798
1873
  }
1799
1874
  });
1875
+ invalidateSupportSettingsCache();
1800
1876
  return Response.json({ enabled: !!enabled });
1801
1877
  } catch (error) {
1802
1878
  const authResponse = handleAuthError(error);
@@ -4058,7 +4134,7 @@ function createInvoiceEndpoint(slugs) {
4058
4134
  function getClient4(aiSettings) {
4059
4135
  const Anthropic = __require("@anthropic-ai/sdk").default;
4060
4136
  if (aiSettings.provider === "ollama") {
4061
- const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
4137
+ const baseURL = resolveOllamaBaseUrl();
4062
4138
  return new Anthropic({ apiKey: "ollama", baseURL });
4063
4139
  }
4064
4140
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
@@ -5751,62 +5827,73 @@ function createImportConversationEndpoint(slugs, store) {
5751
5827
  }
5752
5828
  };
5753
5829
  }
5754
-
5755
- // src/utils/fireWebhooks.ts
5756
- async function fireWebhooks(payload, slugs, event, data) {
5830
+ function dispatchWebhook(data, event, payload, slugs) {
5831
+ const done = _dispatch(data, event, payload, slugs);
5832
+ return done;
5833
+ }
5834
+ async function _dispatch(data, event, payload, slugs) {
5757
5835
  try {
5758
- const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
5836
+ const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
5759
5837
  where: {
5760
- active: { equals: true },
5761
- events: { contains: event }
5838
+ and: [
5839
+ { active: { equals: true } },
5840
+ { events: { contains: event } }
5841
+ ]
5762
5842
  },
5763
5843
  limit: 50,
5764
5844
  depth: 0,
5765
5845
  overrideAccess: true
5766
5846
  });
5767
- if (endpoints.docs.length === 0) return;
5768
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5847
+ if (endpoints.length === 0) return;
5848
+ const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
5769
5849
  await Promise.allSettled(
5770
- endpoints.docs.map(async (endpoint) => {
5771
- try {
5772
- const res = await fetch(endpoint.url, {
5773
- method: "POST",
5774
- headers: {
5775
- "Content-Type": "application/json",
5776
- ...endpoint.secret ? { "X-Webhook-Secret": endpoint.secret } : {}
5777
- },
5778
- body: JSON.stringify({ event, data, timestamp }),
5779
- signal: AbortSignal.timeout(1e4)
5780
- });
5781
- try {
5782
- await dbUpdate(payload, slugs.webhookEndpoints, {
5783
- id: endpoint.id,
5784
- data: {
5785
- lastTriggeredAt: timestamp,
5786
- lastStatus: res.status
5787
- },
5788
- overrideAccess: true
5789
- });
5790
- } catch {
5791
- }
5792
- } catch (error) {
5793
- console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
5794
- try {
5795
- await dbUpdate(payload, slugs.webhookEndpoints, {
5796
- id: endpoint.id,
5797
- data: {
5798
- lastTriggeredAt: timestamp,
5799
- lastStatus: 0
5800
- },
5801
- overrideAccess: true
5802
- });
5803
- } catch {
5804
- }
5805
- }
5806
- })
5850
+ endpoints.map((endpoint) => _sendToEndpoint(endpoint, body, payload, slugs))
5807
5851
  );
5808
- } catch (error) {
5809
- console.error("[support] Failed to fire webhooks:", error);
5852
+ } catch (err) {
5853
+ console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
5854
+ }
5855
+ }
5856
+ async function _sendToEndpoint(endpoint, body, payload, slugs) {
5857
+ try {
5858
+ const headers = {
5859
+ "Content-Type": "application/json",
5860
+ "User-Agent": "PayloadSupport-Webhook/1.0"
5861
+ };
5862
+ if (endpoint.secret) {
5863
+ const signature = crypto3.createHmac("sha256", endpoint.secret).update(body).digest("hex");
5864
+ headers["X-Webhook-Signature"] = signature;
5865
+ }
5866
+ const response = await fetch(endpoint.url, {
5867
+ method: "POST",
5868
+ headers,
5869
+ body,
5870
+ signal: AbortSignal.timeout(1e4)
5871
+ // 10s timeout
5872
+ });
5873
+ await dbUpdate(payload, slugs.webhookEndpoints, {
5874
+ id: endpoint.id,
5875
+ data: {
5876
+ lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
5877
+ lastStatus: response.status
5878
+ },
5879
+ overrideAccess: true
5880
+ });
5881
+ if (!response.ok) {
5882
+ console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
5883
+ }
5884
+ } catch (err) {
5885
+ console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
5886
+ try {
5887
+ await dbUpdate(payload, slugs.webhookEndpoints, {
5888
+ id: endpoint.id,
5889
+ data: {
5890
+ lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
5891
+ lastStatus: 0
5892
+ },
5893
+ overrideAccess: true
5894
+ });
5895
+ } catch {
5896
+ }
5810
5897
  }
5811
5898
  }
5812
5899
 
@@ -5893,13 +5980,18 @@ function createProcessScheduledEndpoint(slugs) {
5893
5980
  overrideAccess: true
5894
5981
  });
5895
5982
  }
5896
- fireWebhooks(payload, slugs, "ticket_replied", {
5897
- ticketId,
5898
- messageId: msg.id,
5899
- authorType: msg.authorType,
5900
- scheduled: true,
5901
- body: msg.body?.length > 500 ? msg.body.slice(0, 500) + "..." : msg.body
5902
- });
5983
+ dispatchWebhook(
5984
+ {
5985
+ ticketId,
5986
+ messageId: msg.id,
5987
+ authorType: msg.authorType,
5988
+ scheduled: true,
5989
+ body: msg.body?.length > 500 ? msg.body.slice(0, 500) + "..." : msg.body
5990
+ },
5991
+ "ticket_replied",
5992
+ payload,
5993
+ slugs
5994
+ );
5903
5995
  results.processed++;
5904
5996
  } catch (err) {
5905
5997
  console.error(`[process-scheduled] Error processing message ${message.id}:`, err);
@@ -6869,74 +6961,6 @@ async function createAdminNotification(payload, data, collectionSlug = "admin-no
6869
6961
  console.error("[notification] Failed to create:", err);
6870
6962
  }
6871
6963
  }
6872
- function dispatchWebhook(data, event, payload, slugs) {
6873
- void _dispatch(data, event, payload, slugs);
6874
- }
6875
- async function _dispatch(data, event, payload, slugs) {
6876
- try {
6877
- const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
6878
- where: {
6879
- and: [
6880
- { active: { equals: true } },
6881
- { events: { contains: event } }
6882
- ]
6883
- },
6884
- limit: 50,
6885
- depth: 0,
6886
- overrideAccess: true
6887
- });
6888
- if (endpoints.length === 0) return;
6889
- const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
6890
- for (const endpoint of endpoints) {
6891
- void _sendToEndpoint(endpoint, body, payload, slugs);
6892
- }
6893
- } catch (err) {
6894
- console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
6895
- }
6896
- }
6897
- async function _sendToEndpoint(endpoint, body, payload, slugs) {
6898
- try {
6899
- const headers = {
6900
- "Content-Type": "application/json",
6901
- "User-Agent": "PayloadSupport-Webhook/1.0"
6902
- };
6903
- if (endpoint.secret) {
6904
- const signature = crypto3.createHmac("sha256", endpoint.secret).update(body).digest("hex");
6905
- headers["X-Webhook-Signature"] = signature;
6906
- }
6907
- const response = await fetch(endpoint.url, {
6908
- method: "POST",
6909
- headers,
6910
- body,
6911
- signal: AbortSignal.timeout(1e4)
6912
- // 10s timeout
6913
- });
6914
- await dbUpdate(payload, slugs.webhookEndpoints, {
6915
- id: endpoint.id,
6916
- data: {
6917
- lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
6918
- lastStatus: response.status
6919
- },
6920
- overrideAccess: true
6921
- });
6922
- if (!response.ok) {
6923
- console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
6924
- }
6925
- } catch (err) {
6926
- console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
6927
- try {
6928
- await dbUpdate(payload, slugs.webhookEndpoints, {
6929
- id: endpoint.id,
6930
- data: {
6931
- lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
6932
- lastStatus: 0
6933
- },
6934
- overrideAccess: true
6935
- });
6936
- } catch {
6937
- }
6938
- }
6939
- }
6940
6964
 
6941
6965
  // src/hooks/ticketStatusEmail.ts
6942
6966
  var STATUS_LABELS = {
@@ -7553,12 +7577,28 @@ function createAssignTicketNumber(slugs, options) {
7553
7577
  }
7554
7578
  function createAssignClientOnCreate(slugs) {
7555
7579
  return async ({ data, operation, req }) => {
7556
- if (operation === "create" && req.user?.collection === slugs.supportClients && !data.client) {
7580
+ if (operation === "create" && req.user?.collection === slugs.supportClients) {
7557
7581
  data.client = req.user.id;
7558
7582
  }
7559
7583
  return data;
7560
7584
  };
7561
7585
  }
7586
+ var CLIENT_CREATABLE_TICKET_FIELDS = ["subject", "category", "priority", "project"];
7587
+ function createRestrictClientCreate(slugs) {
7588
+ return async ({ data, operation, req }) => {
7589
+ if (operation !== "create") return data;
7590
+ if (req.user?.collection !== slugs.supportClients) return data;
7591
+ const incoming = data ?? {};
7592
+ const sanitized = {};
7593
+ for (const field2 of CLIENT_CREATABLE_TICKET_FIELDS) {
7594
+ if (incoming[field2] !== void 0) sanitized[field2] = incoming[field2];
7595
+ }
7596
+ sanitized.status = "open";
7597
+ sanitized.client = req.user.id;
7598
+ sanitized.source = "portal";
7599
+ return sanitized;
7600
+ };
7601
+ }
7562
7602
  var autoPaidAt = async ({ data, operation, originalDoc }) => {
7563
7603
  if (operation !== "update") return data;
7564
7604
  if (data.paymentStatus === "paid" && originalDoc?.paymentStatus !== "paid" && !data.paidAt) {
@@ -7584,16 +7624,8 @@ function createAutoAssignAdmin(slugs) {
7584
7624
  const { payload } = req;
7585
7625
  let roundRobinEnabled = false;
7586
7626
  try {
7587
- const prefs = await payload.find({
7588
- collection: "payload-preferences",
7589
- where: { key: { equals: "support-round-robin" } },
7590
- limit: 1,
7591
- depth: 0,
7592
- overrideAccess: true
7593
- });
7594
- if (prefs.docs.length > 0) {
7595
- roundRobinEnabled = prefs.docs[0].value?.enabled === true;
7596
- }
7627
+ const settings = await readSupportSettings(payload);
7628
+ roundRobinEnabled = settings.features.roundRobin === true;
7597
7629
  } catch (err) {
7598
7630
  console.warn("[support] Failed to read round-robin preferences:", err);
7599
7631
  }
@@ -7802,54 +7834,6 @@ function createNotifyClientOnResolve(slugs) {
7802
7834
  return doc;
7803
7835
  };
7804
7836
  }
7805
- function createFireTicketWebhooks(slugs) {
7806
- return async ({ doc, previousDoc, operation, req }) => {
7807
- const { payload } = req;
7808
- if (operation === "create") {
7809
- fireWebhooks(payload, slugs, "ticket_created", {
7810
- id: doc.id,
7811
- ticketNumber: doc.ticketNumber,
7812
- subject: doc.subject,
7813
- status: doc.status,
7814
- priority: doc.priority,
7815
- category: doc.category
7816
- });
7817
- return doc;
7818
- }
7819
- if (operation === "update" && previousDoc) {
7820
- if (previousDoc.status !== doc.status && doc.status === "resolved") {
7821
- fireWebhooks(payload, slugs, "ticket_resolved", {
7822
- id: doc.id,
7823
- ticketNumber: doc.ticketNumber,
7824
- subject: doc.subject,
7825
- previousStatus: previousDoc.status
7826
- });
7827
- const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
7828
- if (clientId) {
7829
- payload.update({
7830
- collection: "client-summaries",
7831
- where: { client: { equals: clientId } },
7832
- data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
7833
- // Force cache expiry
7834
- overrideAccess: true
7835
- }).catch(() => {
7836
- });
7837
- }
7838
- }
7839
- const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
7840
- const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
7841
- if (newAssigned && oldAssigned !== newAssigned) {
7842
- fireWebhooks(payload, slugs, "ticket_assigned", {
7843
- id: doc.id,
7844
- ticketNumber: doc.ticketNumber,
7845
- subject: doc.subject,
7846
- assignedTo: newAssigned
7847
- });
7848
- }
7849
- }
7850
- return doc;
7851
- };
7852
- }
7853
7837
  function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
7854
7838
  return async ({ doc, operation, req }) => {
7855
7839
  if (operation !== "create") return doc;
@@ -7870,21 +7854,66 @@ function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
7870
7854
  }
7871
7855
  function createDispatchWebhookOnTicket(slugs) {
7872
7856
  return async ({ doc, previousDoc, operation, req }) => {
7857
+ const { payload } = req;
7873
7858
  if (operation === "create") {
7874
7859
  dispatchWebhook(
7875
- { ticketId: doc.id, ticketNumber: doc.ticketNumber, subject: doc.subject },
7860
+ {
7861
+ ticketId: doc.id,
7862
+ id: doc.id,
7863
+ ticketNumber: doc.ticketNumber,
7864
+ subject: doc.subject,
7865
+ status: doc.status,
7866
+ priority: doc.priority,
7867
+ category: doc.category
7868
+ },
7876
7869
  "ticket_created",
7877
- req.payload,
7870
+ payload,
7878
7871
  slugs
7879
7872
  );
7873
+ return doc;
7880
7874
  }
7881
- if (operation === "update" && previousDoc?.status !== doc.status && doc.status === "resolved") {
7882
- dispatchWebhook(
7883
- { ticketId: doc.id, ticketNumber: doc.ticketNumber, subject: doc.subject },
7884
- "ticket_resolved",
7885
- req.payload,
7886
- slugs
7887
- );
7875
+ if (operation === "update" && previousDoc) {
7876
+ if (previousDoc.status !== doc.status && doc.status === "resolved") {
7877
+ dispatchWebhook(
7878
+ {
7879
+ ticketId: doc.id,
7880
+ id: doc.id,
7881
+ ticketNumber: doc.ticketNumber,
7882
+ subject: doc.subject,
7883
+ previousStatus: previousDoc.status
7884
+ },
7885
+ "ticket_resolved",
7886
+ payload,
7887
+ slugs
7888
+ );
7889
+ const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
7890
+ if (clientId) {
7891
+ payload.update({
7892
+ collection: "client-summaries",
7893
+ where: { client: { equals: clientId } },
7894
+ data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
7895
+ // Force cache expiry
7896
+ overrideAccess: true
7897
+ }).catch(() => {
7898
+ });
7899
+ }
7900
+ }
7901
+ const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
7902
+ const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
7903
+ if (newAssigned && oldAssigned !== newAssigned) {
7904
+ dispatchWebhook(
7905
+ {
7906
+ ticketId: doc.id,
7907
+ id: doc.id,
7908
+ ticketNumber: doc.ticketNumber,
7909
+ subject: doc.subject,
7910
+ assignedTo: newAssigned
7911
+ },
7912
+ "ticket_assigned",
7913
+ payload,
7914
+ slugs
7915
+ );
7916
+ }
7888
7917
  }
7889
7918
  return doc;
7890
7919
  };
@@ -8010,7 +8039,10 @@ function createTicketsCollection(slugs, options) {
8010
8039
  { label: "Paiement partiel", value: "partial" },
8011
8040
  { label: "Pay\xE9", value: "paid" }
8012
8041
  ],
8013
- access: { update: ({ req }) => req.user?.collection === "users" },
8042
+ // `slugs.users`, not the literal 'users': the users collection slug is
8043
+ // configurable, and a hard-coded one silently opens this field to everybody
8044
+ // on a host that renamed it.
8045
+ access: { update: ({ req }) => req.user?.collection === slugs.users },
8014
8046
  admin: { width: "50%", condition: (data) => !!data?.invoice }
8015
8047
  },
8016
8048
  {
@@ -8292,6 +8324,7 @@ function createTicketsCollection(slugs, options) {
8292
8324
  { name: "totalTimeMinutes", type: "number", defaultValue: 0, label: "Temps total (minutes)", admin: { readOnly: true, position: "sidebar" } }
8293
8325
  ],
8294
8326
  hooks: {
8327
+ beforeValidate: [createRestrictClientCreate(slugs)],
8295
8328
  beforeChange: [
8296
8329
  createAssignTicketNumber(slugs, options?.ticketNumber),
8297
8330
  createAssignClientOnCreate(slugs),
@@ -8312,7 +8345,6 @@ function createTicketsCollection(slugs, options) {
8312
8345
  createNotifyClientOnResolve(slugs),
8313
8346
  createTicketStatusEmail(slugs),
8314
8347
  createNotifyAdminOnNewTicket(slugs, notificationSlug),
8315
- createFireTicketWebhooks(slugs),
8316
8348
  createDispatchWebhookOnTicket(slugs),
8317
8349
  createApplyAutomationRules(slugs)
8318
8350
  ],
@@ -8493,6 +8525,21 @@ async function queueClientNotification(payload, slugs, item) {
8493
8525
  }
8494
8526
 
8495
8527
  // src/collections/TicketMessages.ts
8528
+ function createRestrictClientTicketTarget(slugs) {
8529
+ return async ({ data, operation, req, originalDoc }) => {
8530
+ if (req.user?.collection !== slugs.supportClients) return data;
8531
+ const raw = data.ticket ?? (operation === "update" ? originalDoc?.ticket : void 0);
8532
+ const targetId = raw && typeof raw === "object" ? raw.id : raw;
8533
+ if (targetId === void 0 || targetId === null || targetId === "") {
8534
+ throw new APIError("Ticket cible requis.", 400);
8535
+ }
8536
+ const accessible = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
8537
+ if (!accessible.some((id) => String(id) === String(targetId))) {
8538
+ throw new APIError("Ticket inaccessible.", 403);
8539
+ }
8540
+ return data;
8541
+ };
8542
+ }
8496
8543
  function createAssignAuthor(slugs) {
8497
8544
  return async ({ data, operation, req }) => {
8498
8545
  if (operation === "create" && req.user?.collection === slugs.supportClients) {
@@ -8727,21 +8774,6 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
8727
8774
  return doc;
8728
8775
  };
8729
8776
  }
8730
- function createFireMessageWebhooks(slugs) {
8731
- return async ({ doc, operation, req }) => {
8732
- if (operation !== "create") return doc;
8733
- if (doc.scheduledAt && !doc.scheduledSent) return doc;
8734
- if (doc.isInternal) return doc;
8735
- const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
8736
- fireWebhooks(req.payload, slugs, "ticket_replied", {
8737
- ticketId,
8738
- messageId: doc.id,
8739
- authorType: doc.authorType,
8740
- body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
8741
- });
8742
- return doc;
8743
- };
8744
- }
8745
8777
  function createDispatchWebhookOnReply(slugs) {
8746
8778
  return async ({ doc, operation, req }) => {
8747
8779
  if (operation !== "create") return doc;
@@ -8749,7 +8781,12 @@ function createDispatchWebhookOnReply(slugs) {
8749
8781
  if (doc.scheduledAt && !doc.scheduledSent) return doc;
8750
8782
  const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
8751
8783
  dispatchWebhook(
8752
- { ticketId, messageId: doc.id, authorType: doc.authorType },
8784
+ {
8785
+ ticketId,
8786
+ messageId: doc.id,
8787
+ authorType: doc.authorType,
8788
+ body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
8789
+ },
8753
8790
  "ticket_replied",
8754
8791
  req.payload,
8755
8792
  slugs
@@ -8932,7 +8969,7 @@ function createTicketMessagesCollection(slugs, options) {
8932
8969
  ] : []
8933
8970
  ],
8934
8971
  hooks: {
8935
- beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
8972
+ beforeChange: [createRestrictClientTicketTarget(slugs), createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
8936
8973
  afterChange: [
8937
8974
  ...options?.capabilities?.sms ? [createSmsNotification(slugs, options.capabilities.sms)] : [],
8938
8975
  createAutoUpdateStatus(slugs),
@@ -8941,7 +8978,6 @@ function createTicketMessagesCollection(slugs, options) {
8941
8978
  createCheckSlaOnReply(slugs, notificationSlug),
8942
8979
  createSyncTicketReplyToChat(slugs),
8943
8980
  createNotifyAdminOnClientMessage(slugs, notificationSlug),
8944
- createFireMessageWebhooks(slugs),
8945
8981
  createDispatchWebhookOnReply(slugs),
8946
8982
  createNotifyMentions(slugs),
8947
8983
  ...options?.capabilities?.threadCleanup ? [createThreadCleanup(options.capabilities.threadCleanup)] : []
@@ -9034,7 +9070,11 @@ function createEnforce2FA(slugs) {
9034
9070
  return user;
9035
9071
  };
9036
9072
  }
9073
+ function createAdminOnlyFieldUpdate(slugs) {
9074
+ return ({ req }) => req.user?.collection === slugs.users;
9075
+ }
9037
9076
  function createSupportClientsCollection(slugs, options) {
9077
+ const adminOnlyFieldUpdate = createAdminOnlyFieldUpdate(slugs);
9038
9078
  return {
9039
9079
  slug: slugs.supportClients,
9040
9080
  labels: {
@@ -9164,7 +9204,8 @@ function createSupportClientsCollection(slugs, options) {
9164
9204
  label: "Opportunites commerciales",
9165
9205
  access: {
9166
9206
  // Admin-only: never serialize commercial notes onto the client's own doc.
9167
- read: ({ req }) => req.user?.collection === slugs.users
9207
+ read: ({ req }) => req.user?.collection === slugs.users,
9208
+ update: adminOnlyFieldUpdate
9168
9209
  },
9169
9210
  admin: {
9170
9211
  description: "Notes commerciales (visible uniquement par les admins)"
@@ -9174,6 +9215,7 @@ function createSupportClientsCollection(slugs, options) {
9174
9215
  name: "tier",
9175
9216
  type: "select",
9176
9217
  label: "Plan",
9218
+ access: { update: adminOnlyFieldUpdate },
9177
9219
  options: [
9178
9220
  { label: "Free", value: "free" },
9179
9221
  { label: "Basic", value: "basic" },
@@ -9187,6 +9229,7 @@ function createSupportClientsCollection(slugs, options) {
9187
9229
  type: "relationship",
9188
9230
  relationTo: slugs.users,
9189
9231
  label: "Agent referent",
9232
+ access: { update: adminOnlyFieldUpdate },
9190
9233
  admin: { position: "sidebar" }
9191
9234
  },
9192
9235
  {
@@ -9202,17 +9245,20 @@ function createSupportClientsCollection(slugs, options) {
9202
9245
  {
9203
9246
  name: "twoFactorCode",
9204
9247
  type: "text",
9248
+ access: { update: adminOnlyFieldUpdate },
9205
9249
  admin: { hidden: true }
9206
9250
  },
9207
9251
  {
9208
9252
  name: "twoFactorExpiry",
9209
9253
  type: "date",
9254
+ access: { update: adminOnlyFieldUpdate },
9210
9255
  admin: { hidden: true }
9211
9256
  },
9212
9257
  {
9213
9258
  // Set by /support/2fa "verify"; gates login via the beforeLogin hook.
9214
9259
  name: "twoFactorVerifiedAt",
9215
9260
  type: "date",
9261
+ access: { update: adminOnlyFieldUpdate },
9216
9262
  admin: { hidden: true }
9217
9263
  },
9218
9264
  {
@@ -9220,6 +9266,7 @@ function createSupportClientsCollection(slugs, options) {
9220
9266
  name: "googleId",
9221
9267
  type: "text",
9222
9268
  index: true,
9269
+ access: { update: adminOnlyFieldUpdate },
9223
9270
  admin: { hidden: true }
9224
9271
  },
9225
9272
  {
@@ -9290,7 +9337,8 @@ function createSupportClientsCollection(slugs, options) {
9290
9337
  label: "Notes internes",
9291
9338
  access: {
9292
9339
  // Admin-only: never serialize internal notes onto the client's own doc.
9293
- read: ({ req }) => req.user?.collection === slugs.users
9340
+ read: ({ req }) => req.user?.collection === slugs.users,
9341
+ update: adminOnlyFieldUpdate
9294
9342
  },
9295
9343
  admin: {
9296
9344
  description: "Visible uniquement par les admins",
@@ -9363,36 +9411,49 @@ function createSupportClientsCollection(slugs, options) {
9363
9411
  }
9364
9412
 
9365
9413
  // src/collections/TimeEntries.ts
9414
+ async function recalculateTicketTime(payload, slugs, ticketId) {
9415
+ let totalMinutes = 0;
9416
+ let page = 1;
9417
+ let hasMore = true;
9418
+ while (hasMore) {
9419
+ const entries = await payload.find({
9420
+ collection: slugs.timeEntries,
9421
+ where: { ticket: { equals: ticketId } },
9422
+ limit: 100,
9423
+ page,
9424
+ depth: 0,
9425
+ overrideAccess: true,
9426
+ select: { duration: true }
9427
+ });
9428
+ for (const entry of entries.docs) {
9429
+ totalMinutes += entry.duration || 0;
9430
+ }
9431
+ hasMore = entries.hasNextPage ?? false;
9432
+ page++;
9433
+ }
9434
+ await payload.update({
9435
+ collection: slugs.tickets,
9436
+ id: ticketId,
9437
+ data: { totalTimeMinutes: totalMinutes },
9438
+ overrideAccess: true
9439
+ });
9440
+ }
9366
9441
  function createRecalculateTicketTime(slugs) {
9367
9442
  return async ({ doc, req }) => {
9368
9443
  if (!doc.ticket) return;
9369
- const { payload } = req;
9370
9444
  const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
9371
- let totalMinutes = 0;
9372
- let page = 1;
9373
- let hasMore = true;
9374
- while (hasMore) {
9375
- const entries = await payload.find({
9376
- collection: slugs.timeEntries,
9377
- where: { ticket: { equals: ticketId } },
9378
- limit: 100,
9379
- page,
9380
- depth: 0,
9381
- overrideAccess: true,
9382
- select: { duration: true }
9383
- });
9384
- for (const entry of entries.docs) {
9385
- totalMinutes += entry.duration || 0;
9386
- }
9387
- hasMore = entries.hasNextPage ?? false;
9388
- page++;
9445
+ await recalculateTicketTime(req.payload, slugs, ticketId);
9446
+ };
9447
+ }
9448
+ function createRecalculateTicketTimeOnDelete(slugs) {
9449
+ return async ({ doc, req }) => {
9450
+ if (!doc?.ticket) return;
9451
+ const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
9452
+ try {
9453
+ await recalculateTicketTime(req.payload, slugs, ticketId);
9454
+ } catch (err) {
9455
+ console.error("[support] Failed to recalculate ticket time after delete:", err);
9389
9456
  }
9390
- await payload.update({
9391
- collection: slugs.tickets,
9392
- id: ticketId,
9393
- data: { totalTimeMinutes: totalMinutes },
9394
- overrideAccess: true
9395
- });
9396
9457
  };
9397
9458
  }
9398
9459
  function createTimeEntriesCollection(slugs) {
@@ -9454,7 +9515,8 @@ function createTimeEntriesCollection(slugs) {
9454
9515
  }
9455
9516
  ],
9456
9517
  hooks: {
9457
- afterChange: [createRecalculateTicketTime(slugs)]
9518
+ afterChange: [createRecalculateTicketTime(slugs)],
9519
+ afterDelete: [createRecalculateTicketTimeOnDelete(slugs)]
9458
9520
  },
9459
9521
  access: {
9460
9522
  create: ({ req }) => req.user?.collection === slugs.users,
@@ -10880,7 +10942,12 @@ function createTicketCollaboratorsCollection(slugs) {
10880
10942
  }
10881
10943
  return false;
10882
10944
  },
10883
- create: ({ req }) => !!req.user,
10945
+ // Staff only. This collection is the source of truth for
10946
+ // resolveAccessibleTicketIds(), which widens `read` on tickets, messages
10947
+ // and the activity log — so letting any authenticated principal POST here
10948
+ // let a support client grant themselves access to any ticket by id.
10949
+ // The /invite endpoint is unaffected: it writes with overrideAccess: true.
10950
+ create: ({ req }) => req.user?.collection === slugs.users,
10884
10951
  update: () => false,
10885
10952
  delete: ({ req }) => req.user?.collection === slugs.users
10886
10953
  },
@@ -11221,4 +11288,4 @@ function supportPlugin(config) {
11221
11288
  };
11222
11289
  }
11223
11290
 
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 };
11291
+ export { DEFAULT_FEATURES, DEFAULT_INBOUND_EMAIL_LIMITS, DEFAULT_SETTINGS, DEFAULT_SLUGS, DEFAULT_TICKETING_FEATURES, 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, normalizeFeatures, projectAutoClose, readSupportSettings, readSupportSettingsState, readUserPrefs, resolveSlugs, stripProjectedFeatures, supportPlugin, validateInboundEmailPayload, verifySecret };