@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.cjs CHANGED
@@ -377,14 +377,81 @@ function dbDelete(payload, slug, options) {
377
377
  return payload.delete({ collection: slug, ...options });
378
378
  }
379
379
 
380
+ // src/utils/features.ts
381
+ var DEFAULT_TICKETING_FEATURES = {
382
+ timeTracking: true,
383
+ ai: true,
384
+ satisfaction: true,
385
+ chat: true,
386
+ emailTracking: true,
387
+ canned: true,
388
+ merge: true,
389
+ snooze: true,
390
+ externalMessages: true,
391
+ clientHistory: true,
392
+ activityLog: true,
393
+ splitTicket: true,
394
+ scheduledReplies: true,
395
+ autoClose: true,
396
+ autoCloseDays: 7,
397
+ roundRobin: false
398
+ };
399
+ var BOOLEAN_FEATURE_KEYS = [
400
+ "timeTracking",
401
+ "ai",
402
+ "satisfaction",
403
+ "chat",
404
+ "emailTracking",
405
+ "canned",
406
+ "merge",
407
+ "snooze",
408
+ "externalMessages",
409
+ "clientHistory",
410
+ "activityLog",
411
+ "splitTicket",
412
+ "scheduledReplies",
413
+ "autoClose",
414
+ "roundRobin"
415
+ ];
416
+ var PROJECTED_FEATURE_KEYS = ["autoClose", "autoCloseDays"];
417
+ function normalizeFeatures(raw) {
418
+ const out = { ...DEFAULT_TICKETING_FEATURES };
419
+ if (!raw || typeof raw !== "object") return out;
420
+ const src = raw;
421
+ for (const key of BOOLEAN_FEATURE_KEYS) {
422
+ if (typeof src[key] === "boolean") out[key] = src[key];
423
+ }
424
+ const days = src.autoCloseDays;
425
+ if (typeof days === "number" && Number.isFinite(days) && days >= 1) {
426
+ out.autoCloseDays = Math.floor(days);
427
+ }
428
+ return out;
429
+ }
430
+ function projectAutoClose(features, autoClose) {
431
+ if (!autoClose) return features;
432
+ return {
433
+ ...features,
434
+ autoClose: typeof autoClose.enabled === "boolean" ? autoClose.enabled : features.autoClose,
435
+ autoCloseDays: typeof autoClose.daysBeforeClose === "number" && autoClose.daysBeforeClose >= 1 ? Math.floor(autoClose.daysBeforeClose) : features.autoCloseDays
436
+ };
437
+ }
438
+ function stripProjectedFeatures(features) {
439
+ const out = { ...features };
440
+ for (const key of PROJECTED_FEATURE_KEYS) delete out[key];
441
+ return out;
442
+ }
443
+
380
444
  // src/utils/readSettings.ts
381
- var PREF_KEY = "support-settings";
445
+ var SUPPORT_SETTINGS_PREF_KEY = "support-settings";
446
+ var PREF_KEY = SUPPORT_SETTINGS_PREF_KEY;
382
447
  var USER_PREFS_KEY_PREFIX = "support-user-prefs";
448
+ var LEGACY_ROUND_ROBIN_KEY = "support-round-robin";
383
449
  var DEFAULT_SETTINGS = {
384
450
  email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
385
451
  ai: { provider: "anthropic", model: "claude-haiku-4-5-20251001", enableSentiment: true, enableSynthesis: true, enableSuggestion: true, enableRewrite: true },
386
452
  sla: { firstResponseMinutes: 120, resolutionMinutes: 1440, businessHoursOnly: true, escalationEmail: "" },
387
- autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
453
+ autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 },
454
+ features: { ...DEFAULT_TICKETING_FEATURES }
388
455
  };
