@consilioweb/payload-support 2.0.1 → 4.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 (246) hide show
  1. package/README.md +670 -402
  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/index.d.ts +4 -0
  29. package/dist/components/TicketConversation/index.js +2 -2
  30. package/dist/components/TicketConversation/types.d.ts +56 -0
  31. package/dist/components/TicketConversation/utils.d.ts +2 -0
  32. package/dist/index.cjs +940 -404
  33. package/dist/index.d.cts +119 -7
  34. package/dist/index.d.ts +119 -7
  35. package/dist/index.js +936 -405
  36. package/dist/utils/features.d.ts +123 -0
  37. package/dist/utils/features.js +147 -0
  38. package/dist/views/BillingView/client.d.ts +2 -0
  39. package/dist/views/BillingView/index.d.ts +4 -0
  40. package/dist/views/ChatView/client.d.ts +2 -0
  41. package/dist/views/ChatView/client.js +9 -9
  42. package/dist/views/ChatView/index.d.ts +4 -0
  43. package/dist/views/CrmView/client.d.ts +2 -0
  44. package/dist/views/CrmView/client.js +6 -9
  45. package/dist/views/CrmView/index.d.ts +4 -0
  46. package/dist/views/EmailTrackingView/client.d.ts +2 -0
  47. package/dist/views/EmailTrackingView/index.d.ts +4 -0
  48. package/dist/views/ImportConversationView/client.d.ts +2 -0
  49. package/dist/views/ImportConversationView/index.d.ts +4 -0
  50. package/dist/views/LogsView/client.d.ts +2 -0
  51. package/dist/views/LogsView/index.d.ts +4 -0
  52. package/dist/views/NewTicketView/client.d.ts +2 -0
  53. package/dist/views/NewTicketView/index.d.ts +4 -0
  54. package/dist/views/PendingEmailsView/client.d.ts +2 -0
  55. package/dist/views/PendingEmailsView/index.d.ts +4 -0
  56. package/dist/views/SupportDashboardView/client.d.ts +2 -0
  57. package/dist/views/SupportDashboardView/index.d.ts +4 -0
  58. package/dist/views/TicketDetailView/NextActionItem.d.ts +6 -0
  59. package/dist/views/TicketDetailView/RewriteDropdown.d.ts +7 -0
  60. package/dist/views/TicketDetailView/client.d.ts +2 -0
  61. package/dist/views/TicketDetailView/client.js +2 -2
  62. package/dist/views/TicketDetailView/constants.d.ts +13 -0
  63. package/dist/views/TicketDetailView/helpers.d.ts +2 -0
  64. package/dist/views/TicketDetailView/index.d.ts +4 -0
  65. package/dist/views/TicketDetailView/types.d.ts +40 -0
  66. package/dist/views/TicketInboxView/client.d.ts +2 -0
  67. package/dist/views/TicketInboxView/client.js +5 -5
  68. package/dist/views/TicketInboxView/index.d.ts +4 -0
  69. package/dist/views/TicketingSettingsView/client.d.ts +2 -0
  70. package/dist/views/TicketingSettingsView/client.js +5 -4
  71. package/dist/views/TicketingSettingsView/index.d.ts +4 -0
  72. package/dist/views/TimeDashboardView/client.d.ts +2 -0
  73. package/dist/views/TimeDashboardView/index.d.ts +4 -0
  74. package/dist/views/shared/AdminViewHeader.d.ts +13 -0
  75. package/dist/views/shared/ErrorBoundary.d.ts +17 -0
  76. package/dist/views/shared/Skeleton.d.ts +21 -0
  77. package/dist/views/shared/StatusPill.d.ts +8 -0
  78. package/dist/views/shared/adminTokens.d.ts +19 -0
  79. package/dist/views/shared/config.d.ts +8 -0
  80. package/dist/views/shared/config.js +1 -40
  81. package/dist/views/shared/dateLocale.d.ts +5 -0
  82. package/dist/views/shared/index.d.ts +12 -0
  83. package/dist/views/shared/index.js +2 -1
  84. package/dist/views/shared/sla.d.ts +14 -0
  85. package/dist/views.d.ts +13 -13
  86. package/package.json +16 -31
  87. package/src/collections/ChatMessages.ts +59 -2
  88. package/src/collections/ClientSummaries.ts +10 -4
  89. package/src/collections/SupportClients.ts +29 -0
  90. package/src/collections/TicketCollaborators.ts +6 -1
  91. package/src/collections/TicketMessages.ts +49 -24
  92. package/src/collections/Tickets.ts +117 -79
  93. package/src/collections/TimeEntries.ts +57 -32
  94. package/src/collections/WebhookEndpoints.ts +44 -2
  95. package/src/components/TicketConversation/config.ts +16 -80
  96. package/src/components/TicketConversation/hooks/useFeatures.ts +109 -0
  97. package/src/components/TicketConversation/index.tsx +2 -2
  98. package/src/endpoints/admin-chat.ts +3 -3
  99. package/src/endpoints/ai-agent.ts +3 -3
  100. package/src/endpoints/ai.ts +5 -4
  101. package/src/endpoints/auth-2fa.ts +16 -4
  102. package/src/endpoints/capabilities.ts +6 -6
  103. package/src/endpoints/chat.ts +7 -5
  104. package/src/endpoints/chatbot.ts +48 -2
  105. package/src/endpoints/client-intelligence.ts +6 -5
  106. package/src/endpoints/email-stats.ts +19 -3
  107. package/src/endpoints/import-conversation.ts +1 -1
  108. package/src/endpoints/index.ts +1 -1
  109. package/src/endpoints/invite-collaborator.ts +29 -3
  110. package/src/endpoints/login.ts +15 -2
  111. package/src/endpoints/oauth-google.ts +130 -8
  112. package/src/endpoints/process-scheduled.ts +13 -8
  113. package/src/endpoints/resend-notification.ts +3 -3
  114. package/src/endpoints/round-robin-config.ts +30 -20
  115. package/src/endpoints/send-reminder.ts +3 -3
  116. package/src/endpoints/settings.ts +28 -77
  117. package/src/endpoints/signature.ts +9 -2
  118. package/src/endpoints/ticket-synthesis.ts +3 -3
  119. package/src/endpoints/transfer-ticket.ts +28 -3
  120. package/src/endpoints/typing.ts +117 -14
  121. package/src/endpoints/user-prefs.ts +5 -2
  122. package/src/index.ts +6 -2
  123. package/src/plugin.ts +12 -0
  124. package/src/portal/LiveChat.tsx +33 -22
  125. package/src/portal/auth/layout.tsx +19 -1
  126. package/src/portal/auth/tickets/detail/MessageBody.tsx +88 -0
  127. package/src/portal/auth/tickets/detail/TicketPolling.tsx +5 -1
  128. package/src/portal/auth/tickets/detail/page.tsx +2 -6
  129. package/src/portal/login/page.tsx +11 -3
  130. package/src/types/lucide-react.d.ts +10 -2
  131. package/src/utils/aiAgent.ts +2 -1
  132. package/src/utils/aiProvider.ts +21 -0
  133. package/src/utils/features.ts +297 -0
  134. package/src/utils/fireWebhooks.ts +10 -1
  135. package/src/utils/generateTicketSynthesis.ts +2 -1
  136. package/src/utils/index.ts +4 -2
  137. package/src/utils/rateLimiter.ts +39 -5
  138. package/src/utils/readSettings.ts +199 -16
  139. package/src/utils/ticketAccess.ts +16 -1
  140. package/src/utils/twoFactorChallenge.ts +80 -0
  141. package/src/utils/urlSafety.ts +230 -0
  142. package/src/utils/webhookDispatcher.ts +24 -8
  143. package/src/views/ChatView/client.tsx +13 -9
  144. package/src/views/CrmView/client.tsx +10 -11
  145. package/src/views/TicketDetailView/client.tsx +2 -2
  146. package/src/views/TicketInboxView/client.tsx +10 -5
  147. package/src/views/TicketingSettingsView/client.tsx +8 -6
  148. package/src/views/shared/config.ts +14 -80
  149. package/src/views/shared/index.ts +3 -1
  150. package/dist/components/RichTextEditor/index.cjs +0 -279
  151. package/dist/components/TicketConversation/RewriteDropdown.cjs +0 -98
  152. package/dist/components/TicketConversation/SkeletonText.cjs +0 -21
  153. package/dist/components/TicketConversation/components/AISummaryPanel.cjs +0 -85
  154. package/dist/components/TicketConversation/components/ActionPanels.cjs +0 -193
  155. package/dist/components/TicketConversation/components/ActivityLog.cjs +0 -24
  156. package/dist/components/TicketConversation/components/ClientBar.cjs +0 -43
  157. package/dist/components/TicketConversation/components/ClientHistory.cjs +0 -138
  158. package/dist/components/TicketConversation/components/CodeBlock.cjs +0 -171
  159. package/dist/components/TicketConversation/components/CodeBlockInserter.cjs +0 -156
  160. package/dist/components/TicketConversation/components/QuickActions.cjs +0 -87
  161. package/dist/components/TicketConversation/components/TicketHeader.cjs +0 -93
  162. package/dist/components/TicketConversation/components/TimeTrackingPanel.cjs +0 -134
  163. package/dist/components/TicketConversation/config.cjs +0 -44
  164. package/dist/components/TicketConversation/constants.cjs +0 -127
  165. package/dist/components/TicketConversation/context.cjs +0 -13
  166. package/dist/components/TicketConversation/hooks/useAI.cjs +0 -196
  167. package/dist/components/TicketConversation/hooks/useDocumentIdFromUrl.cjs +0 -20
  168. package/dist/components/TicketConversation/hooks/useMessageActions.cjs +0 -136
  169. package/dist/components/TicketConversation/hooks/useReply.cjs +0 -188
  170. package/dist/components/TicketConversation/hooks/useTicketActions.cjs +0 -290
  171. package/dist/components/TicketConversation/hooks/useTimeTracking.cjs +0 -123
  172. package/dist/components/TicketConversation/hooks/useTranslation.cjs +0 -101
  173. package/dist/components/TicketConversation/index.cjs +0 -1114
  174. package/dist/components/TicketConversation/types.cjs +0 -2
  175. package/dist/components/TicketConversation/utils.cjs +0 -27
  176. package/dist/views/BillingView/client.cjs +0 -469
  177. package/dist/views/BillingView/index.cjs +0 -34
  178. package/dist/views/ChatView/client.cjs +0 -301
  179. package/dist/views/ChatView/index.cjs +0 -34
  180. package/dist/views/CrmView/client.cjs +0 -458
  181. package/dist/views/CrmView/index.cjs +0 -34
  182. package/dist/views/EmailTrackingView/client.cjs +0 -171
  183. package/dist/views/EmailTrackingView/index.cjs +0 -34
  184. package/dist/views/ImportConversationView/client.cjs +0 -238
  185. package/dist/views/ImportConversationView/index.cjs +0 -34
  186. package/dist/views/LogsView/client.cjs +0 -148
  187. package/dist/views/LogsView/index.cjs +0 -32
  188. package/dist/views/NewTicketView/client.cjs +0 -213
  189. package/dist/views/NewTicketView/index.cjs +0 -32
  190. package/dist/views/PendingEmailsView/client.cjs +0 -471
  191. package/dist/views/PendingEmailsView/index.cjs +0 -34
  192. package/dist/views/SupportDashboardView/client.cjs +0 -437
  193. package/dist/views/SupportDashboardView/index.cjs +0 -34
  194. package/dist/views/TicketDetailView/NextActionItem.cjs +0 -40
  195. package/dist/views/TicketDetailView/RewriteDropdown.cjs +0 -80
  196. package/dist/views/TicketDetailView/client.cjs +0 -1412
  197. package/dist/views/TicketDetailView/constants.cjs +0 -20
  198. package/dist/views/TicketDetailView/helpers.cjs +0 -23
  199. package/dist/views/TicketDetailView/index.cjs +0 -34
  200. package/dist/views/TicketDetailView/types.cjs +0 -2
  201. package/dist/views/TicketInboxView/client.cjs +0 -359
  202. package/dist/views/TicketInboxView/index.cjs +0 -32
  203. package/dist/views/TicketingSettingsView/client.cjs +0 -740
  204. package/dist/views/TicketingSettingsView/index.cjs +0 -34
  205. package/dist/views/TimeDashboardView/client.cjs +0 -180
  206. package/dist/views/TimeDashboardView/index.cjs +0 -34
  207. package/dist/views/shared/AdminViewHeader.cjs +0 -68
  208. package/dist/views/shared/ErrorBoundary.cjs +0 -55
  209. package/dist/views/shared/Skeleton.cjs +0 -77
  210. package/dist/views/shared/StatusPill.cjs +0 -47
  211. package/dist/views/shared/adminTokens.cjs +0 -34
  212. package/dist/views/shared/config.cjs +0 -44
  213. package/dist/views/shared/dateLocale.cjs +0 -5
  214. package/dist/views/shared/index.cjs +0 -72
  215. package/dist/views/shared/sla.cjs +0 -37
  216. package/dist/views.cjs +0 -86
  217. package/dist/views.d.cts +0 -13
  218. package/src/__tests__/aiSummaryRendering.test.ts +0 -21
  219. package/src/__tests__/authResponses.test.ts +0 -69
  220. package/src/__tests__/capabilities.test.ts +0 -71
  221. package/src/__tests__/generateTrackingToken.test.ts +0 -67
  222. package/src/__tests__/integration/ai-agent.test.ts +0 -46
  223. package/src/__tests__/integration/auth.test.ts +0 -43
  224. package/src/__tests__/integration/automation-rules.test.ts +0 -68
  225. package/src/__tests__/integration/buildTestPayload.ts +0 -52
  226. package/src/__tests__/integration/channels.test.ts +0 -52
  227. package/src/__tests__/integration/core-behaviors.test.ts +0 -81
  228. package/src/__tests__/integration/dashboard-volume.test.ts +0 -31
  229. package/src/__tests__/integration/digest.test.ts +0 -48
  230. package/src/__tests__/integration/invoice-pdf.test.ts +0 -30
  231. package/src/__tests__/integration/isolation.test.ts +0 -81
  232. package/src/__tests__/integration/mentions-invoice.test.ts +0 -50
  233. package/src/__tests__/integration/nps.test.ts +0 -37
  234. package/src/__tests__/integration/per-team-sla.test.ts +0 -44
  235. package/src/__tests__/integration/push.test.ts +0 -75
  236. package/src/__tests__/integration/sanitization.test.ts +0 -35
  237. package/src/__tests__/integration/sla-pause.test.ts +0 -39
  238. package/src/__tests__/integration/smoke.test.ts +0 -26
  239. package/src/__tests__/integration/snooze.test.ts +0 -68
  240. package/src/__tests__/integration/teams.test.ts +0 -38
  241. package/src/__tests__/rateLimiter.test.ts +0 -83
  242. package/src/__tests__/sanitizeHtml.test.ts +0 -165
  243. package/src/__tests__/sla.test.ts +0 -258
  244. package/src/__tests__/trackOpenEndpoint.test.ts +0 -54
  245. package/src/__tests__/translations.test.ts +0 -67
  246. package/src/__tests__/webhookSecurity.test.ts +0 -58
