@consilioweb/payload-support 3.0.0 → 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 (43) hide show
  1. package/README.md +36 -2
  2. package/dist/index.cjs +558 -94
  3. package/dist/index.d.cts +20 -5
  4. package/dist/index.d.ts +20 -5
  5. package/dist/index.js +558 -94
  6. package/package.json +1 -1
  7. package/src/collections/ChatMessages.ts +59 -2
  8. package/src/collections/ClientSummaries.ts +10 -4
  9. package/src/collections/TicketMessages.ts +4 -1
  10. package/src/collections/WebhookEndpoints.ts +44 -2
  11. package/src/endpoints/admin-chat.ts +3 -3
  12. package/src/endpoints/ai-agent.ts +3 -3
  13. package/src/endpoints/ai.ts +3 -3
  14. package/src/endpoints/auth-2fa.ts +16 -4
  15. package/src/endpoints/capabilities.ts +6 -6
  16. package/src/endpoints/chat.ts +7 -5
  17. package/src/endpoints/chatbot.ts +48 -2
  18. package/src/endpoints/client-intelligence.ts +4 -4
  19. package/src/endpoints/email-stats.ts +19 -3
  20. package/src/endpoints/import-conversation.ts +1 -1
  21. package/src/endpoints/index.ts +1 -1
  22. package/src/endpoints/invite-collaborator.ts +29 -3
  23. package/src/endpoints/login.ts +15 -2
  24. package/src/endpoints/oauth-google.ts +130 -8
  25. package/src/endpoints/resend-notification.ts +3 -3
  26. package/src/endpoints/send-reminder.ts +3 -3
  27. package/src/endpoints/signature.ts +9 -2
  28. package/src/endpoints/ticket-synthesis.ts +3 -3
  29. package/src/endpoints/transfer-ticket.ts +28 -3
  30. package/src/endpoints/typing.ts +117 -14
  31. package/src/endpoints/user-prefs.ts +5 -2
  32. package/src/plugin.ts +12 -0
  33. package/src/portal/auth/layout.tsx +19 -1
  34. package/src/portal/auth/tickets/detail/MessageBody.tsx +88 -0
  35. package/src/portal/auth/tickets/detail/page.tsx +2 -6
  36. package/src/portal/login/page.tsx +11 -3
  37. package/src/utils/fireWebhooks.ts +4 -1
  38. package/src/utils/rateLimiter.ts +39 -5
  39. package/src/utils/readSettings.ts +124 -14
  40. package/src/utils/ticketAccess.ts +16 -1
  41. package/src/utils/twoFactorChallenge.ts +80 -0
  42. package/src/utils/urlSafety.ts +230 -0
  43. package/src/utils/webhookDispatcher.ts +5 -1
package/README.md CHANGED
@@ -328,6 +328,8 @@ Two flags are projections, deliberately not stored twice: `features.autoClose` m
328
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
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
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. |
331
333
 
332
334
  ### Cron jobs
333
335
 
@@ -428,12 +430,22 @@ to stay enabled for the endpoint to be registered.
428
430
  | Method | Path | Access | Purpose |
429
431
  |---|---|---|---|
430
432
  | `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. |
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. |
432
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. |
433
436
  | `GET` | `/support/export-data` | client | GDPR data export. |
434
437
  | `POST` | `/support/delete-account` | client | GDPR right to erasure. |
435
438
  | `POST` | `/support/merge-clients` | staff | Merge two client records. |
436
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
+
437
449
  ### AI
438
450
 
439
451
  | Method | Path | Access | Flag | Purpose |
@@ -512,6 +524,15 @@ The signature header is only sent when the endpoint has a `secret`, so always se
512
524
  `X-Webhook-Secret` header any more — verify the HMAC signature instead. `lastTriggeredAt` and
513
525
  `lastStatus` are written back on the endpoint after every attempt. Requires `features.webhooks`.
514
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
+
515
536
  | Event | `data` |
516
537
  |---|---|
517
538
  | `ticket_created` | `ticketId`, `id`, `ticketNumber`, `subject`, `status`, `priority`, `category` |
@@ -617,7 +638,15 @@ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outr
617
638
  outside development. Login, OAuth and 2FA responses never return the token in JSON.
618
639
  - **2FA enforced server-side** (`beforeLogin`), and the endpoint refuses to run without
619
640
  `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.
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.
621
650
  - **HMAC everywhere it matters** — webhook deliveries and tracking pixels are signed and verified in
622
651
  constant time, with idempotent writes. Cron and webhook secrets are read from headers only; a
623
652
  query-string secret is rejected.
@@ -626,6 +655,11 @@ Installing on Node 18, React 18, Next 14 or Payload < 3.37 warns, and fails outr
626
655
  - **No silent third-party AI host** — the `ollama` provider fails loudly on a missing
627
656
  `OLLAMA_API_URL` instead of defaulting to someone else's server.
628
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.
629
663
 
630
664
  ### Reporting security issues
631
665