@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/dist/index.d.cts CHANGED
@@ -11,9 +11,29 @@ interface RateLimitStore {
11
11
  /**
12
12
  * Process-local fallback store. Applications running more than one process
13
13
  * should provide a persistent RateLimitStore through the plugin options.
14
+ *
15
+ * BOUNDED, because the keys come from anonymous HTTP traffic: `/support/login`
16
+ * and `/support/chatbot` key their limiter on the client IP, and the IP is read
17
+ * from `x-forwarded-for`. A caller rotating that header wrote one PERMANENT
18
+ * entry per value into a map that had no ceiling, no expiry sweep and no
19
+ * eviction — only `reset()` ever removed anything, and the login path never
20
+ * calls it. Worse, every fresh key opened a fresh window, so the limiter did
21
+ * not even slow down the flood that was exhausting it.
22
+ *
23
+ * Two independent bounds, the same pair `endpoints/typing.ts` already applies
24
+ * to its own module-level map: `clientIpRateKey` caps the SIZE of a key, the
25
+ * ceiling below caps their NUMBER.
14
26
  */
15
27
  declare class MemoryRateLimitStore implements RateLimitStore {
28
+ private readonly maxKeys;
16
29
  private readonly entries;
30
+ constructor(maxKeys?: number);
31
+ /** Distinct keys currently held. Exposed so the ceiling can be asserted. */
32
+ get size(): number;
33
+ /** Reclaims every window that has already closed. */
34
+ private sweepExpired;
35
+ /** Soonest-closing window first, so the ceiling drops the least useful entry. */
36
+ private evictOldest;
17
37
  increment(key: string, windowMs: number): Promise<RateLimitEntry>;
18
38
  reset(key: string): Promise<void>;
19
39
  }
@@ -28,7 +48,19 @@ declare class RateLimiter {
28
48
  private readonly windowMs;
29
49
  private readonly maxRequests;
30
50
  private readonly store;
31
- constructor(windowMs: number, maxRequests: number, store?: RateLimitStore);
51
+ private readonly prefix;
52
+ /**
53
+ * @param namespace Endpoint-scoped prefix for every key this limiter writes.
54
+ * The store is SHARED (`rateLimitStore: 'payload'` builds one instance for
55
+ * all endpoints), and the raw keys collide across endpoints: `ip` was used
56
+ * by both the login and the chatbot limiter — 10 forged chatbot requests
57
+ * locked a victim out of the portal for 15 minutes — and `String(user.id)`
58
+ * by five different endpoints. Always pass one; it is optional only because
59
+ * `RateLimiter` is part of the published API surface.
60
+ */
61
+ constructor(windowMs: number, maxRequests: number, store?: RateLimitStore, namespace?: string);
62
+ /** The key actually written to the store. Exposed for assertions in tests. */
63
+ scopedKey(key: string): string;
32
64
  check(key: string, context?: unknown): Promise<boolean>;
33
65
  reset(key: string, context?: unknown): Promise<void>;
34
66
  }
@@ -505,14 +537,17 @@ interface SupportSettingsState {
505
537
  */
506
538
  featuresConfigured: boolean;
507
539
  }
508
- declare function readSupportSettingsState(payload: Payload): Promise<SupportSettingsState>;
509
- declare function readSupportSettings(payload: Payload): Promise<SupportSettings>;
510
- declare function readUserPrefs(payload: Payload, userId: string | number): Promise<UserPrefs>;
540
+ declare function readSupportSettingsState(payload: Payload, staffSlug?: string): Promise<SupportSettingsState>;
541
+ declare function readSupportSettings(payload: Payload, staffSlug?: string): Promise<SupportSettings>;
542
+ declare function readUserPrefs(payload: Payload, userId: string | number, staffSlug?: string): Promise<UserPrefs>;
511
543
 
512
544
  /**
513
545
  * POST /api/support/oauth/google
514
546
  * Google OAuth — handles both login redirect and callback.
515
- * Body: { action: 'login' } or { code: string, state: string, cookieState: string }
547
+ * Body: { action: 'login' } or { code: string, state: string }
548
+ *
549
+ * The CSRF state is NOT taken from the body: `action: 'login'` issues it as an
550
+ * HttpOnly cookie and the callback reads it back from the `Cookie` header.
516
551
  */
517
552
  interface OAuthGoogleOptions {
518
553
  allowedEmailDomains?: string[];
package/dist/index.d.ts CHANGED
@@ -11,9 +11,29 @@ interface RateLimitStore {
11
11
  /**
12
12
  * Process-local fallback store. Applications running more than one process
13
13
  * should provide a persistent RateLimitStore through the plugin options.
14
+ *
15
+ * BOUNDED, because the keys come from anonymous HTTP traffic: `/support/login`
16
+ * and `/support/chatbot` key their limiter on the client IP, and the IP is read
17
+ * from `x-forwarded-for`. A caller rotating that header wrote one PERMANENT
18
+ * entry per value into a map that had no ceiling, no expiry sweep and no
19
+ * eviction — only `reset()` ever removed anything, and the login path never
20
+ * calls it. Worse, every fresh key opened a fresh window, so the limiter did
21
+ * not even slow down the flood that was exhausting it.
22
+ *
23
+ * Two independent bounds, the same pair `endpoints/typing.ts` already applies
24
+ * to its own module-level map: `clientIpRateKey` caps the SIZE of a key, the
25
+ * ceiling below caps their NUMBER.
14
26
  */
15
27
  declare class MemoryRateLimitStore implements RateLimitStore {
28
+ private readonly maxKeys;
16
29
  private readonly entries;
30
+ constructor(maxKeys?: number);
31
+ /** Distinct keys currently held. Exposed so the ceiling can be asserted. */
32
+ get size(): number;
33
+ /** Reclaims every window that has already closed. */
34
+ private sweepExpired;
35
+ /** Soonest-closing window first, so the ceiling drops the least useful entry. */
36
+ private evictOldest;
17
37
  increment(key: string, windowMs: number): Promise<RateLimitEntry>;
18
38
  reset(key: string): Promise<void>;
19
39
  }
@@ -28,7 +48,19 @@ declare class RateLimiter {
28
48
  private readonly windowMs;
29
49
  private readonly maxRequests;
30
50
  private readonly store;
31
- constructor(windowMs: number, maxRequests: number, store?: RateLimitStore);
51
+ private readonly prefix;
52
+ /**
53
+ * @param namespace Endpoint-scoped prefix for every key this limiter writes.
54
+ * The store is SHARED (`rateLimitStore: 'payload'` builds one instance for
55
+ * all endpoints), and the raw keys collide across endpoints: `ip` was used
56
+ * by both the login and the chatbot limiter — 10 forged chatbot requests
57
+ * locked a victim out of the portal for 15 minutes — and `String(user.id)`
58
+ * by five different endpoints. Always pass one; it is optional only because
59
+ * `RateLimiter` is part of the published API surface.
60
+ */
61
+ constructor(windowMs: number, maxRequests: number, store?: RateLimitStore, namespace?: string);
62
+ /** The key actually written to the store. Exposed for assertions in tests. */
63
+ scopedKey(key: string): string;
32
64
  check(key: string, context?: unknown): Promise<boolean>;
33
65
  reset(key: string, context?: unknown): Promise<void>;
34
66
  }
@@ -505,14 +537,17 @@ interface SupportSettingsState {
505
537
  */
506
538
  featuresConfigured: boolean;
507
539
  }
508
- declare function readSupportSettingsState(payload: Payload): Promise<SupportSettingsState>;
509
- declare function readSupportSettings(payload: Payload): Promise<SupportSettings>;
510
- declare function readUserPrefs(payload: Payload, userId: string | number): Promise<UserPrefs>;
540
+ declare function readSupportSettingsState(payload: Payload, staffSlug?: string): Promise<SupportSettingsState>;
541
+ declare function readSupportSettings(payload: Payload, staffSlug?: string): Promise<SupportSettings>;
542
+ declare function readUserPrefs(payload: Payload, userId: string | number, staffSlug?: string): Promise<UserPrefs>;
511
543
 
512
544
  /**
513
545
  * POST /api/support/oauth/google
514
546
  * Google OAuth — handles both login redirect and callback.
515
- * Body: { action: 'login' } or { code: string, state: string, cookieState: string }
547
+ * Body: { action: 'login' } or { code: string, state: string }
548
+ *
549
+ * The CSRF state is NOT taken from the body: `action: 'login'` issues it as an
550
+ * HttpOnly cookie and the callback reads it back from the `Cookie` header.
516
551
  */
517
552
  interface OAuthGoogleOptions {
518
553
  allowedEmailDomains?: string[];