@consilioweb/payload-support 3.0.0 → 5.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 (77) hide show
  1. package/README.md +58 -14
  2. package/dist/index.cjs +673 -107
  3. package/dist/index.d.cts +40 -5
  4. package/dist/index.d.ts +40 -5
  5. package/dist/index.js +673 -107
  6. package/dist/utils/db.d.ts +34 -0
  7. package/dist/utils/readSettings.d.ts +90 -0
  8. package/dist/views/BillingView/index.js +4 -4
  9. package/dist/views/ChatView/index.js +4 -4
  10. package/dist/views/CrmView/index.js +4 -4
  11. package/dist/views/EmailTrackingView/index.js +4 -4
  12. package/dist/views/ImportConversationView/index.js +4 -4
  13. package/dist/views/LogsView/index.js +4 -2
  14. package/dist/views/NewTicketView/index.js +4 -2
  15. package/dist/views/PendingEmailsView/index.js +4 -4
  16. package/dist/views/SupportDashboardView/index.js +4 -4
  17. package/dist/views/TicketDetailView/index.js +4 -4
  18. package/dist/views/TicketInboxView/index.js +4 -2
  19. package/dist/views/TicketingSettingsView/index.js +4 -4
  20. package/dist/views/TimeDashboardView/index.js +4 -4
  21. package/dist/views/shared/viewAccess.d.ts +29 -0
  22. package/dist/views/shared/viewAccess.js +24 -0
  23. package/package.json +26 -20
  24. package/src/collections/ChatMessages.ts +59 -2
  25. package/src/collections/ClientSummaries.ts +10 -4
  26. package/src/collections/TicketMessages.ts +4 -1
  27. package/src/collections/WebhookEndpoints.ts +44 -2
  28. package/src/endpoints/admin-chat.ts +3 -3
  29. package/src/endpoints/ai-agent.ts +3 -3
  30. package/src/endpoints/ai.ts +3 -3
  31. package/src/endpoints/auth-2fa.ts +68 -11
  32. package/src/endpoints/capabilities.ts +7 -9
  33. package/src/endpoints/chat.ts +7 -5
  34. package/src/endpoints/chatbot.ts +50 -4
  35. package/src/endpoints/client-intelligence.ts +4 -4
  36. package/src/endpoints/email-stats.ts +19 -3
  37. package/src/endpoints/import-conversation.ts +3 -3
  38. package/src/endpoints/index.ts +1 -1
  39. package/src/endpoints/invite-collaborator.ts +29 -3
  40. package/src/endpoints/login.ts +28 -5
  41. package/src/endpoints/oauth-google.ts +130 -8
  42. package/src/endpoints/push.ts +14 -1
  43. package/src/endpoints/resend-notification.ts +3 -3
  44. package/src/endpoints/send-reminder.ts +3 -3
  45. package/src/endpoints/signature.ts +9 -2
  46. package/src/endpoints/statuses.ts +17 -0
  47. package/src/endpoints/ticket-synthesis.ts +3 -3
  48. package/src/endpoints/transfer-ticket.ts +28 -3
  49. package/src/endpoints/typing.ts +117 -14
  50. package/src/endpoints/user-prefs.ts +5 -2
  51. package/src/plugin.ts +12 -0
  52. package/src/portal/auth/layout.tsx +19 -1
  53. package/src/portal/auth/tickets/detail/MessageBody.tsx +88 -0
  54. package/src/portal/auth/tickets/detail/page.tsx +2 -6
  55. package/src/portal/login/page.tsx +23 -5
  56. package/src/utils/fireWebhooks.ts +4 -1
  57. package/src/utils/push.ts +22 -0
  58. package/src/utils/rateLimiter.ts +136 -4
  59. package/src/utils/readSettings.ts +124 -14
  60. package/src/utils/ticketAccess.ts +16 -1
  61. package/src/utils/twoFactorChallenge.ts +85 -0
  62. package/src/utils/urlSafety.ts +265 -0
  63. package/src/utils/webhookDispatcher.ts +5 -1
  64. package/src/views/BillingView/index.tsx +4 -4
  65. package/src/views/ChatView/index.tsx +4 -4
  66. package/src/views/CrmView/index.tsx +4 -4
  67. package/src/views/EmailTrackingView/index.tsx +4 -4
  68. package/src/views/ImportConversationView/index.tsx +4 -4
  69. package/src/views/LogsView/index.tsx +4 -2
  70. package/src/views/NewTicketView/index.tsx +4 -2
  71. package/src/views/PendingEmailsView/index.tsx +4 -4
  72. package/src/views/SupportDashboardView/index.tsx +4 -4
  73. package/src/views/TicketDetailView/index.tsx +4 -4
  74. package/src/views/TicketInboxView/index.tsx +4 -2
  75. package/src/views/TicketingSettingsView/index.tsx +4 -4
  76. package/src/views/TimeDashboardView/index.tsx +4 -4
  77. package/src/views/shared/viewAccess.ts +73 -0
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/@consilioweb/payload-support.svg)](https://www.npmjs.com/package/@consilioweb/payload-support)
6
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)
7
+ [![payload](https://img.shields.io/badge/payload-%5E3.79.1-blue.svg)](https://payloadcms.com)
8
8
 
9
9
  ## About
10
10
 
@@ -81,12 +81,18 @@ pnpm add @consilioweb/payload-support
81
81
  **Peer dependencies** — all six are required, none is optional:
82
82
 
83
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"
84
+ pnpm add payload@^3.79.1 @payloadcms/next@^3.79.1 next@^15.2.9 react@^19 react-dom@^19 lucide-react@">=0.300.0"
85
85
  ```
86
86
 
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.
87
+ - `payload@^3.79.1` — the floor is a security floor, and it is stated as such rather than
88
+ dressed up as an API constraint. Below 3.79.1 Payload is vulnerable to a pre-authentication
89
+ account takeover (GHSA-hp5w-3hxx-vmwf) and to a SQL injection, so a range that admitted those
90
+ versions — the previous `^3.37.0` did — let an install satisfy this plugin's requirements while
91
+ remaining exploitable. The API surface allows lower: the Google OAuth endpoint imports `jwtSign`
92
+ and `getFieldsToSign` from the `payload` barrel, and a missing named export fails the whole
93
+ `payload.config.ts` rather than just that endpoint, but both have been exported since 3.37 and
94
+ every symbol this package imports was verified present in 3.79.1. CI builds and tests against
95
+ the 3.88 line; 3.79.1 to 3.87 are permitted on that verification, not on a test run.
90
96
  - `@payloadcms/next` and `next` — the 13 admin views are registered unless you pass
91
97
  `skipViews: true`, and each imports `DefaultTemplate` from `@payloadcms/next/templates` plus
92
98
  `next/navigation` / `next/link` statically. Both are already present in any Payload 3 admin app.
@@ -328,6 +334,8 @@ Two flags are projections, deliberately not stored twice: `features.autoClose` m
328
334
  | `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
335
  | `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
336
  | `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. |
337
+ | `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. |
338
+ | `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. |
331
339
 
332
340
  ### Cron jobs
333
341
 
@@ -405,7 +413,7 @@ to stay enabled for the endpoint to be registered.
405
413
  |---|---|---|---|---|
406
414
  | `GET` | `/support/search` | staff | — | Global search over tickets, messages, clients and the knowledge base. |
407
415
  | `GET` | `/support/kb/search` | public | — | Knowledge-base search (used by the portal and the chatbot). |
408
- | `GET` | `/support/statuses` | authenticated | `customStatuses` | Ticket statuses, sorted. |
416
+ | `GET` | `/support/statuses` | staff or support-client | `customStatuses` | Ticket statuses, sorted. Mirrors `ticket-statuses.access.read`: a principal from any other auth collection of your app gets 403, not the taxonomy. |
409
417
  | `POST` | `/support/bulk-action` | staff | `bulkActions` | Apply one action to many tickets. |
410
418
  | `POST` | `/support/merge-tickets` | staff | `merge` | Merge a source ticket into a target. |
411
419
  | `POST` | `/support/split-ticket` | staff | `splitTicket` | Extract a message into a new ticket. |
@@ -428,12 +436,22 @@ to stay enabled for the endpoint to be registered.
428
436
  | Method | Path | Access | Purpose |
429
437
  |---|---|---|---|
430
438
  | `POST` | `/support/login` | public, rate-limited | Portal password login. Sets an `HttpOnly` cookie; the JWT is never returned in JSON. |
431
- | `POST` | `/support/2fa` | public, rate-limited | Email second factor. |
439
+ | `POST` | `/support/2fa` | public, rate-limited | Email second factor. **Both** `action: 'send'` and `action: 'verify'` require the short-lived `challenge` returned by `/support/login` (or by the Google callback) alongside `requires2FA`; without it, an anonymous caller who merely knew an address could burn the victim's send quota *or* their 5 verification attempts and lock them out of the only route that clears the 2FA gate. Every accepted `send` returns a refreshed `challenge` — keep the latest one and pass it to `verify` along with the code. |
432
440
  | `POST` | `/support/oauth/google` | public | Google sign-in, optionally restricted by `allowedEmailDomains`. |
441
+ | `GET` | `/support/email-stats` | **staff**, rate-limited | Email pipeline aggregate. Was reachable by any authenticated session before 3.0.1. |
433
442
  | `GET` | `/support/export-data` | client | GDPR data export. |
434
443
  | `POST` | `/support/delete-account` | client | GDPR right to erasure. |
435
444
  | `POST` | `/support/merge-clients` | staff | Merge two client records. |
436
445
 
446
+ **Google OAuth, CSRF state.** `{ "action": "login" }` now answers with a `Set-Cookie:
447
+ support-oauth-state=…; HttpOnly; SameSite=Lax` alongside the `url` and `state` it already returned.
448
+ The callback reads that cookie **server-side** and compares it, in constant time, with the `state`
449
+ Google sends back — it no longer accepts a `cookieState` field in the request body, which any
450
+ non-browser caller could simply send twice. If you wrote your own Google button, drop `cookieState`
451
+ from the callback payload and let the browser carry the cookie (`credentials: 'include'` on a
452
+ cross-origin fetch). The callback also enforces 2FA: an account with `twoFactorEnabled` gets
453
+ `{ requires2FA: true }` and no token, exactly like `/support/login`.
454
+
437
455
  ### AI
438
456
 
439
457
  | Method | Path | Access | Flag | Purpose |
@@ -483,7 +501,7 @@ await fetch('/api/support/ai-agent', {
483
501
  | `GET`/`POST` | `/support/settings` | staff | — | Read and write the support settings, including the runtime feature flags. |
484
502
  | `GET`/`POST` | `/support/round-robin-config` | staff | `roundRobin` | Read and toggle round-robin assignment. |
485
503
  | `GET` | `/support/push/vapid-public-key` | public | — | VAPID public key for the browser subscription. |
486
- | `POST` | `/support/push/subscribe` | staff | — | Register an agent's push subscription. |
504
+ | `POST` | `/support/push/subscribe` | staff | — | Register an agent's push subscription. The `endpoint` must be an `https://` URL on a public host — private, loopback and link-local targets are refused with 400 (SSRF), at write time and again at send time. |
487
505
  | `GET` | `/support/auto-close` | `x-cron-secret` | `autoClose` | Remind, then close, inactive tickets. |
488
506
  | `POST` | `/support/process-snooze` | `x-cron-secret` | `snooze` | Wake snoozed tickets. |
489
507
  | `POST` | `/support/process-scheduled` | `x-cron-secret` | `scheduledReplies` | Release scheduled replies. |
@@ -512,6 +530,15 @@ The signature header is only sent when the endpoint has a `secret`, so always se
512
530
  `X-Webhook-Secret` header any more — verify the HMAC signature instead. `lastTriggeredAt` and
513
531
  `lastStatus` are written back on the endpoint after every attempt. Requires `features.webhooks`.
514
532
 
533
+ **The endpoint URL is validated as an SSRF target, not as free text.** Only `https://` is accepted
534
+ (set `SUPPORT_ALLOW_INSECURE_WEBHOOKS=1` for local `http://`), literal loopback / private /
535
+ link-local / IPv4-mapped-IPv6 hosts are rejected at save time, the hostname is re-resolved and
536
+ re-checked immediately before the request (DNS rebinding), and redirects are followed **manually**
537
+ so a `302` towards `127.0.0.1` or `169.254.169.254` cannot slip past the checks. An endpoint saved
538
+ before 3.0 with an `http://` URL or an internal host stops delivering and must be re-pointed — but
539
+ it stays editable: the check runs only on a URL you actually write, so such a row can still be
540
+ renamed or deactivated, and `lastStatus: 0` is still recorded on it so a dead endpoint is visible.
541
+
515
542
  | Event | `data` |
516
543
  |---|---|
517
544
  | `ticket_created` | `ticketId`, `id`, `ticketNumber`, `subject`, `status`, `priority`, `category` |
@@ -593,16 +620,18 @@ import type { SupportPluginConfig, SupportFeatures } from '@consilioweb/payload-
593
620
  | Requirement | Range | Source |
594
621
  |---|---|---|
595
622
  | Node.js | `>=20.9.0` | `engines.node` |
596
- | Payload | `^3.37.0` | `peerDependencies.payload` |
597
- | `@payloadcms/next` | `^3.37.0` | `peerDependencies` |
623
+ | Payload | `^3.79.1` | `peerDependencies.payload` |
624
+ | `@payloadcms/next` | `^3.79.1` | `peerDependencies` |
598
625
  | Next.js | `^15.2.9 \|\| ^16.0.0` | `peerDependencies` |
599
626
  | React / React DOM | `^19.0.0` | `peerDependencies` |
600
627
  | `lucide-react` | `>=0.300.0` | `peerDependencies` |
601
628
  | `@anthropic-ai/sdk` | any recent version | Runtime-only, install it yourself if you use the AI features |
602
629
  | `@payloadcms/richtext-lexical` | any 3.x | Only if you mount the portal FAQ page |
603
630
 
604
- Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outright under
605
- `engine-strict` or a strict peer resolver.
631
+ Installing on Node 18, React 18, Next 14 or Payload < 3.79.1 warns, and fails outright under
632
+ `engine-strict` or a strict peer resolver. Payload below 3.79.1 is additionally vulnerable to a
633
+ pre-authentication account takeover (GHSA-hp5w-3hxx-vmwf) and to a SQL injection: upgrade rather
634
+ than force the install.
606
635
 
607
636
  ## Security
608
637
 
@@ -617,7 +646,15 @@ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outr
617
646
  outside development. Login, OAuth and 2FA responses never return the token in JSON.
618
647
  - **2FA enforced server-side** (`beforeLogin`), and the endpoint refuses to run without
619
648
  `PAYLOAD_SECRET` rather than falling back to an insecure default. OAuth verifies the Google email.
620
- - **Stored-XSS protection** — message HTML is sanitized on write, not on render.
649
+ - **Stored-XSS protection** — message HTML is sanitized on write, not on render, and the portal
650
+ renders plain-text bodies as JSX (no hand-rolled HTML escaping) so an inbound email cannot inject
651
+ an attribute into a link.
652
+ - **Owner-scoped preferences** — the plugin's `payload-preferences` rows (settings, per-agent
653
+ signature and locale) are read back with a `user.relationTo` constraint on the staff collection.
654
+ `payload-preferences` accepts a write from *any* authenticated principal, so reading by key alone
655
+ let a client own the server settings.
656
+ - **Auth-collection checks, not duck typing** — every guard compares `user.collection` to the
657
+ configured slug: a user of another auth collection of the host app is not a support client.
621
658
  - **HMAC everywhere it matters** — webhook deliveries and tracking pixels are signed and verified in
622
659
  constant time, with idempotent writes. Cron and webhook secrets are read from headers only; a
623
660
  query-string secret is rejected.
@@ -626,6 +663,13 @@ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outr
626
663
  - **No silent third-party AI host** — the `ollama` provider fails loudly on a missing
627
664
  `OLLAMA_API_URL` instead of defaulting to someone else's server.
628
665
  - **Bounded inbound email** payloads and attachments.
666
+ - **Bounded outbound mail** — ticket transfers and collaborator invitations are capped per user over
667
+ a long window, not only per ticket (a client can create tickets at will).
668
+ - **SSRF-guarded outbound requests** — scheme, literal host, resolved address and every redirect hop
669
+ for webhooks; scheme, literal host and resolved address for Web Push subscription endpoints, which
670
+ `web-push` would otherwise hand host and port straight to `https.request`.
671
+ - **2FA also enforced on the Google OAuth path**, which mints its session outside `payload.login`
672
+ and therefore outside the `beforeLogin` hook.
629
673
 
630
674
  ### Reporting security issues
631
675
 
@@ -707,7 +751,7 @@ auto-migrated: generate and push it before deploying. See [Troubleshooting](#tro
707
751
  recomputing it — so a stale total keeps over-billing until something touches a time entry on that
708
752
  ticket.
709
753
  5. **Import `./views` and `./components/TicketConversation` with ESM** — their CJS builds are gone.
710
- 6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.37+, and install
754
+ 6. Check your platform: Node 20.9+, React 19, Next 15.2.9+, Payload 3.79.1+, and install
711
755
  `lucide-react` explicitly if you were relying on it being optional.
712
756
 
713
757
  ### 1.x → 2.0