389
456
  var DEFAULT_USER_PREFS = {
390
457
  locale: "fr",
@@ -395,32 +462,71 @@ var SETTINGS_TTL_MS = 6e4;
395
462
  function invalidateSupportSettingsCache() {
396
463
  settingsCache = null;
397
464
  }
398
- async function readSupportSettings(payload) {
465
+ function mergeSupportSettings(stored, base = DEFAULT_SETTINGS) {
466
+ const autoClose = { ...base.autoClose, ...stored?.autoClose };
467
+ return {
468
+ email: { ...base.email, ...stored?.email },
469
+ ai: { ...base.ai, ...stored?.ai },
470
+ sla: { ...base.sla, ...stored?.sla },
471
+ autoClose,
472
+ // `autoClose` / `autoCloseDays` are projections of the block above, so they
473
+ // are recomputed here rather than trusted from whatever was persisted.
474
+ features: projectAutoClose(
475
+ normalizeFeatures({ ...base.features, ...stored?.features }),
476
+ autoClose
477
+ )
478
+ };
479
+ }
480
+ async function readSupportSettingsState(payload) {
399
481
  if (settingsCache && Date.now() - settingsCache.ts < SETTINGS_TTL_MS) {
400
482
  return settingsCache.value;
401
483
  }
402
- let value = { ...DEFAULT_SETTINGS };
484
+ let value = {
485
+ settings: mergeSupportSettings(null),
486
+ featuresConfigured: false
487
+ };
403
488
  try {
404
489
  const prefs = await dbFind(payload, "payload-preferences", {
405
490
  where: { key: { equals: PREF_KEY } },
491
+ // The upsert is scoped per admin user, so several rows can share the key.
492
+ // Sorting makes "last write wins" deterministic instead of arbitrary.
493
+ sort: "-updatedAt",
406
494
  limit: 1,
407
495
  depth: 0,
408
496
  overrideAccess: true
409
497
  });
410
498
  if (prefs.docs.length > 0) {
411
499
  const stored = prefs.docs[0].value;
412
- value = {
413
- email: { ...DEFAULT_SETTINGS.email, ...stored.email },
414
- ai: { ...DEFAULT_SETTINGS.ai, ...stored.ai },
415
- sla: { ...DEFAULT_SETTINGS.sla, ...stored.sla },
416
- autoClose: { ...DEFAULT_SETTINGS.autoClose, ...stored.autoClose }
417
- };
500
+ const featuresConfigured = !!stored.features && typeof stored.features === "object";
501
+ const settings = mergeSupportSettings(stored);
502
+ if (!featuresConfigured) {
503
+ settings.features.roundRobin = await readLegacyRoundRobin(payload);
504
+ }
505
+ value = { settings, featuresConfigured };
418
506
  }
419
507
  } catch {
420
508
  }
421
509
  settingsCache = { value, ts: Date.now() };
422
510
  return value;
423
511
  }
512
+ async function readSupportSettings(payload) {
513
+ return (await readSupportSettingsState(payload)).settings;
514
+ }
515
+ async function readLegacyRoundRobin(payload) {
516
+ try {
517
+ const prefs = await dbFind(payload, "payload-preferences", {
518
+ where: { key: { equals: LEGACY_ROUND_ROBIN_KEY } },
519
+ limit: 1,
520
+ depth: 0,
521
+ overrideAccess: true
522
+ });
523
+ if (prefs.docs.length > 0) {
524
+ return prefs.docs[0].value?.enabled === true;
525
+ }
526
+ } catch {
527
+ }
528
+ return DEFAULT_TICKETING_FEATURES.roundRobin;
529
+ }
424
530
  async function readUserPrefs(payload, userId) {
425
531
  try {
426
532
  const key = `${USER_PREFS_KEY_PREFIX}-${userId}`;
@@ -441,13 +547,20 @@ async function readUserPrefs(payload, userId) {
441
547
  }
442
548
  return { ...DEFAULT_USER_PREFS };
443
549
  }
550
+ function resolveOllamaBaseUrl() {
551
+ const baseURL = process.env.OLLAMA_API_URL;
552
+ if (!baseURL) {
553
+ throw new payload.APIError("provider 'ollama' requires OLLAMA_API_URL", 500);
554
+ }
555
+ return baseURL;
556
+ }
444
557
 
445
558
  // src/endpoints/ai.ts
446
559
  async function getClient(aiSettings) {
447
560
  const moduleName = "@anthropic-ai/sdk";
448
561
  const { default: Anthropic } = await import(moduleName);
449
562
  if (aiSettings.provider === "ollama") {
450
- const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
563
+ const baseURL = resolveOllamaBaseUrl();
451
564
  return new Anthropic({ apiKey: "ollama", baseURL });
452
565
  }
453
566
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
@@ -646,7 +759,7 @@ async function applyAgentDecision(payload, slugs, ticketId, decision) {
646
759
  function getClient2(aiSettings) {
647
760
  const Anthropic = __require("@anthropic-ai/sdk").default;
648
761
  if (aiSettings.provider === "ollama") {
649
- return new Anthropic({ apiKey: "ollama", baseURL: process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1" });
762
+ return new Anthropic({ apiKey: "ollama", baseURL: resolveOllamaBaseUrl() });
650
763
  }
651
764
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
652
765
  }
@@ -746,7 +859,7 @@ async function getClient3(aiSettings) {
746
859
  const moduleName = "@anthropic-ai/sdk";
747
860
  const { default: Anthropic } = await import(moduleName);
748
861
  if (aiSettings.provider === "ollama") {
749
- const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
862
+ const baseURL = resolveOllamaBaseUrl();
750
863
  return new Anthropic({ apiKey: "ollama", baseURL });
751
864
  }
752
865
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
@@ -1582,25 +1695,6 @@ function createPresenceGetEndpoint(slugs) {
1582
1695
  }
1583
1696
 
1584
1697
  // src/endpoints/settings.ts
1585
- var PREF_KEY2 = "support-settings";
1586
- var DEFAULT_SUPPORT_SETTINGS = {
1587
- email: { fromAddress: "", fromName: "Support", replyToAddress: "" },
1588
- ai: {
1589
- provider: "anthropic",
1590
- model: "claude-haiku-4-5-20251001",
1591
- enableSentiment: true,
1592
- enableSynthesis: true,
1593
- enableSuggestion: true,
1594
- enableRewrite: true
1595
- },
1596
- sla: {
1597
- firstResponseMinutes: 120,
1598
- resolutionMinutes: 1440,
1599
- businessHoursOnly: true,
1600
- escalationEmail: ""
1601
- },
1602
- autoClose: { enabled: true, daysBeforeClose: 7, reminderDaysBefore: 2 }
1603
- };
1604
1698
  function createSettingsGetEndpoint(slugs) {
1605
1699
  return {
1606
1700
  path: "/support/settings",
@@ -1609,23 +1703,8 @@ function createSettingsGetEndpoint(slugs) {
1609
1703
  try {
1610
1704
  const payload = req.payload;
1611
1705
  requireAdmin(req, slugs);
1612
- const prefs = await dbFind(payload, "payload-preferences", {
1613
- where: { key: { equals: PREF_KEY2 } },
1614
- limit: 1,
1615
- depth: 0,
1616
- overrideAccess: true
1617
- });
1618
- let settings = { ...DEFAULT_SUPPORT_SETTINGS };
1619
- if (prefs.docs.length > 0) {
1620
- const stored = prefs.docs[0].value;
1621
- settings = {
1622
- email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...stored.email },
1623
- ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...stored.ai },
1624
- sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...stored.sla },
1625
- autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...stored.autoClose }
1626
- };
1627
- }
1628
- return Response.json(settings);
1706
+ const { settings, featuresConfigured } = await readSupportSettingsState(payload);
1707
+ return Response.json({ ...settings, featuresConfigured });
1629
1708
  } catch (error) {
1630
1709
  const authResponse = handleAuthError(error);
1631
1710
  if (authResponse) return authResponse;
@@ -1644,30 +1723,31 @@ function createSettingsPostEndpoint(slugs) {
1644
1723
  const payload = req.payload;
1645
1724
  requireAdmin(req, slugs);
1646
1725
  const body = await req.json();
1647
- const merged = {
1648
- email: { ...DEFAULT_SUPPORT_SETTINGS.email, ...body.email },
1649
- ai: { ...DEFAULT_SUPPORT_SETTINGS.ai, ...body.ai },
1650
- sla: { ...DEFAULT_SUPPORT_SETTINGS.sla, ...body.sla },
1651
- autoClose: { ...DEFAULT_SUPPORT_SETTINGS.autoClose, ...body.autoClose }
1652
- };
1726
+ const { settings: current } = await readSupportSettingsState(payload);
1727
+ const merged = mergeSupportSettings(body, current);
1653
1728
  await payload.db.upsert({
1654
1729
  collection: "payload-preferences",
1655
1730
  data: {
1656
- key: PREF_KEY2,
1731
+ key: SUPPORT_SETTINGS_PREF_KEY,
1657
1732
  user: { relationTo: req.user.collection, value: req.user.id },
1658
- value: merged
1733
+ value: {
1734
+ ...merged,
1735
+ // Persist the flags without the two projected ones, so `features`
1736
+ // and the `autoClose` block can never drift apart.
1737
+ features: stripProjectedFeatures(merged.features)
1738
+ }
1659
1739
  },
1660
1740
  req: { payload, user: req.user },
1661
1741
  where: {
1662
1742
  and: [
1663
- { key: { equals: PREF_KEY2 } },
1743
+ { key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
1664
1744
  { "user.value": { equals: req.user.id } },
1665
1745
  { "user.relationTo": { equals: req.user.collection } }
1666
1746
  ]
1667
1747
  }
1668
1748
  });
1669
1749
  invalidateSupportSettingsCache();
1670
- return Response.json(merged);
1750
+ return Response.json({ ...merged, featuresConfigured: true });
1671
1751
  } catch (error) {
1672
1752
  const authResponse = handleAuthError(error);
1673
1753
  if (authResponse) return authResponse;
@@ -1679,7 +1759,7 @@ function createSettingsPostEndpoint(slugs) {
1679
1759
  }
1680
1760
 
1681
1761
  // src/endpoints/signature.ts
1682
- var PREF_KEY3 = "email-signature";
1762
+ var PREF_KEY2 = "email-signature";
1683
1763
  function createSignatureGetEndpoint(slugs) {
1684
1764
  return {
1685
1765
  path: "/support/signature",
@@ -1689,7 +1769,7 @@ function createSignatureGetEndpoint(slugs) {
1689
1769
  const payload = req.payload;
1690
1770
  requireAdmin(req, slugs);
1691
1771
  const prefs = await dbFind(payload, "payload-preferences", {
1692
- where: { key: { equals: `${PREF_KEY3}-${req.user.id}` } },
1772
+ where: { key: { equals: `${PREF_KEY2}-${req.user.id}` } },
1693
1773
  limit: 1,
1694
1774
  depth: 0,
1695
1775
  overrideAccess: true
@@ -1714,7 +1794,7 @@ function createSignaturePostEndpoint(slugs) {
1714
1794
  const payload = req.payload;
1715
1795
  requireAdmin(req, slugs);
1716
1796
  const { signature } = await req.json();
1717
- const key = `${PREF_KEY3}-${req.user.id}`;
1797
+ const key = `${PREF_KEY2}-${req.user.id}`;
1718
1798
  const existing = await dbFind(payload, "payload-preferences", {
1719
1799
  where: { key: { equals: key } },
1720
1800
  limit: 1,
@@ -1749,7 +1829,6 @@ function createSignaturePostEndpoint(slugs) {
1749
1829
  }
1750
1830
 
1751
1831
  // src/endpoints/round-robin-config.ts
1752
- var PREF_KEY4 = "support-round-robin";
1753
1832
  function createRoundRobinConfigGetEndpoint(slugs) {
1754
1833
  return {
1755
1834
  path: "/support/round-robin-config",
@@ -1758,14 +1837,8 @@ function createRoundRobinConfigGetEndpoint(slugs) {
1758
1837
  try {
1759
1838
  const payload = req.payload;
1760
1839
  requireAdmin(req, slugs);
1761
- const prefs = await dbFind(payload, "payload-preferences", {
1762
- where: { key: { equals: PREF_KEY4 } },
1763
- limit: 1,
1764
- depth: 0,
1765
- overrideAccess: true
1766
- });
1767
- const enabled = prefs.docs.length > 0 ? prefs.docs[0].value?.enabled === true : false;
1768
- return Response.json({ enabled });
1840
+ const { settings } = await readSupportSettingsState(payload);
1841
+ return Response.json({ enabled: settings.features.roundRobin });
1769
1842
  } catch (error) {
1770
1843
  const authResponse = handleAuthError(error);
1771
1844
  if (authResponse) return authResponse;
@@ -1784,28 +1857,31 @@ function createRoundRobinConfigPostEndpoint(slugs) {
1784
1857
  const payload = req.payload;
1785
1858
  requireAdmin(req, slugs);
1786
1859
  const { enabled } = await req.json();
1787
- const existing = await dbFind(payload, "payload-preferences", {
1788
- where: { key: { equals: PREF_KEY4 } },
1789
- limit: 1,
1790
- depth: 0,
1791
- overrideAccess: true
1792
- });
1860
+ const { settings: current } = await readSupportSettingsState(payload);
1861
+ const merged = mergeSupportSettings(
1862
+ { features: { ...current.features, roundRobin: !!enabled } },
1863
+ current
1864
+ );
1793
1865
  await payload.db.upsert({
1794
1866
  collection: "payload-preferences",
1795
1867
  data: {
1796
- key: PREF_KEY4,
1868
+ key: SUPPORT_SETTINGS_PREF_KEY,
1797
1869
  user: { relationTo: req.user.collection, value: req.user.id },
1798
- value: { enabled: !!enabled }
1870
+ value: {
1871
+ ...merged,
1872
+ features: stripProjectedFeatures(merged.features)
1873
+ }
1799
1874
  },
1800
1875
  req: { payload, user: req.user },
1801
1876
  where: {
1802
1877
  and: [
1803
- { key: { equals: PREF_KEY4 } },
1878
+ { key: { equals: SUPPORT_SETTINGS_PREF_KEY } },
1804
1879
  { "user.value": { equals: req.user.id } },
1805
1880
  { "user.relationTo": { equals: req.user.collection } }
1806
1881
  ]
1807
1882
  }
1808
1883
  });
1884
+ invalidateSupportSettingsCache();
1809
1885
  return Response.json({ enabled: !!enabled });
1810
1886
  } catch (error) {
1811
1887
  const authResponse = handleAuthError(error);
@@ -4067,7 +4143,7 @@ function createInvoiceEndpoint(slugs) {
4067
4143
  function getClient4(aiSettings) {
4068
4144
  const Anthropic = __require("@anthropic-ai/sdk").default;
4069
4145
  if (aiSettings.provider === "ollama") {
4070
- const baseURL = process.env.OLLAMA_API_URL || "https://ollama.orkelis.app/v1";
4146
+ const baseURL = resolveOllamaBaseUrl();
4071
4147
  return new Anthropic({ apiKey: "ollama", baseURL });
4072
4148
  }
4073
4149
  return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
@@ -5760,62 +5836,73 @@ function createImportConversationEndpoint(slugs, store) {
5760
5836
  }
5761
5837
  };
5762
5838
  }
5763
-
5764
- // src/utils/fireWebhooks.ts
5765
- async function fireWebhooks(payload, slugs, event, data) {
5839
+ function dispatchWebhook(data, event, payload, slugs) {
5840
+ const done = _dispatch(data, event, payload, slugs);
5841
+ return done;
5842
+ }
5843
+ async function _dispatch(data, event, payload, slugs) {
5766
5844
  try {
5767
- const endpoints = await dbFind(payload, slugs.webhookEndpoints, {
5845
+ const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
5768
5846
  where: {
5769
- active: { equals: true },
5770
- events: { contains: event }
5847
+ and: [
5848
+ { active: { equals: true } },
5849
+ { events: { contains: event } }
5850
+ ]
5771
5851
  },
5772
5852
  limit: 50,
5773
5853
  depth: 0,
5774
5854
  overrideAccess: true
5775
5855
  });
5776
- if (endpoints.docs.length === 0) return;
5777
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
5856
+ if (endpoints.length === 0) return;
5857
+ const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
5778
5858
  await Promise.allSettled(
5779
- endpoints.docs.map(async (endpoint) => {
5780
- try {
5781
- const res = await fetch(endpoint.url, {
5782
- method: "POST",
5783
- headers: {
5784
- "Content-Type": "application/json",
5785
- ...endpoint.secret ? { "X-Webhook-Secret": endpoint.secret } : {}
5786
- },
5787
- body: JSON.stringify({ event, data, timestamp }),
5788
- signal: AbortSignal.timeout(1e4)
5789
- });
5790
- try {
5791
- await dbUpdate(payload, slugs.webhookEndpoints, {
5792
- id: endpoint.id,
5793
- data: {
5794
- lastTriggeredAt: timestamp,
5795
- lastStatus: res.status
5796
- },
5797
- overrideAccess: true
5798
- });
5799
- } catch {
5800
- }
5801
- } catch (error) {
5802
- console.warn(`[support] Webhook delivery failed: ${endpoint.url}`, error);
5803
- try {
5804
- await dbUpdate(payload, slugs.webhookEndpoints, {
5805
- id: endpoint.id,
5806
- data: {
5807
- lastTriggeredAt: timestamp,
5808
- lastStatus: 0
5809
- },
5810
- overrideAccess: true
5811
- });
5812
- } catch {
5813
- }
5814
- }
5815
- })
5859
+ endpoints.map((endpoint) => _sendToEndpoint(endpoint, body, payload, slugs))
5816
5860
  );
5817
- } catch (error) {
5818
- console.error("[support] Failed to fire webhooks:", error);
5861
+ } catch (err) {
5862
+ console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
5863
+ }
5864
+ }
5865
+ async function _sendToEndpoint(endpoint, body, payload, slugs) {
5866
+ try {
5867
+ const headers = {
5868
+ "Content-Type": "application/json",
5869
+ "User-Agent": "PayloadSupport-Webhook/1.0"
5870
+ };
5871
+ if (endpoint.secret) {
5872
+ const signature = crypto3__default.default.createHmac("sha256", endpoint.secret).update(body).digest("hex");
5873
+ headers["X-Webhook-Signature"] = signature;
5874
+ }
5875
+ const response = await fetch(endpoint.url, {
5876
+ method: "POST",
5877
+ headers,
5878
+ body,
5879
+ signal: AbortSignal.timeout(1e4)
5880
+ // 10s timeout
5881
+ });
5882
+ await dbUpdate(payload, slugs.webhookEndpoints, {
5883
+ id: endpoint.id,
5884
+ data: {
5885
+ lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
5886
+ lastStatus: response.status
5887
+ },
5888
+ overrideAccess: true
5889
+ });
5890
+ if (!response.ok) {
5891
+ console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
5892
+ }
5893
+ } catch (err) {
5894
+ console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
5895
+ try {
5896
+ await dbUpdate(payload, slugs.webhookEndpoints, {
5897
+ id: endpoint.id,
5898
+ data: {
5899
+ lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
5900
+ lastStatus: 0
5901
+ },
5902
+ overrideAccess: true
5903
+ });
5904
+ } catch {
5905
+ }
5819
5906
  }
5820
5907
  }
5821
5908
 
@@ -5902,13 +5989,18 @@ function createProcessScheduledEndpoint(slugs) {
5902
5989
  overrideAccess: true
5903
5990
  });
5904
5991
  }
5905
- fireWebhooks(payload, slugs, "ticket_replied", {
5906
- ticketId,
5907
- messageId: msg.id,
5908
- authorType: msg.authorType,
5909
- scheduled: true,
5910
- body: msg.body?.length > 500 ? msg.body.slice(0, 500) + "..." : msg.body
5911
- });
5992
+ dispatchWebhook(
5993
+ {
5994
+ ticketId,
5995
+ messageId: msg.id,
5996
+ authorType: msg.authorType,
5997
+ scheduled: true,
5998
+ body: msg.body?.length > 500 ? msg.body.slice(0, 500) + "..." : msg.body
5999
+ },
6000
+ "ticket_replied",
6001
+ payload,
6002
+ slugs
6003
+ );
5912
6004
  results.processed++;
5913
6005
  } catch (err) {
5914
6006
  console.error(`[process-scheduled] Error processing message ${message.id}:`, err);
@@ -6878,74 +6970,6 @@ async function createAdminNotification(payload, data, collectionSlug = "admin-no
6878
6970
  console.error("[notification] Failed to create:", err);
6879
6971
  }
6880
6972
  }
6881
- function dispatchWebhook(data, event, payload, slugs) {
6882
- void _dispatch(data, event, payload, slugs);
6883
- }
6884
- async function _dispatch(data, event, payload, slugs) {
6885
- try {
6886
- const { docs: endpoints } = await dbFind(payload, slugs.webhookEndpoints, {
6887
- where: {
6888
- and: [
6889
- { active: { equals: true } },
6890
- { events: { contains: event } }
6891
- ]
6892
- },
6893
- limit: 50,
6894
- depth: 0,
6895
- overrideAccess: true
6896
- });
6897
- if (endpoints.length === 0) return;
6898
- const body = JSON.stringify({ event, data, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
6899
- for (const endpoint of endpoints) {
6900
- void _sendToEndpoint(endpoint, body, payload, slugs);
6901
- }
6902
- } catch (err) {
6903
- console.error(`[webhook] Failed to fetch endpoints for event ${event}:`, err);
6904
- }
6905
- }
6906
- async function _sendToEndpoint(endpoint, body, payload, slugs) {
6907
- try {
6908
- const headers = {
6909
- "Content-Type": "application/json",
6910
- "User-Agent": "PayloadSupport-Webhook/1.0"
6911
- };
6912
- if (endpoint.secret) {
6913
- const signature = crypto3__default.default.createHmac("sha256", endpoint.secret).update(body).digest("hex");
6914
- headers["X-Webhook-Signature"] = signature;
6915
- }
6916
- const response = await fetch(endpoint.url, {
6917
- method: "POST",
6918
- headers,
6919
- body,
6920
- signal: AbortSignal.timeout(1e4)
6921
- // 10s timeout
6922
- });
6923
- await dbUpdate(payload, slugs.webhookEndpoints, {
6924
- id: endpoint.id,
6925
- data: {
6926
- lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
6927
- lastStatus: response.status
6928
- },
6929
- overrideAccess: true
6930
- });
6931
- if (!response.ok) {
6932
- console.warn(`[webhook] ${endpoint.name || endpoint.url} returned ${response.status}`);
6933
- }
6934
- } catch (err) {
6935
- console.error(`[webhook] Failed to call ${endpoint.name || endpoint.url}:`, err);
6936
- try {
6937
- await dbUpdate(payload, slugs.webhookEndpoints, {
6938
- id: endpoint.id,
6939
- data: {
6940
- lastTriggeredAt: (/* @__PURE__ */ new Date()).toISOString(),
6941
- lastStatus: 0
6942
- },
6943
- overrideAccess: true
6944
- });
6945
- } catch {
6946
- }
6947
- }
6948
- }
6949
6973
 
6950
6974
  // src/hooks/ticketStatusEmail.ts
6951
6975
  var STATUS_LABELS = {
@@ -7562,12 +7586,28 @@ function createAssignTicketNumber(slugs, options) {
7562
7586
  }
7563
7587
  function createAssignClientOnCreate(slugs) {
7564
7588
  return async ({ data, operation, req }) => {
7565
- if (operation === "create" && req.user?.collection === slugs.supportClients && !data.client) {
7589
+ if (operation === "create" && req.user?.collection === slugs.supportClients) {
7566
7590
  data.client = req.user.id;
7567
7591
  }
7568
7592
  return data;
7569
7593
  };
7570
7594
  }
7595
+ var CLIENT_CREATABLE_TICKET_FIELDS = ["subject", "category", "priority", "project"];
7596
+ function createRestrictClientCreate(slugs) {
7597
+ return async ({ data, operation, req }) => {
7598
+ if (operation !== "create") return data;
7599
+ if (req.user?.collection !== slugs.supportClients) return data;
7600
+ const incoming = data ?? {};
7601
+ const sanitized = {};
7602
+ for (const field2 of CLIENT_CREATABLE_TICKET_FIELDS) {
7603
+ if (incoming[field2] !== void 0) sanitized[field2] = incoming[field2];
7604
+ }
7605
+ sanitized.status = "open";
7606
+ sanitized.client = req.user.id;
7607
+ sanitized.source = "portal";
7608
+ return sanitized;
7609
+ };
7610
+ }
7571
7611
  var autoPaidAt = async ({ data, operation, originalDoc }) => {
7572
7612
  if (operation !== "update") return data;
7573
7613
  if (data.paymentStatus === "paid" && originalDoc?.paymentStatus !== "paid" && !data.paidAt) {
@@ -7593,16 +7633,8 @@ function createAutoAssignAdmin(slugs) {
7593
7633
  const { payload } = req;
7594
7634
  let roundRobinEnabled = false;
7595
7635
  try {
7596
- const prefs = await payload.find({
7597
- collection: "payload-preferences",
7598
- where: { key: { equals: "support-round-robin" } },
7599
- limit: 1,
7600
- depth: 0,
7601
- overrideAccess: true
7602
- });
7603
- if (prefs.docs.length > 0) {
7604
- roundRobinEnabled = prefs.docs[0].value?.enabled === true;
7605
- }
7636
+ const settings = await readSupportSettings(payload);
7637
+ roundRobinEnabled = settings.features.roundRobin === true;
7606
7638
  } catch (err) {
7607
7639
  console.warn("[support] Failed to read round-robin preferences:", err);
7608
7640
  }
@@ -7811,54 +7843,6 @@ function createNotifyClientOnResolve(slugs) {
7811
7843
  return doc;
7812
7844
  };
7813
7845
  }
7814
- function createFireTicketWebhooks(slugs) {
7815
- return async ({ doc, previousDoc, operation, req }) => {
7816
- const { payload } = req;
7817
- if (operation === "create") {
7818
- fireWebhooks(payload, slugs, "ticket_created", {
7819
- id: doc.id,
7820
- ticketNumber: doc.ticketNumber,
7821
- subject: doc.subject,
7822
- status: doc.status,
7823
- priority: doc.priority,
7824
- category: doc.category
7825
- });
7826
- return doc;
7827
- }
7828
- if (operation === "update" && previousDoc) {
7829
- if (previousDoc.status !== doc.status && doc.status === "resolved") {
7830
- fireWebhooks(payload, slugs, "ticket_resolved", {
7831
- id: doc.id,
7832
- ticketNumber: doc.ticketNumber,
7833
- subject: doc.subject,
7834
- previousStatus: previousDoc.status
7835
- });
7836
- const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
7837
- if (clientId) {
7838
- payload.update({
7839
- collection: "client-summaries",
7840
- where: { client: { equals: clientId } },
7841
- data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
7842
- // Force cache expiry
7843
- overrideAccess: true
7844
- }).catch(() => {
7845
- });
7846
- }
7847
- }
7848
- const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
7849
- const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
7850
- if (newAssigned && oldAssigned !== newAssigned) {
7851
- fireWebhooks(payload, slugs, "ticket_assigned", {
7852
- id: doc.id,
7853
- ticketNumber: doc.ticketNumber,
7854
- subject: doc.subject,
7855
- assignedTo: newAssigned
7856
- });
7857
- }
7858
- }
7859
- return doc;
7860
- };
7861
- }
7862
7846
  function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
7863
7847
  return async ({ doc, operation, req }) => {
7864
7848
  if (operation !== "create") return doc;
@@ -7879,21 +7863,66 @@ function createNotifyAdminOnNewTicket(slugs, notificationSlug) {
7879
7863
  }
7880
7864
  function createDispatchWebhookOnTicket(slugs) {
7881
7865
  return async ({ doc, previousDoc, operation, req }) => {
7866
+ const { payload } = req;
7882
7867
  if (operation === "create") {
7883
7868
  dispatchWebhook(
7884
- { ticketId: doc.id, ticketNumber: doc.ticketNumber, subject: doc.subject },
7869
+ {
7870
+ ticketId: doc.id,
7871
+ id: doc.id,
7872
+ ticketNumber: doc.ticketNumber,
7873
+ subject: doc.subject,
7874
+ status: doc.status,
7875
+ priority: doc.priority,
7876
+ category: doc.category
7877
+ },
7885
7878
  "ticket_created",
7886
- req.payload,
7879
+ payload,
7887
7880
  slugs
7888
7881
  );
7882
+ return doc;
7889
7883
  }
7890
- if (operation === "update" && previousDoc?.status !== doc.status && doc.status === "resolved") {
7891
- dispatchWebhook(
7892
- { ticketId: doc.id, ticketNumber: doc.ticketNumber, subject: doc.subject },
7893
- "ticket_resolved",
7894
- req.payload,
7895
- slugs
7896
- );
7884
+ if (operation === "update" && previousDoc) {
7885
+ if (previousDoc.status !== doc.status && doc.status === "resolved") {
7886
+ dispatchWebhook(
7887
+ {
7888
+ ticketId: doc.id,
7889
+ id: doc.id,
7890
+ ticketNumber: doc.ticketNumber,
7891
+ subject: doc.subject,
7892
+ previousStatus: previousDoc.status
7893
+ },
7894
+ "ticket_resolved",
7895
+ payload,
7896
+ slugs
7897
+ );
7898
+ const clientId = typeof doc.client === "object" ? doc.client?.id : doc.client;
7899
+ if (clientId) {
7900
+ payload.update({
7901
+ collection: "client-summaries",
7902
+ where: { client: { equals: clientId } },
7903
+ data: { generatedAt: (/* @__PURE__ */ new Date(0)).toISOString() },
7904
+ // Force cache expiry
7905
+ overrideAccess: true
7906
+ }).catch(() => {
7907
+ });
7908
+ }
7909
+ }
7910
+ const oldAssigned = typeof previousDoc.assignedTo === "object" ? previousDoc.assignedTo?.id : previousDoc.assignedTo;
7911
+ const newAssigned = typeof doc.assignedTo === "object" ? doc.assignedTo?.id : doc.assignedTo;
7912
+ if (newAssigned && oldAssigned !== newAssigned) {
7913
+ dispatchWebhook(
7914
+ {
7915
+ ticketId: doc.id,
7916
+ id: doc.id,
7917
+ ticketNumber: doc.ticketNumber,
7918
+ subject: doc.subject,
7919
+ assignedTo: newAssigned
7920
+ },
7921
+ "ticket_assigned",
7922
+ payload,
7923
+ slugs
7924
+ );
7925
+ }
7897
7926
  }
7898
7927
  return doc;
7899
7928
  };
@@ -8019,7 +8048,10 @@ function createTicketsCollection(slugs, options) {
8019
8048
  { label: "Paiement partiel", value: "partial" },
8020
8049
  { label: "Pay\xE9", value: "paid" }
8021
8050
  ],
8022
- access: { update: ({ req }) => req.user?.collection === "users" },
8051
+ // `slugs.users`, not the literal 'users': the users collection slug is
8052
+ // configurable, and a hard-coded one silently opens this field to everybody
8053
+ // on a host that renamed it.
8054
+ access: { update: ({ req }) => req.user?.collection === slugs.users },
8023
8055
  admin: { width: "50%", condition: (data) => !!data?.invoice }
8024
8056
  },
8025
8057
  {
@@ -8301,6 +8333,7 @@ function createTicketsCollection(slugs, options) {
8301
8333
  { name: "totalTimeMinutes", type: "number", defaultValue: 0, label: "Temps total (minutes)", admin: { readOnly: true, position: "sidebar" } }
8302
8334
  ],
8303
8335
  hooks: {
8336
+ beforeValidate: [createRestrictClientCreate(slugs)],
8304
8337
  beforeChange: [
8305
8338
  createAssignTicketNumber(slugs, options?.ticketNumber),
8306
8339
  createAssignClientOnCreate(slugs),
@@ -8321,7 +8354,6 @@ function createTicketsCollection(slugs, options) {
8321
8354
  createNotifyClientOnResolve(slugs),
8322
8355
  createTicketStatusEmail(slugs),
8323
8356
  createNotifyAdminOnNewTicket(slugs, notificationSlug),
8324
- createFireTicketWebhooks(slugs),
8325
8357
  createDispatchWebhookOnTicket(slugs),
8326
8358
  createApplyAutomationRules(slugs)
8327
8359
  ],
@@ -8502,6 +8534,21 @@ async function queueClientNotification(payload, slugs, item) {
8502
8534
  }
8503
8535
 
8504
8536
  // src/collections/TicketMessages.ts
8537
+ function createRestrictClientTicketTarget(slugs) {
8538
+ return async ({ data, operation, req, originalDoc }) => {
8539
+ if (req.user?.collection !== slugs.supportClients) return data;
8540
+ const raw = data.ticket ?? (operation === "update" ? originalDoc?.ticket : void 0);
8541
+ const targetId = raw && typeof raw === "object" ? raw.id : raw;
8542
+ if (targetId === void 0 || targetId === null || targetId === "") {
8543
+ throw new payload.APIError("Ticket cible requis.", 400);
8544
+ }
8545
+ const accessible = await resolveAccessibleTicketIds(req.payload, slugs, req.user.id);
8546
+ if (!accessible.some((id) => String(id) === String(targetId))) {
8547
+ throw new payload.APIError("Ticket inaccessible.", 403);
8548
+ }
8549
+ return data;
8550
+ };
8551
+ }
8505
8552
  function createAssignAuthor(slugs) {
8506
8553
  return async ({ data, operation, req }) => {
8507
8554
  if (operation === "create" && req.user?.collection === slugs.supportClients) {
@@ -8736,21 +8783,6 @@ function createNotifyAdminOnClientMessage(slugs, notificationSlug) {
8736
8783
  return doc;
8737
8784
  };
8738
8785
  }
8739
- function createFireMessageWebhooks(slugs) {
8740
- return async ({ doc, operation, req }) => {
8741
- if (operation !== "create") return doc;
8742
- if (doc.scheduledAt && !doc.scheduledSent) return doc;
8743
- if (doc.isInternal) return doc;
8744
- const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
8745
- fireWebhooks(req.payload, slugs, "ticket_replied", {
8746
- ticketId,
8747
- messageId: doc.id,
8748
- authorType: doc.authorType,
8749
- body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
8750
- });
8751
- return doc;
8752
- };
8753
- }
8754
8786
  function createDispatchWebhookOnReply(slugs) {
8755
8787
  return async ({ doc, operation, req }) => {
8756
8788
  if (operation !== "create") return doc;
@@ -8758,7 +8790,12 @@ function createDispatchWebhookOnReply(slugs) {
8758
8790
  if (doc.scheduledAt && !doc.scheduledSent) return doc;
8759
8791
  const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
8760
8792
  dispatchWebhook(
8761
- { ticketId, messageId: doc.id, authorType: doc.authorType },
8793
+ {
8794
+ ticketId,
8795
+ messageId: doc.id,
8796
+ authorType: doc.authorType,
8797
+ body: doc.body?.length > 500 ? doc.body.slice(0, 500) + "..." : doc.body
8798
+ },
8762
8799
  "ticket_replied",
8763
8800
  req.payload,
8764
8801
  slugs
@@ -8941,7 +8978,7 @@ function createTicketMessagesCollection(slugs, options) {
8941
8978
  ] : []
8942
8979
  ],
8943
8980
  hooks: {
8944
- beforeChange: [createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
8981
+ beforeChange: [createRestrictClientTicketTarget(slugs), createSanitizeMessageHtml(), createResolveMentions(slugs), createAssignAuthor(slugs)],
8945
8982
  afterChange: [
8946
8983
  ...options?.capabilities?.sms ? [createSmsNotification(slugs, options.capabilities.sms)] : [],
8947
8984
  createAutoUpdateStatus(slugs),
@@ -8950,7 +8987,6 @@ function createTicketMessagesCollection(slugs, options) {
8950
8987
  createCheckSlaOnReply(slugs, notificationSlug),
8951
8988
  createSyncTicketReplyToChat(slugs),
8952
8989
  createNotifyAdminOnClientMessage(slugs, notificationSlug),
8953
- createFireMessageWebhooks(slugs),
8954
8990
  createDispatchWebhookOnReply(slugs),
8955
8991
  createNotifyMentions(slugs),
8956
8992
  ...options?.capabilities?.threadCleanup ? [createThreadCleanup(options.capabilities.threadCleanup)] : []
@@ -9043,7 +9079,11 @@ function createEnforce2FA(slugs) {
9043
9079
  return user;
9044
9080
  };
9045
9081
  }
9082
+ function createAdminOnlyFieldUpdate(slugs) {
9083
+ return ({ req }) => req.user?.collection === slugs.users;
9084
+ }
9046
9085
  function createSupportClientsCollection(slugs, options) {
9086
+ const adminOnlyFieldUpdate = createAdminOnlyFieldUpdate(slugs);
9047
9087
  return {
9048
9088
  slug: slugs.supportClients,
9049
9089
  labels: {
@@ -9173,7 +9213,8 @@ function createSupportClientsCollection(slugs, options) {
9173
9213
  label: "Opportunites commerciales",
9174
9214
  access: {
9175
9215
  // Admin-only: never serialize commercial notes onto the client's own doc.
9176
- read: ({ req }) => req.user?.collection === slugs.users
9216
+ read: ({ req }) => req.user?.collection === slugs.users,
9217
+ update: adminOnlyFieldUpdate
9177
9218
  },
9178
9219
  admin: {
9179
9220
  description: "Notes commerciales (visible uniquement par les admins)"
@@ -9183,6 +9224,7 @@ function createSupportClientsCollection(slugs, options) {
9183
9224
  name: "tier",
9184
9225
  type: "select",
9185
9226
  label: "Plan",
9227
+ access: { update: adminOnlyFieldUpdate },
9186
9228
  options: [
9187
9229
  { label: "Free", value: "free" },
9188
9230
  { label: "Basic", value: "basic" },
@@ -9196,6 +9238,7 @@ function createSupportClientsCollection(slugs, options) {
9196
9238
  type: "relationship",
9197
9239
  relationTo: slugs.users,
9198
9240
  label: "Agent referent",
9241
+ access: { update: adminOnlyFieldUpdate },
9199
9242
  admin: { position: "sidebar" }
9200
9243
  },
9201
9244
  {
@@ -9211,17 +9254,20 @@ function createSupportClientsCollection(slugs, options) {
9211
9254
  {
9212
9255
  name: "twoFactorCode",
9213
9256
  type: "text",
9257
+ access: { update: adminOnlyFieldUpdate },
9214
9258
  admin: { hidden: true }
9215
9259
  },
9216
9260
  {
9217
9261
  name: "twoFactorExpiry",
9218
9262
  type: "date",
9263
+ access: { update: adminOnlyFieldUpdate },
9219
9264
  admin: { hidden: true }
9220
9265
  },
9221
9266
  {
9222
9267
  // Set by /support/2fa "verify"; gates login via the beforeLogin hook.
9223
9268
  name: "twoFactorVerifiedAt",
9224
9269
  type: "date",
9270
+ access: { update: adminOnlyFieldUpdate },
9225
9271
  admin: { hidden: true }
9226
9272
  },
9227
9273
  {
@@ -9229,6 +9275,7 @@ function createSupportClientsCollection(slugs, options) {
9229
9275
  name: "googleId",
9230
9276
  type: "text",
9231
9277
  index: true,
9278
+ access: { update: adminOnlyFieldUpdate },
9232
9279
  admin: { hidden: true }
9233
9280
  },
9234
9281
  {
@@ -9299,7 +9346,8 @@ function createSupportClientsCollection(slugs, options) {
9299
9346
  label: "Notes internes",
9300
9347
  access: {
9301
9348
  // Admin-only: never serialize internal notes onto the client's own doc.
9302
- read: ({ req }) => req.user?.collection === slugs.users
9349
+ read: ({ req }) => req.user?.collection === slugs.users,
9350
+ update: adminOnlyFieldUpdate
9303
9351
  },
9304
9352
  admin: {
9305
9353
  description: "Visible uniquement par les admins",
@@ -9372,36 +9420,49 @@ function createSupportClientsCollection(slugs, options) {
9372
9420
  }
9373
9421
 
9374
9422
  // src/collections/TimeEntries.ts
9423
+ async function recalculateTicketTime(payload, slugs, ticketId) {
9424
+ let totalMinutes = 0;
9425
+ let page = 1;
9426
+ let hasMore = true;
9427
+ while (hasMore) {
9428
+ const entries = await payload.find({
9429
+ collection: slugs.timeEntries,
9430
+ where: { ticket: { equals: ticketId } },
9431
+ limit: 100,
9432
+ page,
9433
+ depth: 0,
9434
+ overrideAccess: true,
9435
+ select: { duration: true }
9436
+ });
9437
+ for (const entry of entries.docs) {
9438
+ totalMinutes += entry.duration || 0;
9439
+ }
9440
+ hasMore = entries.hasNextPage ?? false;
9441
+ page++;
9442
+ }
9443
+ await payload.update({
9444
+ collection: slugs.tickets,
9445
+ id: ticketId,
9446
+ data: { totalTimeMinutes: totalMinutes },
9447
+ overrideAccess: true
9448
+ });
9449
+ }
9375
9450
  function createRecalculateTicketTime(slugs) {
9376
9451
  return async ({ doc, req }) => {
9377
9452
  if (!doc.ticket) return;
9378
- const { payload } = req;
9379
9453
  const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
9380
- let totalMinutes = 0;
9381
- let page = 1;
9382
- let hasMore = true;
9383
- while (hasMore) {
9384
- const entries = await payload.find({
9385
- collection: slugs.timeEntries,
9386
- where: { ticket: { equals: ticketId } },
9387
- limit: 100,
9388
- page,
9389
- depth: 0,
9390
- overrideAccess: true,
9391
- select: { duration: true }
9392
- });
9393
- for (const entry of entries.docs) {
9394
- totalMinutes += entry.duration || 0;
9395
- }
9396
- hasMore = entries.hasNextPage ?? false;
9397
- page++;
9454
+ await recalculateTicketTime(req.payload, slugs, ticketId);
9455
+ };
9456
+ }
9457
+ function createRecalculateTicketTimeOnDelete(slugs) {
9458
+ return async ({ doc, req }) => {
9459
+ if (!doc?.ticket) return;
9460
+ const ticketId = typeof doc.ticket === "object" ? doc.ticket.id : doc.ticket;
9461
+ try {
9462
+ await recalculateTicketTime(req.payload, slugs, ticketId);
9463
+ } catch (err) {
9464
+ console.error("[support] Failed to recalculate ticket time after delete:", err);
9398
9465
  }
9399
- await payload.update({
9400
- collection: slugs.tickets,
9401
- id: ticketId,
9402
- data: { totalTimeMinutes: totalMinutes },
9403
- overrideAccess: true
9404
- });
9405
9466
  };
9406
9467
  }
9407
9468
  function createTimeEntriesCollection(slugs) {
@@ -9463,7 +9524,8 @@ function createTimeEntriesCollection(slugs) {
9463
9524
  }
9464
9525
  ],
9465
9526
  hooks: {
9466
- afterChange: [createRecalculateTicketTime(slugs)]
9527
+ afterChange: [createRecalculateTicketTime(slugs)],
9528
+ afterDelete: [createRecalculateTicketTimeOnDelete(slugs)]
9467
9529
  },
9468
9530
  access: {
9469
9531
  create: ({ req }) => req.user?.collection === slugs.users,
@@ -10889,7 +10951,12 @@ function createTicketCollaboratorsCollection(slugs) {
10889
10951
  }
10890
10952
  return false;
10891
10953
  },
10892
- create: ({ req }) => !!req.user,
10954
+ // Staff only. This collection is the source of truth for
10955
+ // resolveAccessibleTicketIds(), which widens `read` on tickets, messages
10956
+ // and the activity log — so letting any authenticated principal POST here
10957
+ // let a support client grant themselves access to any ticket by id.
10958
+ // The /invite endpoint is unaffected: it writes with overrideAccess: true.
10959
+ create: ({ req }) => req.user?.collection === slugs.users,
10893
10960
  update: () => false,
10894
10961
  delete: ({ req }) => req.user?.collection === slugs.users
10895
10962
  },
@@ -11234,6 +11301,7 @@ exports.DEFAULT_FEATURES = DEFAULT_FEATURES;
11234
11301
  exports.DEFAULT_INBOUND_EMAIL_LIMITS = DEFAULT_INBOUND_EMAIL_LIMITS;
11235
11302
  exports.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
11236
11303
  exports.DEFAULT_SLUGS = DEFAULT_SLUGS;
11304
+ exports.DEFAULT_TICKETING_FEATURES = DEFAULT_TICKETING_FEATURES;
11237
11305
  exports.DEFAULT_USER_PREFS = DEFAULT_USER_PREFS;
11238
11306
  exports.MemoryRateLimitStore = MemoryRateLimitStore;
11239
11307
  exports.PayloadRateLimitStore = PayloadRateLimitStore;
@@ -11266,9 +11334,13 @@ exports.createTrackOpenEndpoint = createTrackOpenEndpoint;
11266
11334
  exports.createWebhookEndpointsCollection = createWebhookEndpointsCollection;
11267
11335
  exports.dispatchWebhook = dispatchWebhook;
11268
11336
  exports.generateTicketSynthesis = generateTicketSynthesis;
11337
+ exports.normalizeFeatures = normalizeFeatures;
11338
+ exports.projectAutoClose = projectAutoClose;
11269
11339
  exports.readSupportSettings = readSupportSettings;
11340
+ exports.readSupportSettingsState = readSupportSettingsState;
11270
11341
  exports.readUserPrefs = readUserPrefs;
11271
11342
  exports.resolveSlugs = resolveSlugs;
11343
+ exports.stripProjectedFeatures = stripProjectedFeatures;
11272
11344
  exports.supportPlugin = supportPlugin;
11273
11345
  exports.validateInboundEmailPayload = validateInboundEmailPayload;
11274
11346
  exports.verifySecret = verifySecret;