package/README.md CHANGED
@@ -1,98 +1,115 @@
1
- <div align="center" style="background: linear-gradient(135deg, #1f8a5b 0%, #16a34a 50%, #0d9488 100%); padding: 50px 40px; border-radius: 12px; color: white; margin-bottom: 40px;">
2
- <h1 style="margin: 0 0 15px 0; font-size: 42px; font-weight: 700; letter-spacing: -0.5px;">payload-support</h1>
3
- <p style="margin: 0 auto; font-size: 18px; opacity: 0.95; max-width: 640px; line-height: 1.6;">A complete, self-hosted support &amp; ticketing system for Payload CMS 3 + Next.js tickets, SLA, AI assists and an autonomous AI agent, live chat, a full client portal, time tracking &amp; invoicing, and a visual automation-rules engine. No third-party support SaaS required.</p>
4
- </div>
1
+ # @consilioweb/payload-support
2
+
3
+ > A self-hosted support and ticketing system for Payload CMS 3 + Next.js: tickets, SLA, live chat, a client portal, time tracking and invoicing, AI assists and an automation-rules engine.
4
+
5
+ [![npm](https://img.shields.io/npm/v/@consilioweb/payload-support.svg)](https://www.npmjs.com/package/@consilioweb/payload-support)
6
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
+ [![payload](https://img.shields.io/badge/payload-%5E3.37-blue.svg)](https://payloadcms.com)
8
+
9
+ ## About
10
+
11
+ `supportPlugin()` turns a Payload 3 admin into a helpdesk: it injects 14 to 25 collections
12
+ (depending on the feature flags), 13 admin views and around 60 REST endpoints covering the whole
13
+ ticket lifecycle — intake, assignment, SLA, replies by email or live chat, resolution, CSAT and
14
+ billing. It is aimed at agencies and product teams who already run Payload and do not want to pay
15
+ for, or export their customers' data to, a third-party support SaaS.
16
+
17
+ Two things it deliberately does *not* do: it does not mount the client portal for you (the portal
18
+ ships as a template you copy into your own `app/` directory — see
19
+ [Mounting the client portal](#mounting-the-client-portal)), and it does not bundle an LLM SDK
20
+ (install `@anthropic-ai/sdk` yourself if you enable the AI features).
21
+
22
+ ## Table of Contents
23
+
24
+ - [Features](#features)
25
+ - [Installation](#installation)
26
+ - [Quick Start](#quick-start)
27
+ - [Mounting the client portal](#mounting-the-client-portal)
28
+ - [Configuration](#configuration)
29
+ - [Deployment adapters](#deployment-adapters)
30
+ - [API Endpoints](#api-endpoints)
31
+ - [Webhooks](#webhooks)
32
+ - [Collections](#collections)
33
+ - [Package Exports](#package-exports)
34
+ - [Requirements](#requirements)
35
+ - [Security](#security)
36
+ - [Performance](#performance)
37
+ - [Troubleshooting](#troubleshooting)
38
+ - [FAQ](#faq)
39
+ - [Upgrading](#upgrading)
40
+ - [Contributing](#contributing)
41
+ - [Changelog](#changelog)
42
+ - [Support](#support)
43
+ - [License](#license)
44
+
45
+ ## Features
46
+
47
+ - **Ticketing** — statuses, priorities, categories, tags, merge, split, snooze, scheduled replies,
48
+ internal notes, collaborators, and an append-only activity log (`create` and `update` are denied
49
+ to everyone; only hooks write to it).
50
+ - **Agent inbox** — a keyboard-driven inbox view, bulk actions, macros, canned responses, and typing
51
+ and presence indicators.
52
+ - **SLA** — per-team or default policies, business-hours deadlines, pause-on-hold, escalation, and a
53
+ `sla-check` endpoint that lists what is breaching.
54
+ - **Automation** — an `automation-rules` collection (event → conditions → actions) and optional
55
+ round-robin assignment.
56
+ - **AI assists** — sentiment, reply suggestion, multi-style rewrite, per-ticket cached synthesis, a
57
+ knowledge-base chatbot, a client-intelligence summary, and an autonomous agent that either answers
58
+ from the knowledge base or escalates. Anthropic, or `provider: 'ollama'` to reach a self-hosted
59
+ gateway through the same SDK pointed at your own `OLLAMA_API_URL`.
60
+ - **Omnichannel** — inbound and outbound email with open tracking, live chat (SSE in the portal
61
+ widget, polling in the shipped agent console; the portal widget stays hidden until
62
+ `NEXT_PUBLIC_ENABLE_CHAT_WIDGET=true`), and a normalized inbound webhook for WhatsApp /
63
+ Messenger.
64
+ - **Client portal** — password login, email 2FA, Google OAuth, ticket list and detail, FAQ, profile,
65
+ GDPR data export and account deletion. Shipped as a template you copy (see below).
66
+ - **Time tracking and billing** — timer and manual entries, a per-ticket rollup, a time dashboard,
67
+ per-project pre-billing, and an invoice rendered as HTML or PDF.
68
+ - **Reporting** — dashboard KPIs, CSAT and NPS, email statistics, CSV export.
69
+ - **Notifications** — admin notifications, Web Push (VAPID), and daily/weekly digest emails.
70
+ - **Webhooks** — HMAC-SHA256 signed outbound deliveries on ticket events.
71
+ - **i18n** — French and English catalogs across the admin views, selectable per user.
72
+
73
+ ## Installation
5
74
 
6
- <div align="center">
7
-
8
- [![npm version](https://img.shields.io/npm/v/@consilioweb/payload-support?color=1f8a5b&label=npm)](https://www.npmjs.com/package/@consilioweb/payload-support)
9
- [![MIT License](https://img.shields.io/badge/license-MIT-1f8a5b)](LICENSE)
10
- [![Node.js](https://img.shields.io/badge/node-18+-1f8a5b)](https://nodejs.org)
11
- [![Payload](https://img.shields.io/badge/payload-3.x-1f8a5b)](https://payloadcms.com)
12
- [![Tests](https://img.shields.io/badge/tests-132%20passing-1f8a5b)](src/__tests__)
13
- [![TypeScript](https://img.shields.io/badge/typescript-strict-1f8a5b)](https://www.typescriptlang.org)
14
-
15
- </div>
16
-
17
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
18
-
19
- ## 📑 Table of Contents
20
-
21
- - [Features](#-features)
22
- - [Quick Start](#-quick-start)
23
- - [Installation](#-installation)
24
- - [Usage](#-usage)
25
- - [API Reference](#-api-reference)
26
- - [Configuration](#-configuration)
27
- - [Performance](#-performance)
28
- - [Examples](#-examples)
29
- - [FAQ](#-faq)
30
- - [Troubleshooting](#-troubleshooting)
31
- - [Security](#-security)
32
- - [Contributing](#-contributing)
33
- - [Changelog](#-changelog)
34
- - [Roadmap](#-roadmap)
35
- - [Support](#-support)
36
- - [License](#-license)
37
-
38
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
39
-
40
- ## ✨ Features
41
-
42
- <table>
43
- <tr>
44
- <td width="50%">
45
-
46
- **🎫 Complete ticketing**
47
-
48
- Statuses, priorities, categories, tags, merge/split, snooze, scheduled replies, internal notes, an immutable activity log and a keyboard-driven Superhuman-style inbox.
49
-
50
- </td>
51
- <td width="50%">
52
-
53
- **⏱️ SLA & automation**
54
-
55
- SLA policies (business hours + pause-on-hold), escalation, macros, round-robin, and a **visual automation-rules engine** (conditions → actions).
56
-
57
- </td>
58
- </tr>
59
- <tr>
60
- <td width="50%">
61
-
62
- **🤖 Built-in AI**
63
-
64
- Sentiment, reply suggestions, multi-style rewriting, cached synthesis, a KB chatbot, and an **autonomous AI agent** that answers or escalates. Anthropic, OpenAI or self-hosted Ollama.
65
-
66
- </td>
67
- <td width="50%">
68
-
69
- **💬 Omnichannel & portal**
70
-
71
- Inbound/outbound email, live chat (SSE), widget, a full **client portal** (auth, 2FA, Google OAuth), knowledge base and deflection.
72
-
73
- </td>
74
- </tr>
75
- <tr>
76
- <td width="50%">
77
-
78
- **💶 Time tracking & billing**
75
+ ```bash
76
+ pnpm add @consilioweb/payload-support
77
+ # or: npm install @consilioweb/payload-support
78
+ # or: yarn add @consilioweb/payload-support
79
+ ```
79
80
 
80
- Timer, manual entries, time dashboard, per-project pre-billing and a **print-ready invoice** (HTML → PDF).
81
+ **Peer dependencies** all six are required, none is optional:
81
82
 
82
- </td>
83
- <td width="50%">
83
+ ```bash
84
+ pnpm add payload@^3.37.0 @payloadcms/next@^3.37.0 next@^15.2.9 react@^19 react-dom@^19 lucide-react@">=0.300.0"
85
+ ```
84
86
 
85
- **📊 Reporting & privacy**
87
+ - `payload@^3.37.0` the Google OAuth endpoint imports `jwtSign` from the `payload` barrel, which
88
+ only exists from 3.37.0 on. Below that, the missing named export fails the whole
89
+ `payload.config.ts`, not just that endpoint.
90
+ - `@payloadcms/next` and `next` — the 13 admin views are registered unless you pass
91
+ `skipViews: true`, and each imports `DefaultTemplate` from `@payloadcms/next/templates` plus
92
+ `next/navigation` / `next/link` statically. Both are already present in any Payload 3 admin app.
93
+ - `lucide-react` — imported statically by `PendingEmailsView` and `TicketingSettingsView`. (The
94
+ third importer, `src/portal/LiveChat.tsx`, is a leftover: nothing in the shipped portal mounts
95
+ it.)
96
+
97
+ **AI runtime dependency** — the AI features load `@anthropic-ai/sdk` through a runtime
98
+ `require`/dynamic `import` and it is marked `external` at build time. It is intentionally not a
99
+ dependency of this package, so install it yourself if you use `features.ai`, the chatbot, the
100
+ autonomous agent, ticket synthesis, client intelligence or the AI conversation import:
86
101
 
87
- Real-time dashboard, **CSAT + NPS**, email tracking, HMAC-signed webhooks, notification digests, CSV exports. 100% self-hostable.
102
+ ```bash
103
+ pnpm add @anthropic-ai/sdk
104
+ ```
88
105
 
89
- </td>
90
- </tr>
91
- </table>
106
+ After adding admin components, regenerate the import map:
92
107
 
93
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
108
+ ```bash
109
+ pnpm payload generate:importmap
110
+ ```
94
111
 
95
- ## 🚀 Quick Start
112
+ ## Quick Start
96
113
 
97
114
  ```ts
98
115
  // payload.config.ts
@@ -103,8 +120,7 @@ export default buildConfig({
103
120
  plugins: [
104
121
  supportPlugin({
105
122
  features: { ai: true, sla: true, timeTracking: true, chat: true },
106
- ai: { provider: 'anthropic', model: 'claude-haiku-4-5-20251001' },
107
- locale: 'fr',
123
+ ticketNumber: { prefix: 'TK-', padding: 6 },
108
124
  }),
109
125
  ],
110
126
  })
@@ -115,61 +131,236 @@ pnpm payload generate:importmap
115
131
  pnpm dev
116
132
  ```
117
133
 
118
- Open `/admin/support/inbox` for the agent inbox, or `/support` for the client portal. The plugin injects collections, API endpoints and admin views automatically — no external SaaS.
119
-
120
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
121
-
122
- ## 📦 Installation
123
-
124
- ### npm
125
- ```bash
126
- npm install @consilioweb/payload-support
134
+ Open `/admin/support/inbox` for the agent inbox and `/admin/support/settings` to pick the AI
135
+ provider, the sender addresses, the SLA targets and the runtime feature flags.
136
+
137
+ > **The client portal is not a route the plugin mounts.** `/support` returns 404 until you copy the
138
+ > portal template into your own `app/` directory — see
139
+ > [Mounting the client portal](#mounting-the-client-portal). Everything the portal talks to (the REST
140
+ > endpoints, the auth/2FA/OAuth endpoints, the collections) *is* installed by the plugin; only the
141
+ > Next.js pages have to live in your app.
142
+
143
+ ## Mounting the client portal
144
+
145
+ The portal ships as a **template you copy**, not as an importable subpath — and that is a
146
+ constraint, not an oversight: `src/portal/auth/layout.tsx` and three of its pages import
147
+ `@payload-config`, an alias that only exists inside your application, and `src/portal/layout.tsx`
148
+ renders its own `<html>`/`<body>` as a Next.js **root layout**. Neither can be resolved from
149
+ `node_modules`, so there is no `./portal` export and `plugin.ts` registers no Next route.
150
+
151
+ The sources are published in the tarball (`files` includes `src`), so copy them out of
152
+ `node_modules` into a route group of your own `app/` directory. Two directories have to be
153
+ renamed on the way in — the shipped names are plain folders, not Next.js route syntax — and
154
+ `layout.tsx` has to sit at the **group** level, because it renders its own `<html>`/`<body>`
155
+ and only one root layout per group may do that (the same convention `create-payload-app`
156
+ uses for its `(payload)` / `(frontend)` groups):
157
+
158
+ ```text
159
+ src/app/
160
+ └── (support)/
161
+ ├── layout.tsx ← src/portal/layout.tsx (root layout of the group)
162
+ └── support/
163
+ ├── page.tsx ← src/portal/page.tsx → /support
164
+ ├── login/ register/ forgot-password/ reset-password/
165
+ └── (auth)/ ← src/portal/auth/ (route group: no URL segment)
166
+ ├── layout.tsx (auth guard + header + chatbot and chat widgets)
167
+ ├── dashboard/ faq/ profile/
168
+ └── tickets/
169
+ ├── new/
170
+ └── [id]/ ← src/portal/auth/tickets/detail/ (the page reads `params.id`)
127
171
  ```
128
172
 
129
- ### yarn
130
173
  ```bash
131
- yarn add @consilioweb/payload-support
174
+ mkdir -p 'src/app/(support)/support'
175
+ cp -R node_modules/@consilioweb/payload-support/src/portal/. 'src/app/(support)/support/'
176
+ cd 'src/app/(support)'
177
+ mv support/layout.tsx layout.tsx
178
+ mv support/auth 'support/(auth)'
179
+ mv 'support/(auth)/tickets/detail' 'support/(auth)/tickets/[id]'
132
180
  ```
133
181
 
134
- ### pnpm
135
- ```bash
136
- pnpm add @consilioweb/payload-support
137
- ```
182
+ That yields `/support`, `/support/login`, `/support/register`, `/support/forgot-password`,
183
+ `/support/reset-password` and, behind the auth layout, `/support/dashboard`, `/support/faq`,
184
+ `/support/profile`, `/support/tickets/new` and `/support/tickets/<id>` — exactly the paths the
185
+ plugin's notification emails and the portal's own links point to, so the renames are
186
+ mandatory, not cosmetic.
138
187
 
139
- **Peer dependencies:** `payload@^3`, `react@^18 || ^19`, `react-dom@^18 || ^19`, `next@^14 || ^15 || ^16`. `lucide-react` and the `@payloadcms/*` packages are optional depending on the features you enable. After adding admin components, run `pnpm payload generate:importmap`.
188
+ What your app must provide for those pages to work:
140
189
 
141
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
190
+ | Requirement | Why |
191
+ |---|---|
192
+ | The `@payload-config` alias | The authenticated pages call `getPayload({ config })` directly. Standard in a `create-payload-app` project. |
193
+ | **Tailwind CSS** | 28 of the 35 portal files are styled with Tailwind utility classes only. Without Tailwind the portal renders unstyled. |
194
+ | `@payloadcms/richtext-lexical` | `auth/faq/page.tsx` renders knowledge-base entries with `RichText` from `@payloadcms/richtext-lexical/react`. It is **not** a peer dependency of this plugin — add it yourself if you mount the FAQ page. |
195
+ | `NEXT_PUBLIC_SERVER_URL` | Email and portal links are built from it. |
196
+ | `NEXT_PUBLIC_SUPPORT_PHONE` | The ticket detail page prints a support phone number; without this variable it prints the placeholder `01 23 45 67 89` to your clients. |
142
197
 
143
- ## 💻 Usage
198
+ `lucide-react` is **not** needed by the pages above: the only portal file importing it is
199
+ `src/portal/LiveChat.tsx`, which the shipped `(auth)` layout never mounts. It stays a required peer
200
+ for the admin views.
144
201
 
145
- ### Basic
202
+ The `(auth)` layout mounts two widgets. `ChatbotWidget` is always rendered; `ChatWidget` — the
203
+ floating live-chat FAB — returns `null` unless the host app sets
204
+ `NEXT_PUBLIC_ENABLE_CHAT_WIDGET=true`, whatever `features.chat` says.
146
205
 
147
- ```ts
148
- // All features are on by default turn off what you don't need:
149
- supportPlugin({ features: { chat: false, pendingEmails: false }, locale: 'en' })
150
- ```
206
+ Because it is a copy, the portal is yours to restyle and re-brand; the trade-off is that
207
+ plugin upgrades do not update it. Diff it against
208
+ `node_modules/@consilioweb/payload-support/src/portal` after a version bump.
151
209
 
152
- ### Advanced
210
+ ## Configuration
153
211
 
154
- ```ts
155
- supportPlugin({
156
- features: { ai: true, sla: true, roundRobin: true, webhooks: true, snooze: true },
157
- rateLimitStore: 'payload', // shared, persistent limits for multi-instance deployments
158
- ticketNumber: { prefix: 'TK-', padding: 6 },
159
- ai: { provider: 'ollama', model: 'qwen2.5', baseUrl: process.env.OLLAMA_API_URL },
160
- email: { fromName: 'Support ACME', fromAddress: 'support@acme.com', replyTo: 'support@acme.com' },
161
- allowedEmailDomains: ['acme.com'], // restrict OAuth auto-registration
162
- collectionSlugs: { tickets: 'support-tickets' }, // slug overrides
163
- navGroup: 'Support',
164
- basePath: '/support',
165
- })
212
+ ### Plugin options
213
+
214
+ | Option | Type | Default | Description |
215
+ |---|---|---|---|
216
+ | `features` | `SupportFeatures` | see below | Build-time feature flags — decide which collections, endpoints and views exist. |
217
+ | `rateLimitStore` | `RateLimitStore \| 'payload'` | in-memory | Shared storage for the endpoint rate limits. `'payload'` adds the `support-rate-limits` collection. |
218
+ | `ticketNumber` | `{ prefix?, padding? }` | `{ prefix: 'TK-', padding: 4 }` | Sequential ticket-number formatting, backed by an atomic counter. |
219
+ | `capabilities` | `SupportCapabilities` | — | Host adapters for SMS, inbound email, digests, AI titles/summaries, detailed billing, volunteering, thread cleanup and project suggestions. See [Deployment adapters](#deployment-adapters). |
220
+ | `basePath` | `string` | `'/support'` | Prefix of the admin view routes (`/admin<basePath>/inbox`, …). The conversation-import view is the exception: it is registered at the fixed `/admin/import-conversation`. |
221
+ | `userCollectionSlug` | `string` | `'users'` | The staff/agent auth collection. |
222
+ | `allowedEmailDomains` | `string[]` | — | Restricts Google OAuth **auto-registration** to these domains. Existing accounts are unaffected. |
223
+ | `collectionSlugs` | `object` | see [Collections](#collections) | Per-collection slug overrides. |
224
+ | `notificationSlug` | `string` | `'admin-notifications'` | Collection the plugin writes admin notifications to. |
225
+ | `conversationComponent` | `string` | `'@consilioweb/payload-support/components/TicketConversation'` | Import path of the conversation UI field rendered on a ticket. |
226
+ | `projectCollectionSlug` | `string` | — | When set, adds a `project` relationship to tickets (and enables per-project billing). |
227
+ | `documentsCollectionSlug` | `string` | — | When set, adds quote/invoice upload fields to tickets. |
228
+ | `skipCollections` | `boolean` | `false` | Do not inject the collections. |
229
+ | `skipViews` | `boolean` | `false` | Do not inject the admin views. |
230
+ | `skipEndpoints` | `boolean` | `false` | Do not inject the REST endpoints. |
231
+
232
+ ### Options accepted but not read
233
+
234
+ Four keys are part of `SupportPluginConfig` but no code path reads them. They are listed here so you
235
+ do not configure them and wonder why nothing changes:
236
+
237
+ | Option | What actually decides | Where to set it |
238
+ |---|---|---|
239
+ | `ai` (`AIProviderConfig`) | `settings.ai.provider` / `settings.ai.model` | Admin → Support → Settings, or `POST /api/support/settings`. Defaults to `anthropic` / `claude-haiku-4-5-20251001`. |
240
+ | `email` (`EmailConfig`) | `settings.email.*`, falling back to `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | Same settings view, or the environment. |
241
+ | `locale` | The per-user preference | `GET`/`POST /api/support/user-prefs` (defaults to `fr`). |
242
+ | `navGroup` | — | Nowhere: each collection hard-codes its own `admin.group`. 20 sit under `Support`, three under `Gestion` (`canned-responses`, `ticket-activity-log`, `time-entries`), and `support-counters` / `support-rate-limits` declare none — they are `hidden` anyway. |
243
+
244
+ ### Build-time feature flags
245
+
246
+ `supportPlugin({ features })` accepts `SupportFeatures`. Every flag defaults to `true` except
247
+ `roundRobin` and `customStatuses`; `autoCloseDays` defaults to `7`.
248
+
249
+ | Flag | Default | Turning it off removes |
250
+ |---|---|---|
251
+ | `ai` | `true` | `client-summaries`, `/support/ai`, `/support/ai-agent`, `/support/client-intelligence`, `/support/ticket-synthesis` |
252
+ | `timeTracking` | `true` | `time-entries`, `/support/billing`, `/support/billing/invoice`, the Time dashboard view |
253
+ | `chat` | `true` | `chat-messages`, the four chat endpoints, the Chat view |
254
+ | `emailTracking` | `true` | `email-logs`, `/support/email-stats`, `/support/track-open`, the Tracking view |
255
+ | `pendingEmails` | `true` | `pending-emails`, `/support/pending-emails/:id/process`, the Pending emails view |
256
+ | `sla` | `true` | `sla-policies`, `/support/sla-check` |
257
+ | `webhooks` | `true` | `webhook-endpoints` (and therefore every outbound delivery) |
258
+ | `macros` | `true` | `macros`, `/support/apply-macro` |
259
+ | `authLogs` | `true` | `auth-logs` |
260
+ | `customStatuses` | `false` | *(off by default)* `ticket-statuses`, `/support/statuses` |
261
+ | `autoClose` | `true` | `/support/auto-close`, `/support/send-reminder` |
262
+ | `autoCloseDays` | `7` | — (projection of `settings.autoClose.daysBeforeClose`) |
263
+ | `snooze` | `true` | `/support/process-snooze` |
264
+ | `scheduledReplies` | `true` | `/support/process-scheduled` |
265
+ | `satisfaction` | `true` | `/support/satisfaction` |
266
+ | `merge` | `true` | `/support/merge-tickets` |
267
+ | `splitTicket` | `true` | `/support/split-ticket` |
268
+ | `bulkActions` | `true` | `/support/bulk-action` |
269
+ | `collisionDetection` | `true` | `/support/typing`, `/support/presence` |
270
+ | `signatures` | `true` | `/support/signature` |
271
+ | `chatbot` | `true` | `/support/chatbot` |
272
+ | `roundRobin` | `false` | *(off by default)* `/support/round-robin-config` |
273
+ | `canned`, `externalMessages`, `clientHistory`, `activityLog` | `true` | Nothing at build time — they exist as runtime flags (below) and only hide UI sections. |
274
+ | `commandPalette`, `knowledgeBase` | `true` | Nothing — no code reads either flag, at build time or at runtime. |
275
+
276
+ ### Build-time vs runtime feature flags
277
+
278
+ Two distinct things share the word "features", and they act at different moments:
279
+
280
+ | | `supportPlugin({ features })` | Runtime flags (Settings view) |
281
+ |---|---|---|
282
+ | Type | `SupportFeatures` (28 keys) | `TicketingFeatures` (16 keys) |
283
+ | Decides | which collections and endpoints **exist** | which UI sections are **shown**, plus round-robin and auto-close |
284
+ | Changed by | editing `payload.config.ts` + redeploy | an admin, in the ticketing settings view |
285
+ | Stored in | code | the `features` block of the `support-settings` preference row |
286
+
287
+ Turning a feature off at build time removes its collection and endpoints, so the matching runtime
288
+ flag has nothing left to show. Runtime flags only ever narrow what a build-time-enabled feature
289
+ exposes.
290
+
291
+ Runtime flags are read through `GET /api/support/settings` and written through
292
+ `POST /api/support/settings` (admin-only). They apply to every browser and to server-side code — the
293
+ auto-assign hook and the auto-close cron read the same values. `localStorage` is still written, but
294
+ only as a cache so an admin screen stays usable when the settings call fails; it is never
295
+ authoritative.
296
+
297
+ Two flags are projections, deliberately not stored twice: `features.autoClose` mirrors
298
+ `settings.autoClose.enabled`, and `features.autoCloseDays` mirrors
299
+ `settings.autoClose.daysBeforeClose`.
300
+
301
+ > **Upgrading from ≤ 2.0.1** — flags used to live in each browser's `localStorage`
302
+ > (`ticketing_features`). The first admin view loaded after the upgrade pushes that browser's values
303
+ > to the server, once, if the server has none yet; from then on the server wins. Nothing to run by
304
+ > hand, nothing is lost. If several machines disagree, the first one to load seeds the server and the
305
+ > others adopt its copy.
306
+
307
+ ### Environment variables
308
+
309
+ | Variable | Required | Description |
310
+ |---|---|---|
311
+ | `PAYLOAD_SECRET` | yes | Payload secret. Also signs the 2FA tokens and the email tracking pixels; the 2FA endpoint refuses to operate without it. |
312
+ | `NEXT_PUBLIC_SERVER_URL` | yes | Public URL used to build every email and portal link. |
313
+ | `CRON_SECRET` | for the cron endpoints | Expected value of the `x-cron-secret` header. |
314
+ | `ANTHROPIC_API_KEY` | with the `anthropic` provider | Anthropic API key. |
315
+ | `OLLAMA_API_URL` | with the `ollama` provider | Base URL of your own AI gateway, used as the SDK `baseURL`. **Mandatory** — there is no default host, and the AI paths throw `500 "provider 'ollama' requires OLLAMA_API_URL"` rather than sending ticket content anywhere you did not choose. |
316
+ | `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` | for portal OAuth | Google sign-in on the portal. |
317
+ | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | for Web Push | Generate with `npx web-push generate-vapid-keys`. |
318
+ | `VAPID_SUBJECT` | optional | Push contact (`mailto:` or URL). Defaults to `mailto:support@example.com`. |
319
+ | `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | optional | Fallback sender / reply-to addresses when the settings row leaves them empty. |
320
+ | `SUPPORT_CLIENT_BCC` | optional | Archive mailbox blind-copied on client-facing emails. Use a dedicated address, not `support@`, or the inbound anti-loop guard will re-create tickets from the copies. |
321
+ | `SUPPORT_TEAM_SCOPING` | optional | `1` or `true` to scope agents to their own team's tickets. |
322
+ | `SUPPORT_WEBHOOK_SECRET` | for machine imports | Expected value of the `x-webhook-secret` header on `POST /api/support/import-conversation`. |
323
+ | `CHANNELS_WEBHOOK_SECRET` | for social channels | Expected value of the `x-channel-secret` header on `POST /api/support/channels/webhook`. |
324
+ | `WHATSAPP_TOKEN` / `WHATSAPP_PHONE_ID` | for WhatsApp replies | Outbound WhatsApp credentials. |
325
+ | `MESSENGER_PAGE_TOKEN` | for Messenger replies | Outbound Messenger page token. |
326
+ | `CONTACT_EMAIL` | optional | Address treated as "the admin" when importing an existing email conversation. |
327
+ | `NEXT_PUBLIC_ENABLE_CHAT_WIDGET` | to show the portal chat widget | `'true'` mounts the floating live-chat widget in the portal `(auth)` layout. Anything else — including leaving it unset — makes `ChatWidget` return `null`, so `features.chat: true` on its own displays nothing to clients. |
328
+ | `NEXT_PUBLIC_SUPPORT_PHONE` | for the portal ticket page | Support phone number shown on the portal ticket detail page. Set it: the fallback is the placeholder `01 23 45 67 89`. |
329
+ | `NEXT_PUBLIC_SUPPORT_SEND_ALIASES` | optional | Comma-separated "send as" identities offered in the agent composer. Empty by default, and then the composer offers only the agent themselves. |
330
+ | `NEXT_PUBLIC_SMTP_HOST` / `NEXT_PUBLIC_SMTP_PORT` | optional | Shown read-only in the Settings view. Informational only — mail is sent through `payload.sendEmail`, so these change nothing about delivery. |
331
+ | `SUPPORT_CHATBOT_MAX_PER_HOUR` | optional | Global hourly ceiling on `POST /api/support/chatbot`, all callers combined (default `200`). The per-IP window is keyed on `X-Forwarded-For`, which an anonymous caller can rotate at will; this unkeyed ceiling is what bounds the Anthropic bill. Trade-off to accept knowingly: being unkeyed, it turns a cost abuse into an availability one — one anonymous caller burning the quota mutes the chatbot for every visitor until the next window. On a high-traffic public portal, raise it and enforce the per-IP limit at the reverse proxy, where the real client address is known. |
332
+ | `SUPPORT_ALLOW_INSECURE_WEBHOOKS` | local dev only | `1` allows `http://` webhook endpoint URLs. Without it only `https://` is accepted, at save time and at delivery time. |
333
+
334
+ ### Cron jobs
335
+
336
+ Four endpoints are meant to be called by a scheduler and are guarded by the `x-cron-secret` header.
337
+ Note that `auto-close` is a **GET**:
338
+
339
+ ```bash
340
+ # Hourly
341
+ curl -X GET https://your-app/api/support/auto-close -H "x-cron-secret: $CRON_SECRET"
342
+ curl -X POST https://your-app/api/support/process-snooze -H "x-cron-secret: $CRON_SECRET"
343
+ curl -X POST https://your-app/api/support/process-scheduled -H "x-cron-secret: $CRON_SECRET"
344
+
345
+ # Daily / weekly
346
+ curl -X POST https://your-app/api/support/process-digests -H "x-cron-secret: $CRON_SECRET" \
347
+ -H "Content-Type: application/json" -d '{"frequency":"daily"}'
166
348
  ```
167
349
 
168
- ### Deployment adapters
350
+ ### Automation rules
351
+
352
+ `automation-rules` is edited in the admin, no code required. A rule has a trigger
353
+ (`ticket_created`, `ticket_updated` or `ticket_status_changed`), a match mode (`all` / `any`), a list
354
+ of conditions (field, `equals` / `not_equals` / `contains`, value) and a list of actions
355
+ (`set_status`, `set_priority`, `set_category`, `assign`, `add_tag`). For example: trigger
356
+ `ticket_created`, condition `category equals bug`, action `set_priority = urgent`.
169
357
 
170
- Version 2 keeps provider-specific code in the host application while the plugin owns the
171
- generic support workflow. Optional capabilities include SMS, inbound email, digests, AI
172
- titles and summaries, detailed billing, volunteering, thread cleanup and project suggestions.
358
+ ## Deployment adapters
359
+
360
+ The plugin owns the generic support workflow; anything provider-specific stays in your application
361
+ and is injected through `capabilities`. Every key is optional and a missing adapter simply disables
362
+ the corresponding behaviour (`inboundEmail`, `projectSuggestions` and `aiTitles` also gate the
363
+ endpoints they need).
173
364
 
174
365
  ```ts
175
366
  supportPlugin({
@@ -192,24 +383,79 @@ supportPlugin({
192
383
  })
193
384
  ```
194
385
 
195
- ### Upgrading from 1.x
196
-
197
- Version 2.0 is a security and ownership release with intentional breaking changes:
198
-
199
- 1. Generate an additive Payload migration for the new `support-counters` collection and,
200
- when `rateLimitStore: 'payload'` is enabled, `support-rate-limits`. Run it before restart.
201
- 2. Regenerate Payload types and the admin import map.
202
- 3. Remove duplicated host routes and enable the plugin endpoints (`skipEndpoints: false`).
203
- 4. Read portal authentication exclusively from the `HttpOnly` cookie. Login, OAuth and 2FA
204
- responses no longer expose the JWT in JSON.
205
- 5. Send cron and webhook secrets only through their configured headers. Query-string secrets
206
- are rejected.
207
- 6. Custom rate-limit stores must implement the asynchronous `RateLimitStore` interface.
208
-
209
- The process-local memory store remains the default for development and single-instance use.
210
- Use `rateLimitStore: 'payload'` or a shared custom store in production with multiple instances.
211
-
212
- ### Run the AI agent on a ticket
386
+ | Capability | Shape | Effect |
387
+ |---|---|---|
388
+ | `sms` | `{ adapter, buildMessage? }` | Sends an SMS alongside the client notification. |
389
+ | `digests` | `boolean` | **Inert** — no code reads it. `notification-queue` and `POST /api/support/process-digests` are registered unconditionally. |
390
+ | `inboundEmail` | `{ handle, secret?, secretHeader? }` | Registers `POST /api/support-webhook/inbound-email`, validated and rate-limited by the plugin. |
391
+ | `aiTitles` | `{ generate }` | Registers `POST /api/support/ticket-title` and `POST /api/support/generate-missing-titles`, and adds the title fields on clients. |
392
+ | `aiSummaries` | `{ generate }` | Overrides the default resolved-ticket synthesis generator. |
393
+ | `detailedBilling` | `boolean` | Adds the billing-line fields and their invoicing state. |
394
+ | `volunteering` | `boolean` | Adds pro-bono tracking and its estimated value. |
395
+ | `threadCleanup` | `{ clean }` | Best-effort cleanup of noisy messages after an inbound client reply. |
396
+ | `projectSuggestions` | `{ suggest }` | Registers `POST /api/support/suggest-projects`. |
397
+
398
+ ## API Endpoints
399
+
400
+ All paths are relative to `/api`. "Staff" means a user of `userCollectionSlug` (default `users`);
401
+ "client" means a `support-clients` user. The *Flag* column names the build-time feature flag that has
402
+ to stay enabled for the endpoint to be registered.
403
+
404
+ ### Tickets and agent tools
405
+
406
+ | Method | Path | Access | Flag | Purpose |
407
+ |---|---|---|---|---|
408
+ | `GET` | `/support/search` | staff | — | Global search over tickets, messages, clients and the knowledge base. |
409
+ | `GET` | `/support/kb/search` | public | — | Knowledge-base search (used by the portal and the chatbot). |
410
+ | `GET` | `/support/statuses` | authenticated | `customStatuses` | Ticket statuses, sorted. |
411
+ | `POST` | `/support/bulk-action` | staff | `bulkActions` | Apply one action to many tickets. |
412
+ | `POST` | `/support/merge-tickets` | staff | `merge` | Merge a source ticket into a target. |
413
+ | `POST` | `/support/split-ticket` | staff | `splitTicket` | Extract a message into a new ticket. |
414
+ | `POST` | `/support/apply-macro` | staff | `macros` | Run a multi-action macro on a ticket. |
415
+ | `POST` | `/support/tickets/:id/escalate` | staff | — | Move a ticket to `escalated`. |
416
+ | `POST` | `/support/tickets/:id/transfer` | staff or ticket owner | — | Email a conversation recap to an external address. |
417
+ | `POST` | `/support/tickets/:id/invite` | staff or ticket owner | — | Invite a collaborator on the ticket. |
418
+ | `POST` | `/support/tickets/:id/feedback` | client | — | Submit a rating (1-5) and an optional comment. |
419
+ | `POST` | `/support/satisfaction` | client | `satisfaction` | Submit the CSAT survey of a resolved ticket. |
420
+ | `POST` | `/support/send-reminder` | staff | `autoClose` | Nudge a client whose ticket awaits their reply. |
421
+ | `POST` | `/support/resend-notification` | staff | — | Re-send the email notification of one message. |
422
+ | `POST` | `/support/import-conversation` | staff, or `x-webhook-secret` | — | Import an existing email thread as a ticket. |
423
+ | `GET`/`POST` | `/support/typing` | authenticated | `collisionDetection` | Typing indicator. |
424
+ | `GET`/`POST` | `/support/presence` | staff | `collisionDetection` | Who is looking at this ticket. |
425
+ | `GET`/`POST` | `/support/signature` | staff | `signatures` | Per-agent email signature. |
426
+ | `GET`/`POST` | `/support/user-prefs` | staff | — | Per-user locale and signature. |
427
+
428
+ ### Portal authentication and GDPR
429
+
430
+ | Method | Path | Access | Purpose |
431
+ |---|---|---|---|
432
+ | `POST` | `/support/login` | public, rate-limited | Portal password login. Sets an `HttpOnly` cookie; the JWT is never returned in JSON. |
433
+ | `POST` | `/support/2fa` | public, rate-limited | Email second factor. `action: 'send'` requires the short-lived `challenge` returned by `/support/login` (or by the Google callback) alongside `requires2FA` — without it an anonymous caller could burn a victim's send quota and lock them out. `action: 'verify'` takes the code. |
434
+ | `POST` | `/support/oauth/google` | public | Google sign-in, optionally restricted by `allowedEmailDomains`. |
435
+ | `GET` | `/support/email-stats` | **staff**, rate-limited | Email pipeline aggregate. Was reachable by any authenticated session before 3.0.1. |
436
+ | `GET` | `/support/export-data` | client | GDPR data export. |
437
+ | `POST` | `/support/delete-account` | client | GDPR right to erasure. |
438
+ | `POST` | `/support/merge-clients` | staff | Merge two client records. |
439
+
440
+ **Google OAuth, CSRF state.** `{ "action": "login" }` now answers with a `Set-Cookie:
441
+ support-oauth-state=…; HttpOnly; SameSite=Lax` alongside the `url` and `state` it already returned.
442
+ The callback reads that cookie **server-side** and compares it, in constant time, with the `state`
443
+ Google sends back — it no longer accepts a `cookieState` field in the request body, which any
444
+ non-browser caller could simply send twice. If you wrote your own Google button, drop `cookieState`
445
+ from the callback payload and let the browser carry the cookie (`credentials: 'include'` on a
446
+ cross-origin fetch). The callback also enforces 2FA: an account with `twoFactorEnabled` gets
447
+ `{ requires2FA: true }` and no token, exactly like `/support/login`.
448
+
449
+ ### AI
450
+
451
+ | Method | Path | Access | Flag | Purpose |
452
+ |---|---|---|---|---|
453
+ | `POST` | `/support/ai` | staff, rate-limited | `ai` | `sentiment`, `synthesis`, `suggest_reply` or `rewrite`. |
454
+ | `POST` | `/support/ai-agent` | staff, rate-limited | `ai` | Autonomous agent: answers from the knowledge base or escalates. |
455
+ | `POST` | `/support/ticket-synthesis` | staff, rate-limited | `ai` | Generate (or return the cached) per-ticket synthesis. |
456
+ | `GET`/`POST` | `/support/client-intelligence` | staff, rate-limited | `ai` | Cached client summary. |
457
+ | `POST` | `/support/chatbot` | public, rate-limited | `chatbot` | Knowledge-base chatbot used for deflection. |
458
+ | `POST` | `/support/seed-kb` | staff | — | Seed the knowledge base. |
213
459
 
214
460
  ```ts
215
461
  await fetch('/api/support/ai-agent', {
@@ -219,300 +465,322 @@ await fetch('/api/support/ai-agent', {
219
465
  })
220
466
  ```
221
467
 
222
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
223
-
224
- ## 🔌 API Reference
225
-
226
- ### `supportPlugin(config)`
227
-
228
- The Payload plugin. Adds collections, admin views, editor components and API endpoints.
229
-
230
- ```typescript
231
- supportPlugin(config?: SupportPluginConfig): Plugin
468
+ ### Live chat
469
+
470
+ | Method | Path | Access | Flag | Purpose |
471
+ |---|---|---|---|---|
472
+ | `GET`/`POST` | `/support/chat` | client | `chat` | Portal chat polling and send. |
473
+ | `GET` | `/support/chat-stream` | client | `chat` | SSE stream for the portal widget. |
474
+ | `GET`/`POST` | `/support/admin-chat` | staff | `chat` | Agent console: sessions, messages, send. |
475
+ | `GET` | `/support/admin-chat-stream` | staff | `chat` | SSE stream of the session list, or of one session's messages with `?session=`. Available for a custom agent console — the shipped Chat view polls `/support/admin-chat` instead. |
476
+
477
+ ### Reporting, billing and email
478
+
479
+ | Method | Path | Access | Flag | Purpose |
480
+ |---|---|---|---|---|
481
+ | `GET` | `/support/admin-stats` | staff | — | Dashboard KPIs, volume series, CSAT and NPS. |
482
+ | `GET` | `/support/export-csv` | staff | — | CSV export of the tickets. |
483
+ | `GET` | `/support/sla-check` | staff | `sla` | Tickets breaching or close to breaching SLA. |
484
+ | `GET` | `/support/billing` | staff | `timeTracking` | Pre-billing for a period, optionally per project. |
485
+ | `GET` | `/support/billing/invoice` | staff | `timeTracking` | Invoice for the period, `format=html\|pdf`. |
486
+ | `GET` | `/support/email-stats` | authenticated | `emailTracking` | Email open/sent aggregates. |
487
+ | `GET` | `/support/track-open` | public, HMAC-signed | `emailTracking` | Tracking pixel. |
488
+ | `POST` | `/support/pending-emails/:id/process` | staff | `pendingEmails` | Turn a pending email into a ticket or a reply. |
489
+ | `DELETE` | `/support/purge-logs` | staff | — | Delete logs older than N days. |
490
+
491
+ ### Settings, notifications and machine-to-machine
492
+
493
+ | Method | Path | Access | Flag | Purpose |
494
+ |---|---|---|---|---|
495
+ | `GET`/`POST` | `/support/settings` | staff | — | Read and write the support settings, including the runtime feature flags. |
496
+ | `GET`/`POST` | `/support/round-robin-config` | staff | `roundRobin` | Read and toggle round-robin assignment. |
497
+ | `GET` | `/support/push/vapid-public-key` | public | — | VAPID public key for the browser subscription. |
498
+ | `POST` | `/support/push/subscribe` | staff | — | Register an agent's push subscription. |
499
+ | `GET` | `/support/auto-close` | `x-cron-secret` | `autoClose` | Remind, then close, inactive tickets. |
500
+ | `POST` | `/support/process-snooze` | `x-cron-secret` | `snooze` | Wake snoozed tickets. |
501
+ | `POST` | `/support/process-scheduled` | `x-cron-secret` | `scheduledReplies` | Release scheduled replies. |
502
+ | `POST` | `/support/process-digests` | `x-cron-secret` | — | Send the daily or weekly digests. |
503
+ | `POST` | `/support/channels/webhook` | `x-channel-secret` | — | Normalized inbound WhatsApp / Messenger webhook. |
504
+ | `POST` | `/support-webhook/inbound-email` | configured secret header | `capabilities.inboundEmail` | Inbound email, validated and bounded by the plugin. |
505
+ | `POST` | `/support/suggest-projects` | staff | `capabilities.projectSuggestions` | Deployment-specific project suggestions. |
506
+ | `POST` | `/support/ticket-title` | staff | `capabilities.aiTitles` | Generate a short display title for one ticket. |
507
+ | `POST` | `/support/generate-missing-titles` | staff | `capabilities.aiTitles` | Backfill the missing display titles. |
508
+
509
+ ## Webhooks
510
+
511
+ Create a row in `webhook-endpoints` with a URL, a `secret` and the events you want, and set it
512
+ `active`. Each event is delivered **once** per matching endpoint (up to 50, 10 s timeout each), as a
513
+ single `POST`:
514
+
515
+ ```http
516
+ Content-Type: application/json
517
+ User-Agent: PayloadSupport-Webhook/1.0
518
+ X-Webhook-Signature: <HMAC-SHA256 of the raw body, keyed with the endpoint secret>
519
+
520
+ { "event": "ticket_created", "data": { … }, "timestamp": "2026-09-07T10:00:00.000Z" }
232
521
  ```
233
522
 
234
- ### Exports
523
+ The signature header is only sent when the endpoint has a `secret`, so always set one. There is no
524
+ `X-Webhook-Secret` header any more — verify the HMAC signature instead. `lastTriggeredAt` and
525
+ `lastStatus` are written back on the endpoint after every attempt. Requires `features.webhooks`.
526
+
527
+ **The endpoint URL is validated as an SSRF target, not as free text.** Only `https://` is accepted
528
+ (set `SUPPORT_ALLOW_INSECURE_WEBHOOKS=1` for local `http://`), literal loopback / private /
529
+ link-local / IPv4-mapped-IPv6 hosts are rejected at save time, the hostname is re-resolved and
530
+ re-checked immediately before the request (DNS rebinding), and redirects are followed **manually**
531
+ so a `302` towards `127.0.0.1` or `169.254.169.254` cannot slip past the checks. An endpoint saved
532
+ before 3.0 with an `http://` URL or an internal host stops delivering and must be re-pointed — but
533
+ it stays editable: the check runs only on a URL you actually write, so such a row can still be
534
+ renamed or deactivated, and `lastStatus: 0` is still recorded on it so a dead endpoint is visible.
535
+
536
+ | Event | `data` |
537
+ |---|---|
538
+ | `ticket_created` | `ticketId`, `id`, `ticketNumber`, `subject`, `status`, `priority`, `category` |
539
+ | `ticket_resolved` | `ticketId`, `id`, `ticketNumber`, `subject`, `previousStatus` |
540
+ | `ticket_assigned` | `ticketId`, `id`, `ticketNumber`, `subject`, `assignedTo` |
541
+ | `ticket_replied` | `ticketId`, `messageId`, `authorType`, `body` (first 500 characters), plus `scheduled: true` when released by the scheduler |
542
+ | `sla_breached` | Selectable on an endpoint, but nothing dispatches it — subscribing to it delivers nothing. |
543
+
544
+ ## Collections
545
+
546
+ "Staff" is `userCollectionSlug` (default `users`), "client" is a `support-clients` user.
547
+
548
+ `collectionSlugs` accepts 19 keys: `tickets`, `ticketMessages`, `supportClients`, `timeEntries`,
549
+ `cannedResponses`, `ticketActivityLog`, `satisfactionSurveys`, `knowledgeBase`, `chatMessages`,
550
+ `pendingEmails`, `emailLogs`, `authLogs`, `webhookEndpoints`, `slaPolicies`, `macros`,
551
+ `ticketStatuses`, `ticketFeedback`, `rateLimits` and `counters`. Three caveats:
552
+
553
+ - **`media` has no key, and it is the blocking one.** The internal `CollectionSlugs` type carries a
554
+ `media` slug (default `media`) and uses it as the `relationTo` of the attachment fields on
555
+ `ticket-messages` and `pending-emails`, but `collectionSlugs` exposes no way to set it. If your
556
+ uploads collection is named anything else, Payload throws `InvalidFieldRelationship` at config
557
+ sanitisation and the whole `payload.config.ts` fails to build. Your uploads collection has to be
558
+ named `media`.
559
+ - Three slugs are genuinely fixed: `ticketFeedback` is accepted but ignored, and
560
+ `ticket-collaborators` and `client-summaries` have no key at all — those three files write their
561
+ own `slug:` as a literal.
562
+ - `notificationQueue`, `automationRules`, `supportTeams` and `pushSubscriptions` are a different
563
+ case: their collections *do* read `slugs.*`, only the public option type omits the keys. The
564
+ plumbing is there; widening the type would be enough.
565
+
566
+ | Slug | Injected | Role | Read | Write |
567
+ |---|---|---|---|---|
568
+ | `tickets` | always | The tickets themselves | staff; clients see the tickets they own or collaborate on | staff; a client create keeps only `subject`, `category`, `priority`, `project` |
569
+ | `ticket-messages` | always | Conversation, internal notes, scheduled replies | staff; clients see non-internal, already-sent messages of their tickets | staff; clients only on an accessible ticket, and may edit only their own messages |
570
+ | `support-clients` | always | Portal auth collection (2FA, OAuth, notification preferences) | staff; a client reads their own row | staff; a client updates their own row minus `tier`, `accountManager`, `opportunities`, `notes`, `googleId` and the 2FA internals |
571
+ | `ticket-collaborators` | always | Extra clients invited on a ticket — source of truth for client read scope | staff and the invited client | staff only (`update` denied to everyone); the invite endpoint writes with `overrideAccess`. **Fixed slug.** |
572
+ | `ticket-activity-log` | always | Append-only audit trail | staff; clients on their own tickets | nobody — hooks only (`create`/`update` denied) |
573
+ | `canned-responses` | always | Quick reply templates | staff | staff |
574
+ | `satisfaction-surveys` | always | CSAT after resolution | staff and the surveyed client | immutable once submitted |
575
+ | `ticket-feedback` | always | Portal rating and comment | staff and the author | immutable once submitted. **Fixed slug.** |
576
+ | `knowledge-base` | always | FAQ and chatbot corpus | staff; published entries publicly | staff |
577
+ | `notification-queue` | always | Digest queue | staff | hooks only |
578
+ | `automation-rules` | always | Event → conditions → actions | staff | staff |
579
+ | `support-teams` | always | Teams, used by team SLA and `SUPPORT_TEAM_SCOPING` | staff | staff |
580
+ | `push-subscriptions` | always | Agent Web Push subscriptions | staff | the subscribe endpoint only |
581
+ | `support-counters` | always | Atomic ticket-number counter | nobody | internal only |
582
+ | `auth-logs` | `authLogs` | Portal authentication audit | staff | system-generated |
583
+ | `time-entries` | `timeTracking` | Time spent, rolled up into `tickets.totalTimeMinutes` | staff | staff |
584
+ | `email-logs` | `emailTracking` | Sent/opened tracking | staff | system-generated |
585
+ | `webhook-endpoints` | `webhooks` | Outbound webhook subscriptions | staff | staff |
586
+ | `sla-policies` | `sla` | Default and per-team SLA targets | staff | staff |
587
+ | `macros` | `macros` | Multi-action shortcuts | staff | staff |
588
+ | `chat-messages` | `chat` | Live chat sessions | staff and the session's client | authenticated |
589
+ | `pending-emails` | `pendingEmails` | Inbound emails awaiting triage | staff | staff, or a create carrying a valid `x-webhook-secret` |
590
+ | `ticket-statuses` | `customStatuses` | Configurable statuses | authenticated | staff |
591
+ | `client-summaries` | `ai` | Cached client-intelligence summaries | staff | staff. **Fixed slug**, and its access rules compare against the literal `users` collection, so a renamed staff collection loses access to it. |
592
+ | `support-rate-limits` | `rateLimitStore: 'payload'` | Shared rate-limit counters | nobody | internal only |
593
+
594
+ ## Package Exports
595
+
596
+ | Subpath | Exposes | Formats | Environment |
597
+ |---|---|---|---|
598
+ | `@consilioweb/payload-support` | `supportPlugin`, the collection factories, the SLA and status-email hooks, `dispatchWebhook`, `generateTicketSynthesis`, the rate-limit stores, the settings and feature helpers, and the public types | ESM + CJS, typed | Server (`payload.config.ts`) |
599
+ | `@consilioweb/payload-support/client` | `SUPPORT_CLIENT_VERSION` — the `'use client'` barrel marker | ESM + CJS, typed | Client |
600
+ | `@consilioweb/payload-support/views` | The 13 admin views, for the Payload import map | **ESM only**, typed | Server components |
601
+ | `@consilioweb/payload-support/components/TicketConversation` | The conversation field component | **ESM only**, typed | Client |
602
+
603
+ The two view/component subpaths lost their CJS build in 3.0.0: the emitted `.cjs` barrels
604
+ `require`d their ESM siblings and threw `ERR_REQUIRE_ESM`, so they were unreachable. Payload 3 and
605
+ Next are ESM-first; import them with ESM.
235
606
 
236
- ```typescript
237
- import { supportPlugin, generateTicketSynthesis, dispatchWebhook } from '@consilioweb/payload-support'
607
+ ```ts
608
+ import { supportPlugin, dispatchWebhook, generateTicketSynthesis } from '@consilioweb/payload-support'
238
609
  import type { SupportPluginConfig, SupportFeatures } from '@consilioweb/payload-support'
239
610
  ```
240
611
 
241
- ### Key HTTP endpoints (under `/api/support`)
612
+ ## Requirements
242
613
 
243
- | Method | Path | Purpose |
614
+ | Requirement | Range | Source |
244
615
  |---|---|---|
245
- | `POST` | `/ai` | Sentiment, reply suggestion, rewrite, synthesis (admin). |
246
- | `POST` | `/ai-agent` | **Autonomous AI agent** — answers from the KB or escalates. |
247
- | `POST` | `/login`, `/2fa`, `/oauth/google` | Client portal auth (password, 2FA, OAuth). |
248
- | `GET` | `/admin-stats` | Dashboard KPIs (real volume series, CSAT, NPS). |
249
- | `GET` | `/billing`, `/billing/invoice` | Pre-billing + print-ready invoice. |
250
- | `POST` | `/process-snooze`, `/process-digests`, `/process-scheduled`, `/auto-close` | Cron jobs (guarded by `x-cron-secret`). |
251
-
252
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
253
-
254
- ## ⚙️ Configuration
255
-
256
- | Option | Type | Default | Description |
257
- |--------|------|---------|-------------|
258
- | `features` | `SupportFeatures` | all `true` | Toggle each feature on/off. |
259
- | `ai` | `AIProviderConfig` | `anthropic` | AI provider: `anthropic` \| `openai` \| `ollama` \| `custom`. |
260
- | `email` | `EmailConfig` | | `fromName`, `fromAddress`, `replyTo`. |
261
- | `rateLimitStore` | `RateLimitStore \| 'payload'` | memory | Persistent/shared storage for endpoint limits. |
262
- | `ticketNumber` | `{ prefix?, padding? }` | `TK-`, no padding | Atomic sequential ticket-number formatting. |
263
- | `capabilities` | `SupportCapabilities` | | Optional host adapters for SMS, inbound email, AI and deployment-specific workflows. |
264
- | `locale` | `'fr' \| 'en'` | `'fr'` | Admin/portal language. |
265
- | `basePath` | `string` | `'/support'` | Admin views prefix. |
266
- | `userCollectionSlug` | `string` | `'users'` | Agents collection. |
267
- | `allowedEmailDomains` | `string[]` | | Domains allowed for OAuth auto-registration. |
268
- | `collectionSlugs` | `object` | | Collection slug overrides. |
269
- | `skipCollections` / `skipViews` / `skipEndpoints` | `boolean` | `false` | Skip injecting that part. |
270
-
271
- ### Environment variables
272
-
273
- | Env var | Required | Description |
274
- |---------|----------|-------------|
275
- | `PAYLOAD_SECRET` | | Payload secret (also signs 2FA & tracking). |
276
- | `NEXT_PUBLIC_SERVER_URL` | | Public URL (email/portal links). |
277
- | `CRON_SECRET` | for crons | `x-cron-secret` header for the cron endpoints. |
278
- | `ANTHROPIC_API_KEY` / `OLLAMA_API_URL` | if AI | AI provider keys/URL. |
279
- | `GOOGLE_OAUTH_CLIENT_ID` / `_SECRET` | if OAuth | Portal Google sign-in. |
280
- | `VAPID_PUBLIC_KEY` / `VAPID_PRIVATE_KEY` | if push | Web Push keys (`npx web-push generate-vapid-keys`). |
281
- | `VAPID_SUBJECT` | optional | Push contact (`mailto:` or URL), defaults to `mailto:support@example.com`. |
282
- | `SUPPORT_TEAM_SCOPING` | optional | `1` to scope agents to their team's tickets. |
283
- | `SUPPORT_EMAIL` / `SUPPORT_REPLY_TO` | optional | From/reply-to addresses. |
284
-
285
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
286
-
287
- ## Performance
288
-
289
- Engineered to stay fast under load (no fabricated benchmarks figures depend on your volume and host):
616
+ | Node.js | `>=20.9.0` | `engines.node` |
617
+ | Payload | `^3.37.0` | `peerDependencies.payload` |
618
+ | `@payloadcms/next` | `^3.37.0` | `peerDependencies` |
619
+ | Next.js | `^15.2.9 \|\| ^16.0.0` | `peerDependencies` |
620
+ | React / React DOM | `^19.0.0` | `peerDependencies` |
621
+ | `lucide-react` | `>=0.300.0` | `peerDependencies` |
622
+ | `@anthropic-ai/sdk` | any recent version | Runtime-only, install it yourself if you use the AI features |
623
+ | `@payloadcms/richtext-lexical` | any 3.x | Only if you mount the portal FAQ page |
624
+
625
+ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outright under
626
+ `engine-strict` or a strict peer resolver.
627
+
628
+ ## Security
629
+
630
+ - **Cross-client isolation** reads on `tickets`, `ticket-messages` and `ticket-activity-log` are
631
+ constrained to the ticket ids a client owns or collaborates on, resolved server-side and failing
632
+ closed on an empty set. Client writes are validated against that same set.
633
+ - **Allow-listed client writes** a client-initiated ticket create keeps only `subject`, `category`,
634
+ `priority` and `project`; `client`, `status` and `source` are forced. A client updating their own
635
+ profile cannot touch `tier`, `accountManager`, `opportunities`, `notes`, `googleId` or the 2FA
636
+ internals.
637
+ - **Cookie-only portal JWTs** `HttpOnly`, `SameSite=Lax`, `Path=/`, 2 h max age, and `Secure`
638
+ outside development. Login, OAuth and 2FA responses never return the token in JSON.
639
+ - **2FA enforced server-side** (`beforeLogin`), and the endpoint refuses to run without
640
+ `PAYLOAD_SECRET` rather than falling back to an insecure default. OAuth verifies the Google email.
641
+ - **Stored-XSS protection** — message HTML is sanitized on write, not on render, and the portal
642
+ renders plain-text bodies as JSX (no hand-rolled HTML escaping) so an inbound email cannot inject
643
+ an attribute into a link.
644
+ - **Owner-scoped preferences** the plugin's `payload-preferences` rows (settings, per-agent
645
+ signature and locale) are read back with a `user.relationTo` constraint on the staff collection.
646
+ `payload-preferences` accepts a write from *any* authenticated principal, so reading by key alone
647
+ let a client own the server settings.
648
+ - **Auth-collection checks, not duck typing** every guard compares `user.collection` to the
649
+ configured slug: a user of another auth collection of the host app is not a support client.
650
+ - **HMAC everywhere it matters** webhook deliveries and tracking pixels are signed and verified in
651
+ constant time, with idempotent writes. Cron and webhook secrets are read from headers only; a
652
+ query-string secret is rejected.
653
+ - **Persistent rate limiting** on login, 2FA, chat, invitations, transfers, imports and every AI
654
+ endpoint. Use `rateLimitStore: 'payload'` (or your own shared store) on multi-instance deployments.
655
+ - **No silent third-party AI host** — the `ollama` provider fails loudly on a missing
656
+ `OLLAMA_API_URL` instead of defaulting to someone else's server.
657
+ - **Bounded inbound email** payloads and attachments.
658
+ - **Bounded outbound mail** — ticket transfers and collaborator invitations are capped per user over
659
+ a long window, not only per ticket (a client can create tickets at will).
660
+ - **SSRF-guarded outbound webhooks** scheme, literal host, resolved address and every redirect hop.
661
+ - **2FA also enforced on the Google OAuth path**, which mints its session outside `payload.login`
662
+ and therefore outside the `beforeLogin` hook.
663
+
664
+ ### Reporting security issues
665
+
666
+ Email **contact@consilioweb.fr** instead of opening a public issue.
667
+
668
+ ### Best practices
669
+
670
+ - Set a strong `PAYLOAD_SECRET` and a real `CRON_SECRET`.
671
+ - Restrict read access on your `media` collection to the ticket owner.
672
+ - Put the app behind a trusted proxy so `x-forwarded-for` is meaningful.
673
+ - Rotate every webhook endpoint secret that existed before 3.0.0 — the legacy transport sent it in
674
+ clear on every delivery.
675
+
676
+ ## Performance
677
+
678
+ No benchmarks are claimed here; the figures depend on your volume and host. What the code actually
679
+ does:
290
680
 
291
681
  | Concern | Approach |
292
- |---------|----------|
293
- | Settings reads | In-process cache (TTL + invalidation)avoids ~8 redundant DB reads per ticket mutation. |
294
- | List queries | SQLite indexes on filtered fields inbox/dashboard/SLA in `O(index)` instead of full scans. |
295
- | Email sends | Fire-and-forget the response no longer waits on the SMTP round-trip (−200-500 ms). |
296
- | Stats & billing | Paginated aggregation + bounded `select` never loads all tickets in memory. |
297
- | AI synthesis | Cached per ticket no LLM recompute on every view. |
298
-
299
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
300
-
301
- ## 📚 Examples
302
-
303
- ### Automation rule (no code)
304
-
305
- Create a **Automation Rules** entry: `event = ticket_created`, condition `category = bug`, action `set_priority = urgent`. New "bug" tickets become "urgent" automatically.
306
-
307
- ### Wire the cron jobs
308
-
309
- ```bash
310
- # Hourly: auto-close, snooze wake-up, scheduled replies
311
- curl -X POST https://your-app/api/support/auto-close -H "x-cron-secret: $CRON_SECRET"
312
- curl -X POST https://your-app/api/support/process-snooze -H "x-cron-secret: $CRON_SECRET"
313
- # Daily / weekly: notification digests
314
- curl -X POST https://your-app/api/support/process-digests -H "x-cron-secret: $CRON_SECRET" -d '{"frequency":"daily"}'
315
- ```
316
-
317
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
318
-
319
- ## ❓ FAQ
320
-
321
- <details>
322
- <summary><b>Which databases are supported?</b></summary>
323
-
324
- Built and tested on **SQLite** (`@payloadcms/db-sqlite`) with a sequential seed and `busyTimeout`. It works with any Payload adapter, but indexes and queries were validated on SQLite.
325
-
326
- </details>
327
-
328
- <details>
329
- <summary><b>What Node / Payload versions are supported?</b></summary>
330
-
331
- Node.js 18+, Payload 3.x, React 18 or 19.
332
-
333
- </details>
334
-
335
- <details>
336
- <summary><b>Is there TypeScript support?</b></summary>
337
-
338
- Yes — strict TypeScript, with business types exported (`SupportPluginConfig`, `SupportFeatures`, …).
682
+ |---|---|
683
+ | Settings reads | In-process cache with a 60 s TTL and explicit invalidation on save the `afterChange` hook chain reads the same preference row many times per ticket mutation. |
684
+ | List queries | Indexed fields on `tickets` for the columns the inbox, dashboard and SLA views filter on. |
685
+ | Stats and billing | Paginated aggregation (100 rows per page) with a bounded `select` never loads all tickets in memory. |
686
+ | Counters | The inbox counters and the portal poller call `/api/*/count` instead of fetching rows. |
687
+ | Client time totals | The CRM sums the per-ticket `totalTimeMinutes` rollup instead of scanning `time-entries`. |
688
+ | AI synthesis | Persisted on the ticket (`aiSummary`, `aiSummaryGeneratedAt`) and reused until forced. |
339
689
 
340
- </details>
690
+ ## Troubleshooting
341
691
 
342
- <details>
343
- <summary><b>Do I need an external AI service?</b></summary>
692
+ **`useServerFunctions must be used within ServerFunctionsProvider`** — align every `@payloadcms/*`
693
+ package on the same version, 3.75 or later.
344
694
 
345
- No. AI features are optional and you can run **self-hosted Ollama** to depend on no cloud (sovereignty/GDPR). Anthropic and OpenAI are also supported.
695
+ **`SQLITE_BUSY` / database is locked during seed** make the seed sequential (no `Promise.all` on
696
+ inserts) and add `busyTimeout: 10000` to the SQLite adapter.
346
697
 
347
- </details>
698
+ **Admin views do not load** — regenerate the import map (`pnpm payload generate:importmap`). In a
699
+ headless context, pass `skipViews: true`.
348
700
 
349
- <details>
350
- <summary><b>How do I migrate the schema in production?</b></summary>
701
+ **New fields or collections missing in production** — standalone builds do not run migrations. Push
702
+ the schema before deploying, including `googleId`, `twoFactorVerifiedAt`, `slaPausedAt`, `nps`,
703
+ `mentions`, and the `notification-queue`, `automation-rules`, `ticket-collaborators`,
704
+ `support-counters` collections.
351
705
 
352
- In standalone mode the schema isn't auto-migrated: generate/push the schema (new fields and collections) before deploying. See [Troubleshooting](#-troubleshooting).
706
+ **`Cannot find module '@anthropic-ai/sdk'`** install it in your app; the plugin does not bundle or
707
+ declare it.
353
708
 
354
- </details>
709
+ **`ERR_REQUIRE_ESM` on `@consilioweb/payload-support/views`** — that subpath is ESM-only since 3.0.0.
355
710
 
356
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
711
+ ## FAQ
357
712
 
358
- ## 🔧 Troubleshooting
713
+ **Which databases are supported?** — built and tested on SQLite (`@payloadcms/db-sqlite`) with a
714
+ sequential seed and `busyTimeout`. It works with any Payload adapter, but the indexes and queries
715
+ were validated on SQLite.
359
716
 
360
- ### `useServerFunctions must be used within ServerFunctionsProvider`
717
+ **Do I need an external AI service?** — no. The AI features are optional, and `provider: 'ollama'`
718
+ points the SDK at the `OLLAMA_API_URL` you choose, so no ticket content has to leave your
719
+ infrastructure. `provider: 'openai'` and `provider: 'custom'` are accepted by the `AIProviderConfig`
720
+ type but not implemented: at runtime everything that is not `ollama` goes to Anthropic.
361
721
 
362
- Use Payload **3.75+** for all `@payloadcms/*` packages (aligned versions).
722
+ **Is TypeScript supported?** yes, strict, with the business types exported
723
+ (`SupportPluginConfig`, `SupportFeatures`, `TicketData`, …). All four export subpaths ship
724
+ declarations.
363
725
 
364
- ### `SQLITE_BUSY` / database is locked during seed
726
+ **How do I migrate the schema in production?** — in standalone mode the schema is not
727
+ auto-migrated: generate and push it before deploying. See [Troubleshooting](#troubleshooting).
365
728
 
366
- Make the seed **sequential** (no `Promise.all` on inserts) and add `busyTimeout: 10000` to the SQLite adapter.
729
+ ## Upgrading
367
730
 
368
- ### Admin views don't load
731
+ ### 2.x 3.0
369
732
 
370
- Regenerate the import map after adding components: `pnpm payload generate:importmap`. In a plugin/headless context, use `skipViews: true` if you don't mount the admin UI.
733
+ 1. **Rotate the secret of every webhook endpoint.** The legacy transport sent it in clear in
734
+ `X-Webhook-Secret` on every delivery. Subscribers must now verify `X-Webhook-Signature`.
735
+ 2. **Set `OLLAMA_API_URL`** before restarting if you use `provider: 'ollama'` — the hard-coded
736
+ fallback host is gone and its absence is now a hard error.
737
+ 3. **Move any integration that authenticated as a portal client** to a staff account or to a
738
+ server-side call with `overrideAccess: true`: client writes are scoped and allow-listed now.
739
+ 4. **Recompute `tickets.totalTimeMinutes`** where time entries were deleted under 2.x. The new
740
+ `afterDelete` hook only fires on future deletions, and the CRM now trusts the rollup instead of
741
+ recomputing it — so a stale total keeps over-billing until something touches a time entry on that
742
+ ticket.
743
+ 5. **Import `./views` and `./components/TicketConversation` with ESM** — their CJS builds are gone.
744
+ 6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.37+, and install
745
+ `lucide-react` explicitly if you were relying on it being optional.
371
746
 
372
- ### New fields/collections missing in production
747
+ ### 1.x 2.0
373
748
 
374
- Standalone doesn't run migrations: push the schema for `googleId`, `twoFactorVerifiedAt`, `slaPausedAt`, `nps`, `mentions`, and the `notification-queue` / `automation-rules` collections.
375
-
376
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
377
-
378
- ## 🔐 Security
379
-
380
- Security is a first-class concern several guardrails are validated by integration tests.
381
-
382
- - **Cross-client isolation** a client can never read another's tickets/messages (filtered by owned tickets).
383
- - **2FA** enforced server-side (`beforeLogin`); **OAuth** verifies the Google email.
384
- - **Sanitization** of message HTML server-side (stored-XSS protection).
385
- - **HMAC-signed** webhooks and tracking pixels with constant-time verification and idempotent writes.
386
- - **Cookie-only JWTs** for portal authentication (`HttpOnly`, `Secure`, `SameSite=Lax`).
387
- - **Persistent rate limiting** for authentication, 2FA, chats, invitations, transfers and AI endpoints.
388
- - **Bounded inbound email** payloads and attachments; cron/webhook secrets are accepted in headers only.
389
-
390
- ### Reporting Security Issues
391
-
392
- Please email **contact@consilioweb.fr** instead of opening a public issue.
393
-
394
- ### Best Practices
395
-
396
- - ✅ Set a strong `PAYLOAD_SECRET` (never a default value)
397
- - ✅ Set `CRON_SECRET` to protect the cron endpoints
398
- - ✅ Restrict read access to the `media` collection to the ticket owner
399
- - ✅ Put the app behind a trusted proxy (`x-forwarded-for` header)
400
- - ✅ Keep the plugin up-to-date
401
-
402
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
403
-
404
- ## 🤝 Contributing
749
+ 1. Generate an additive Payload migration for the `support-counters` collection and, when
750
+ `rateLimitStore: 'payload'` is enabled, `support-rate-limits`. Run it before restart.
751
+ 2. Regenerate the Payload types and the admin import map.
752
+ 3. Remove duplicated host routes and enable the plugin endpoints (`skipEndpoints: false`).
753
+ 4. Read portal authentication exclusively from the `HttpOnly` cookie. Login, OAuth and 2FA responses
754
+ no longer expose the JWT in JSON.
755
+ 5. Send cron and webhook secrets only through their configured headers. Query-string secrets are
756
+ rejected.
757
+ 6. Custom rate-limit stores must implement the asynchronous `RateLimitStore` interface.
405
758
 
406
- Contributions are very welcome!
759
+ ## Contributing
407
760
 
408
- 1. **Fork** the repository
409
- 2. **Create** a feature branch (`git checkout -b feature/AmazingFeature`)
410
- 3. **Commit** your changes (`git commit -m 'feat: add AmazingFeature'`)
411
- 4. **Push** the branch (`git push origin feature/AmazingFeature`)
412
- 5. **Open** a Pull Request
761
+ 1. Fork the repository.
762
+ 2. Create a feature branch (`git checkout -b feature/AmazingFeature`).
763
+ 3. Commit your changes (`git commit -m 'feat: add AmazingFeature'`).
764
+ 4. Push the branch and open a Pull Request.
413
765
 
414
- Run the checks before submitting:
766
+ Run the checks before submitting — the suite is 179 vitest tests:
415
767
 
416
768
  ```bash
417
769
  pnpm typecheck && pnpm test && pnpm build
418
770
  ```
419
771
 
420
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
421
-
422
- ## 📝 Changelog
772
+ ## Changelog
423
773
 
424
774
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
425
775
 
426
- ### [2.0.1] — 2026-07-16
427
-
428
- - 🌍 Loads the complete shared FR/EN catalogs in every support admin view, fixing raw keys such as ticket statuses, relative dates, time tracking, tags and billing labels.
429
- - 🧭 Removes translation-key collisions in the support dashboard and settings views.
430
- - ✅ Adds regression coverage for all 518 literal translation keys used by the support UI and keeps both locales aligned (132 tests passing).
431
-
432
- ### [2.0.0] — 2026-07-16
433
-
434
- - 🔐 Security hardening for authentication, tracking pixels, webhook/cron secrets, HTML/AI output and inbound-email limits.
435
- - 🚦 Asynchronous persistent rate limiting with a Payload-backed store and memory fallback.
436
- - 🔢 Atomic persistent ticket counters with configurable prefix and padding.
437
- - 🧩 Typed deployment adapters for SMS, inbound email, AI workflows, billing, volunteering, cleanup and project suggestions.
438
- - 🏗️ The plugin is now the single owner of generic support collections, hooks, views and endpoints.
439
- - ✅ 124 tests covering the security regressions and new capabilities.
440
-
441
- ### [1.1.1] — 2026-06-26
442
-
443
- - 🐛 **Fix client ticket reopen**: allow clients to set `waiting_support` (the status the portal's "Reopen" button sends). It was silently rejected, leaving the ticket `resolved`.
444
-
445
- ### [1.1.0] — 2026-06-25
446
-
447
- - 🎯 **Per-team SLA policies & dashboards** (team policy overrides the default, `?teamId=` scoping).
448
- - 🔔 **Native push / browser notifications** (Web Push / VAPID, agent pushed on new client messages).
449
- - 🎭 **End-to-end UI test harness** (browser-driven admin via Playwright, `pnpm test:e2e`).
450
- - ✅ 109 integration tests (up from 102).
451
-
452
- ### [1.0.0] — 2026-06-25
453
-
454
- - ✨ Complete ticketing, SLA (+ pause-on-hold), automation & **visual rules engine**.
455
- - 🤖 AI (sentiment, suggestion, synthesis, chatbot) + **autonomous AI agent**.
456
- - 💬 Live chat, client portal (2FA, OAuth), knowledge base.
457
- - 💶 Time tracking, pre-billing & print-ready invoice.
458
- - 📊 CSAT + NPS, real volume series, digests, webhooks.
459
- - 🔐 Security hardening (cross-client isolation, anti-XSS, server-side 2FA).
460
- - ✅ Integration test harness (Payload + in-memory SQLite), 102 tests.
461
-
462
- <details>
463
- <summary><b>Previous versions (0.x)</b></summary>
464
-
465
- - **0.16.0** — Manual client reminder + auto-close after 24h
466
- - **0.15.0** — Next 16 compatibility + accumulated features
467
- - **0.9.0 → 0.9.13** — Enriched views (Client Intelligence, Billing), bundled RichTextEditor, code blocks in emails, inline message editing, enriched pre-billing, per-ticket cached AI synthesis, multi-style rewriting
468
- - **0.6.0 → 0.6.4** — Split build (`bundle:false`) for Next.js RSC compatibility + barrel exports
469
- - **0.5.0** — Full i18n across the 13 admin views
470
- - **0.4.0** — Per-user preferences (locale, signature) vs global settings
471
- - **0.3.0** — Feature parity with ConsilioWEB + `skipCollections` / `skipViews` / `skipEndpoints`
472
- - **0.2.0** — 2026-04-08 — Security overhaul, SSE live chat, webhooks, SLA, scheduled replies, client portal
473
- - **0.1.0** — 2026-04-08 — Initial scaffold: `supportPlugin()`, 15 collections, feature flags, AI provider abstraction
474
-
475
- </details>
476
-
477
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
478
-
479
- ## 🗺️ Roadmap
480
-
481
- - [x] Ticketing, SLA, time tracking & billing
482
- - [x] AI assists + autonomous AI agent
483
- - [x] Live chat, client portal, knowledge base
484
- - [x] Visual automation-rules engine
485
- - [x] Native binary PDF invoice (alongside the print-ready HTML)
486
- - [x] Full admin i18n (all views)
487
- - [x] Social channels (WhatsApp, Messenger)
488
- - [x] Multi-team / workspaces mode
489
- - [x] Per-team SLA policies & dashboards
490
- - [x] Native push / browser notifications
491
- - [x] End-to-end UI test harness (browser-driven admin)
492
-
493
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
494
-
495
- ## ☕ Support
496
-
497
- If this plugin saves you time, consider buying me a coffee!
498
-
499
- <a href="https://buymeacoffee.com/pown3d">
500
- <img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" width="217" />
501
- </a>
502
-
503
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
776
+ ## Support
504
777
 
505
- ## 📄 License
778
+ If this plugin saves you time, consider [buying me a coffee](https://buymeacoffee.com/pown3d).
506
779
 
507
- Licensed under the **MIT License**. See the [LICENSE](LICENSE) file for details.
780
+ ## License
508
781
 
509
- <img src="https://raw.githubusercontent.com/andreasbm/readme/master/assets/lines/rainbow.png" width="100%" alt="" />
782
+ MIT see [LICENSE](LICENSE).
510
783
 
511
- <div align="center" style="padding: 40px 0; color: #666; border-top: 1px solid #e0e0e0; margin-top: 50px;">
512
- <p style="margin: 10px 0;">Built and maintained by <a href="https://consilioweb.fr" style="color: #1f8a5b; text-decoration: none;">ConsilioWEB</a></p>
513
- <p style="margin: 10px 0; font-size: 13px;">
514
- <a href="https://github.com/pOwn3d/payload-support#readme" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">Documentation</a>
515
- <a href="https://github.com/pOwn3d/payload-support/issues" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">Issues</a>
516
- <a href="https://www.npmjs.com/package/@consilioweb/payload-support" style="color: #1f8a5b; text-decoration: none; margin: 0 15px;">npm</a>
517
- </p>
518
- </div>
784
+ Built and maintained by [ConsilioWEB](https://consilioweb.fr) ·
785
+ [Issues](https://github.com/pOwn3d/payload-support/issues) ·
786
+ [npm](https://www.npmjs.com/package/@consilioweb/payload-support)