@consilioweb/payload-support 4.0.0 → 6.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.
- package/README.md +272 -35
- package/dist/components/RichTextEditor/index.js +1 -1
- package/dist/components/TicketConversation/RewriteDropdown.js +1 -1
- package/dist/components/TicketConversation/components/AISummaryPanel.js +3 -0
- package/dist/components/TicketConversation/components/ActionPanels.js +13 -8
- package/dist/components/TicketConversation/components/ClientHistory.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlock.js +1 -0
- package/dist/components/TicketConversation/components/CodeBlockInserter.js +1 -1
- package/dist/components/TicketConversation/components/QuickActions.js +10 -8
- package/dist/components/TicketConversation/components/TicketHeader.js +2 -0
- package/dist/components/TicketConversation/components/TimeTrackingPanel.js +15 -9
- package/dist/components/TicketConversation/index.d.ts +10 -0
- package/dist/components/TicketConversation/index.js +25 -7
- package/dist/index.cjs +421 -73
- package/dist/index.d.cts +127 -38
- package/dist/index.d.ts +127 -38
- package/dist/index.js +415 -72
- package/dist/styles/BillingView.module.scss +11 -11
- package/dist/styles/ChatView.module.scss +7 -7
- package/dist/styles/CommandPalette.module.scss +1 -1
- package/dist/styles/CrmView.module.scss +9 -9
- package/dist/styles/EmailTracking.module.scss +6 -6
- package/dist/styles/ImportConversation.module.scss +5 -5
- package/dist/styles/Logs.module.scss +5 -5
- package/dist/styles/NewTicket.module.scss +2 -2
- package/dist/styles/PendingEmails.module.scss +13 -13
- package/dist/styles/SupportDashboard.module.scss +24 -8
- package/dist/styles/TicketDetail.module.scss +20 -20
- package/dist/styles/TicketInbox.module.scss +6 -6
- package/dist/styles/TicketingSettings.module.scss +10 -10
- package/dist/styles/TimeDashboard.module.scss +6 -6
- package/dist/styles/_tokens.scss +3 -1
- package/dist/utils/db.d.ts +34 -0
- package/dist/utils/readSettings.d.ts +90 -0
- package/dist/views/BillingView/client.js +15 -8
- package/dist/views/BillingView/index.js +4 -4
- package/dist/views/ChatView/client.js +2 -0
- package/dist/views/ChatView/index.js +4 -4
- package/dist/views/CrmView/client.js +2 -0
- package/dist/views/CrmView/index.js +4 -4
- package/dist/views/EmailTrackingView/client.js +23 -11
- package/dist/views/EmailTrackingView/index.js +4 -4
- package/dist/views/ImportConversationView/client.js +1 -0
- package/dist/views/ImportConversationView/index.js +4 -4
- package/dist/views/LogsView/client.js +1 -1
- package/dist/views/LogsView/index.js +4 -2
- package/dist/views/NewTicketView/client.js +20 -13
- package/dist/views/NewTicketView/index.js +4 -2
- package/dist/views/PendingEmailsView/client.js +9 -3
- package/dist/views/PendingEmailsView/index.js +4 -4
- package/dist/views/SupportDashboardView/client.js +17 -19
- package/dist/views/SupportDashboardView/index.js +4 -4
- package/dist/views/TicketDetailView/client.js +23 -11
- package/dist/views/TicketDetailView/index.js +4 -4
- package/dist/views/TicketInboxView/client.js +52 -26
- package/dist/views/TicketInboxView/index.js +4 -2
- package/dist/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/dist/views/TicketingSettingsView/client.js +44 -26
- package/dist/views/TicketingSettingsView/index.js +4 -4
- package/dist/views/TimeDashboardView/client.js +10 -7
- package/dist/views/TimeDashboardView/index.js +4 -4
- package/dist/views/shared/ErrorBoundary.d.ts +36 -1
- package/dist/views/shared/ErrorBoundary.js +59 -28
- package/dist/views/shared/adminTokens.d.ts +15 -5
- package/dist/views/shared/adminTokens.js +23 -7
- package/dist/views/shared/icons.d.ts +49 -0
- package/dist/views/shared/icons.js +92 -0
- package/dist/views/shared/locales/en.json +15 -1
- package/dist/views/shared/locales/fr.json +15 -1
- package/dist/views/shared/viewAccess.d.ts +29 -0
- package/dist/views/shared/viewAccess.js +24 -0
- package/package.json +31 -21
- package/scripts/uninstall-data.mjs +178 -0
- package/scripts/uninstall.mjs +184 -0
- package/src/collections/Tickets.ts +7 -0
- package/src/collections/TimeEntries.ts +11 -3
- package/src/components/RichTextEditor/index.tsx +1 -1
- package/src/components/TicketConversation/RewriteDropdown.tsx +1 -1
- package/src/components/TicketConversation/components/AISummaryPanel.tsx +3 -0
- package/src/components/TicketConversation/components/ActionPanels.tsx +13 -8
- package/src/components/TicketConversation/components/ClientHistory.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlock.tsx +1 -0
- package/src/components/TicketConversation/components/CodeBlockInserter.tsx +1 -1
- package/src/components/TicketConversation/components/QuickActions.tsx +10 -8
- package/src/components/TicketConversation/components/TicketHeader.tsx +2 -0
- package/src/components/TicketConversation/components/TimeTrackingPanel.tsx +17 -9
- package/src/components/TicketConversation/index.tsx +41 -7
- package/src/endpoints/auth-2fa.ts +53 -8
- package/src/endpoints/capabilities.ts +2 -4
- package/src/endpoints/chatbot.ts +2 -2
- package/src/endpoints/delete-account.ts +191 -47
- package/src/endpoints/export-data.ts +104 -4
- package/src/endpoints/import-conversation.ts +2 -2
- package/src/endpoints/login.ts +13 -3
- package/src/endpoints/purge-logs.ts +6 -15
- package/src/endpoints/push.ts +14 -1
- package/src/endpoints/statuses.ts +17 -0
- package/src/index.ts +2 -0
- package/src/plugin.ts +51 -1
- package/src/portal/LiveChat.tsx +1 -1
- package/src/portal/auth/profile/page.tsx +3 -2
- package/src/portal/icons.tsx +91 -0
- package/src/portal/login/page.tsx +14 -4
- package/src/styles/BillingView.module.scss +11 -11
- package/src/styles/ChatView.module.scss +7 -7
- package/src/styles/CommandPalette.module.scss +1 -1
- package/src/styles/CrmView.module.scss +9 -9
- package/src/styles/EmailTracking.module.scss +6 -6
- package/src/styles/ImportConversation.module.scss +5 -5
- package/src/styles/Logs.module.scss +5 -5
- package/src/styles/NewTicket.module.scss +2 -2
- package/src/styles/PendingEmails.module.scss +13 -13
- package/src/styles/SupportDashboard.module.scss +24 -8
- package/src/styles/TicketDetail.module.scss +20 -20
- package/src/styles/TicketInbox.module.scss +6 -6
- package/src/styles/TicketingSettings.module.scss +10 -10
- package/src/styles/TimeDashboard.module.scss +6 -6
- package/src/styles/_tokens.scss +3 -1
- package/src/types.ts +20 -0
- package/src/utils/push.ts +22 -0
- package/src/utils/rateLimiter.ts +98 -0
- package/src/utils/retention.ts +111 -0
- package/src/utils/slugs.ts +15 -0
- package/src/utils/twoFactorChallenge.ts +6 -1
- package/src/utils/urlSafety.ts +36 -1
- package/src/views/BillingView/client.tsx +17 -8
- package/src/views/BillingView/index.tsx +4 -4
- package/src/views/ChatView/client.tsx +2 -0
- package/src/views/ChatView/index.tsx +4 -4
- package/src/views/CrmView/client.tsx +2 -0
- package/src/views/CrmView/index.tsx +4 -4
- package/src/views/EmailTrackingView/client.tsx +18 -7
- package/src/views/EmailTrackingView/index.tsx +4 -4
- package/src/views/ImportConversationView/client.tsx +1 -0
- package/src/views/ImportConversationView/index.tsx +4 -4
- package/src/views/LogsView/client.tsx +1 -1
- package/src/views/LogsView/index.tsx +4 -2
- package/src/views/NewTicketView/client.tsx +22 -13
- package/src/views/NewTicketView/index.tsx +4 -2
- package/src/views/PendingEmailsView/client.tsx +9 -3
- package/src/views/PendingEmailsView/index.tsx +4 -4
- package/src/views/SupportDashboardView/client.tsx +11 -5
- package/src/views/SupportDashboardView/index.tsx +4 -4
- package/src/views/TicketDetailView/client.tsx +25 -11
- package/src/views/TicketDetailView/index.tsx +4 -4
- package/src/views/TicketInboxView/client.tsx +47 -7
- package/src/views/TicketInboxView/index.tsx +4 -2
- package/src/views/TicketingSettingsView/TicketingSettings.module.scss +7 -7
- package/src/views/TicketingSettingsView/client.tsx +201 -144
- package/src/views/TicketingSettingsView/index.tsx +4 -4
- package/src/views/TimeDashboardView/client.tsx +10 -7
- package/src/views/TimeDashboardView/index.tsx +4 -4
- package/src/views/shared/ErrorBoundary.tsx +95 -31
- package/src/views/shared/adminTokens.ts +28 -11
- package/src/views/shared/icons.tsx +146 -0
- package/src/views/shared/locales/en.json +15 -1
- package/src/views/shared/locales/fr.json +15 -1
- package/src/views/shared/viewAccess.ts +73 -0
- package/src/types/lucide-react.d.ts +0 -42
|
@@ -26,7 +26,7 @@ $radius-md: 12px;
|
|
|
26
26
|
|
|
27
27
|
.subtitle {
|
|
28
28
|
font-size: 13px;
|
|
29
|
-
color: var(--theme-elevation-
|
|
29
|
+
color: var(--theme-elevation-650);
|
|
30
30
|
margin: 0;
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -75,7 +75,7 @@ $radius-md: 12px;
|
|
|
75
75
|
font-weight: 700;
|
|
76
76
|
text-transform: uppercase;
|
|
77
77
|
letter-spacing: 0.6px;
|
|
78
|
-
color: var(--theme-elevation-
|
|
78
|
+
color: var(--theme-elevation-650);
|
|
79
79
|
margin-bottom: 8px;
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -148,7 +148,7 @@ $radius-md: 12px;
|
|
|
148
148
|
font-weight: 700;
|
|
149
149
|
text-transform: uppercase;
|
|
150
150
|
letter-spacing: 0.5px;
|
|
151
|
-
color: var(--theme-elevation-
|
|
151
|
+
color: var(--theme-elevation-650);
|
|
152
152
|
margin: 0 0 16px;
|
|
153
153
|
}
|
|
154
154
|
|
|
@@ -207,7 +207,7 @@ $radius-md: 12px;
|
|
|
207
207
|
font-weight: 600;
|
|
208
208
|
font-variant-numeric: tabular-nums;
|
|
209
209
|
font-size: 12px;
|
|
210
|
-
color: var(--theme-elevation-
|
|
210
|
+
color: var(--theme-elevation-650);
|
|
211
211
|
white-space: nowrap;
|
|
212
212
|
}
|
|
213
213
|
|
|
@@ -219,9 +219,25 @@ $radius-md: 12px;
|
|
|
219
219
|
font-weight: 500;
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
// The whole row looks clickable, but only this link actually is — it is what
|
|
223
|
+
// carries the ticket into the keyboard tab order.
|
|
224
|
+
.ticketLink {
|
|
225
|
+
display: block;
|
|
226
|
+
overflow: hidden;
|
|
227
|
+
text-overflow: ellipsis;
|
|
228
|
+
white-space: nowrap;
|
|
229
|
+
color: inherit;
|
|
230
|
+
text-decoration: none;
|
|
231
|
+
|
|
232
|
+
&:hover,
|
|
233
|
+
&:focus-visible {
|
|
234
|
+
text-decoration: underline;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
222
238
|
.ticketClient {
|
|
223
239
|
font-size: 12px;
|
|
224
|
-
color: var(--theme-elevation-
|
|
240
|
+
color: var(--theme-elevation-650);
|
|
225
241
|
max-width: 120px;
|
|
226
242
|
overflow: hidden;
|
|
227
243
|
text-overflow: ellipsis;
|
|
@@ -430,7 +446,7 @@ $radius-md: 12px;
|
|
|
430
446
|
font-weight: 700;
|
|
431
447
|
text-transform: uppercase;
|
|
432
448
|
letter-spacing: 0.5px;
|
|
433
|
-
color: var(--theme-elevation-
|
|
449
|
+
color: var(--theme-elevation-650);
|
|
434
450
|
margin: 0 0 16px;
|
|
435
451
|
}
|
|
436
452
|
|
|
@@ -582,7 +598,7 @@ $radius-md: 12px;
|
|
|
582
598
|
font-size: 12px;
|
|
583
599
|
font-weight: 600;
|
|
584
600
|
font-variant-numeric: tabular-nums;
|
|
585
|
-
color: var(--theme-elevation-
|
|
601
|
+
color: var(--theme-elevation-650);
|
|
586
602
|
white-space: nowrap;
|
|
587
603
|
flex-shrink: 0;
|
|
588
604
|
}
|
|
@@ -637,7 +653,7 @@ $radius-md: 12px;
|
|
|
637
653
|
.errorState {
|
|
638
654
|
text-align: center;
|
|
639
655
|
padding: 60px 24px;
|
|
640
|
-
color: var(--theme-elevation-
|
|
656
|
+
color: var(--theme-elevation-650);
|
|
641
657
|
font-size: 14px;
|
|
642
658
|
|
|
643
659
|
strong {
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
font-family: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace;
|
|
53
53
|
font-size: 12px;
|
|
54
54
|
font-weight: 400;
|
|
55
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
55
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
56
56
|
display: inline-flex;
|
|
57
57
|
align-items: center;
|
|
58
58
|
gap: 8px;
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
|
|
65
65
|
> .ticketNumberCat,
|
|
66
66
|
> .ticketNumberSource {
|
|
67
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
67
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
68
68
|
display: inline-flex;
|
|
69
69
|
align-items: center;
|
|
70
70
|
gap: 4px;
|
|
@@ -194,7 +194,7 @@
|
|
|
194
194
|
text-transform: uppercase;
|
|
195
195
|
letter-spacing: 0.08em;
|
|
196
196
|
line-height: 1.3;
|
|
197
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
197
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
198
198
|
margin: 0 0 12px;
|
|
199
199
|
display: flex;
|
|
200
200
|
justify-content: space-between;
|
|
@@ -353,7 +353,7 @@
|
|
|
353
353
|
font-size: 11px;
|
|
354
354
|
font-weight: 500;
|
|
355
355
|
line-height: 1.3;
|
|
356
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
356
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
357
357
|
white-space: nowrap;
|
|
358
358
|
text-transform: uppercase;
|
|
359
359
|
letter-spacing: 0.08em;
|
|
@@ -450,7 +450,7 @@
|
|
|
450
450
|
font-size: 11px;
|
|
451
451
|
font-weight: 500;
|
|
452
452
|
line-height: 1.3;
|
|
453
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
453
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
// Pills "Interne" / "Solution" — pill style, 22px height
|
|
@@ -499,7 +499,7 @@
|
|
|
499
499
|
margin-top: 4px;
|
|
500
500
|
font-size: 11px;
|
|
501
501
|
font-weight: 500;
|
|
502
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
502
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
503
503
|
opacity: 0;
|
|
504
504
|
transition: opacity 80ms ease;
|
|
505
505
|
}
|
|
@@ -514,7 +514,7 @@
|
|
|
514
514
|
font-size: 11px;
|
|
515
515
|
font-weight: 500;
|
|
516
516
|
line-height: 1.3;
|
|
517
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
517
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
518
518
|
display: inline-flex;
|
|
519
519
|
align-items: center;
|
|
520
520
|
height: auto;
|
|
@@ -749,7 +749,7 @@
|
|
|
749
749
|
display: inline-flex;
|
|
750
750
|
align-items: center;
|
|
751
751
|
gap: 4px;
|
|
752
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
752
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
753
753
|
font-size: 11px;
|
|
754
754
|
font-weight: 500;
|
|
755
755
|
margin-left: auto;
|
|
@@ -910,7 +910,7 @@
|
|
|
910
910
|
background: none;
|
|
911
911
|
border: none;
|
|
912
912
|
cursor: pointer;
|
|
913
|
-
color: var(--theme-elevation-
|
|
913
|
+
color: var(--theme-elevation-650);
|
|
914
914
|
font-size: 13px;
|
|
915
915
|
font-weight: 600;
|
|
916
916
|
padding: 0;
|
|
@@ -941,7 +941,7 @@
|
|
|
941
941
|
|
|
942
942
|
.activityContent { flex: 1; min-width: 0; }
|
|
943
943
|
.activityText { font-weight: 500; color: var(--theme-text); font-size: 12px; }
|
|
944
|
-
.activityTime { font-size: 11px; color: var(--theme-elevation-
|
|
944
|
+
.activityTime { font-size: 11px; color: var(--theme-elevation-650); margin-top: 1px; }
|
|
945
945
|
|
|
946
946
|
// ─── DROPDOWN ───
|
|
947
947
|
.dropdown { position: relative; display: inline-block; }
|
|
@@ -1055,7 +1055,7 @@
|
|
|
1055
1055
|
|
|
1056
1056
|
.splitPreview {
|
|
1057
1057
|
font-size: 12px;
|
|
1058
|
-
color: var(--theme-elevation-
|
|
1058
|
+
color: var(--theme-elevation-650);
|
|
1059
1059
|
margin: 0 0 16px;
|
|
1060
1060
|
line-height: 1.5;
|
|
1061
1061
|
max-height: 80px;
|
|
@@ -1092,7 +1092,7 @@
|
|
|
1092
1092
|
background: none;
|
|
1093
1093
|
font-size: 13px;
|
|
1094
1094
|
font-weight: 600;
|
|
1095
|
-
color: var(--theme-elevation-
|
|
1095
|
+
color: var(--theme-elevation-650);
|
|
1096
1096
|
cursor: pointer;
|
|
1097
1097
|
&:hover { background: var(--theme-elevation-100); }
|
|
1098
1098
|
}
|
|
@@ -1145,7 +1145,7 @@
|
|
|
1145
1145
|
cursor: pointer;
|
|
1146
1146
|
font-size: 11px;
|
|
1147
1147
|
line-height: 1;
|
|
1148
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
1148
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
1149
1149
|
padding: 0;
|
|
1150
1150
|
display: inline-flex;
|
|
1151
1151
|
align-items: center;
|
|
@@ -1166,7 +1166,7 @@
|
|
|
1166
1166
|
font-size: 11px;
|
|
1167
1167
|
font-weight: 500;
|
|
1168
1168
|
line-height: 1;
|
|
1169
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
1169
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
1170
1170
|
cursor: pointer;
|
|
1171
1171
|
transition: border-color 80ms ease, color 80ms ease;
|
|
1172
1172
|
&:hover {
|
|
@@ -1607,7 +1607,7 @@
|
|
|
1607
1607
|
align-items: center;
|
|
1608
1608
|
gap: 6px;
|
|
1609
1609
|
font-size: 10px;
|
|
1610
|
-
color: var(--theme-elevation-
|
|
1610
|
+
color: var(--theme-elevation-650);
|
|
1611
1611
|
}
|
|
1612
1612
|
|
|
1613
1613
|
.previousTicketsStatus {
|
|
@@ -1755,7 +1755,7 @@
|
|
|
1755
1755
|
text-transform: uppercase;
|
|
1756
1756
|
letter-spacing: 0.08em;
|
|
1757
1757
|
line-height: 1.3;
|
|
1758
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
1758
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
1759
1759
|
margin: 12px 0 8px;
|
|
1760
1760
|
}
|
|
1761
1761
|
.aiCardNextActions {
|
|
@@ -1805,13 +1805,13 @@
|
|
|
1805
1805
|
}
|
|
1806
1806
|
.aiCardNextLabelDone {
|
|
1807
1807
|
text-decoration: line-through;
|
|
1808
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
1808
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
1809
1809
|
}
|
|
1810
1810
|
.aiCardNextWhen {
|
|
1811
1811
|
font-size: 11px;
|
|
1812
1812
|
font-weight: 500;
|
|
1813
1813
|
line-height: 1.3;
|
|
1814
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
1814
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
1815
1815
|
flex-shrink: 0;
|
|
1816
1816
|
}
|
|
1817
1817
|
|
|
@@ -1856,12 +1856,12 @@
|
|
|
1856
1856
|
margin: 0 2px;
|
|
1857
1857
|
}
|
|
1858
1858
|
.ticketNumberCat {
|
|
1859
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
1859
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
1860
1860
|
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
1861
1861
|
font-weight: 400;
|
|
1862
1862
|
}
|
|
1863
1863
|
.ticketNumberSource {
|
|
1864
|
-
color: var(--theme-text-tertiary, var(--theme-elevation-
|
|
1864
|
+
color: var(--theme-text-tertiary, var(--theme-elevation-650));
|
|
1865
1865
|
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
1866
1866
|
display: inline-flex;
|
|
1867
1867
|
align-items: center;
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
.subtitle {
|
|
28
28
|
font-size: 13px;
|
|
29
|
-
color: var(--theme-elevation-
|
|
29
|
+
color: var(--theme-elevation-650);
|
|
30
30
|
margin: 2px 0 0;
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -125,7 +125,7 @@
|
|
|
125
125
|
padding: 0 12px;
|
|
126
126
|
font-size: 13px;
|
|
127
127
|
font-weight: 500;
|
|
128
|
-
color: var(--theme-elevation-
|
|
128
|
+
color: var(--theme-elevation-650);
|
|
129
129
|
background: none;
|
|
130
130
|
border: none;
|
|
131
131
|
border-radius: 6px;
|
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
padding: 0 6px;
|
|
156
156
|
font-size: 11px;
|
|
157
157
|
font-weight: 600;
|
|
158
|
-
color: var(--theme-elevation-
|
|
158
|
+
color: var(--theme-elevation-650);
|
|
159
159
|
background: var(--theme-elevation-200);
|
|
160
160
|
border-radius: 999px;
|
|
161
161
|
}
|
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
.sortSelect {
|
|
180
180
|
font-size: 12px;
|
|
181
181
|
font-weight: 600;
|
|
182
|
-
color: var(--theme-elevation-
|
|
182
|
+
color: var(--theme-elevation-650);
|
|
183
183
|
background: none;
|
|
184
184
|
border: none;
|
|
185
185
|
cursor: pointer;
|
|
@@ -268,7 +268,7 @@
|
|
|
268
268
|
|
|
269
269
|
.client {
|
|
270
270
|
font-size: 12px;
|
|
271
|
-
color: var(--theme-elevation-
|
|
271
|
+
color: var(--theme-elevation-650);
|
|
272
272
|
overflow: hidden;
|
|
273
273
|
text-overflow: ellipsis;
|
|
274
274
|
white-space: nowrap;
|
|
@@ -333,7 +333,7 @@
|
|
|
333
333
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
|
334
334
|
font-size: 11px;
|
|
335
335
|
font-weight: 500;
|
|
336
|
-
color: var(--theme-elevation-
|
|
336
|
+
color: var(--theme-elevation-650);
|
|
337
337
|
display: flex;
|
|
338
338
|
gap: 12px;
|
|
339
339
|
z-index: 10;
|
|
@@ -15,7 +15,7 @@ $green: #16a34a;
|
|
|
15
15
|
|
|
16
16
|
.intro {
|
|
17
17
|
font-size: 13px;
|
|
18
|
-
color: var(--theme-elevation-
|
|
18
|
+
color: var(--theme-elevation-650);
|
|
19
19
|
margin: 0 0 24px;
|
|
20
20
|
line-height: 1.6;
|
|
21
21
|
}
|
|
@@ -59,7 +59,7 @@ $green: #16a34a;
|
|
|
59
59
|
|
|
60
60
|
.sectionChevron {
|
|
61
61
|
font-size: 16px;
|
|
62
|
-
color: var(--theme-elevation-
|
|
62
|
+
color: var(--theme-elevation-650);
|
|
63
63
|
transition: transform 150ms;
|
|
64
64
|
|
|
65
65
|
&.open {
|
|
@@ -73,7 +73,7 @@ $green: #16a34a;
|
|
|
73
73
|
|
|
74
74
|
.sectionDescription {
|
|
75
75
|
font-size: 12px;
|
|
76
|
-
color: var(--theme-elevation-
|
|
76
|
+
color: var(--theme-elevation-650);
|
|
77
77
|
margin: 0 0 16px;
|
|
78
78
|
line-height: 1.5;
|
|
79
79
|
}
|
|
@@ -97,7 +97,7 @@ $green: #16a34a;
|
|
|
97
97
|
|
|
98
98
|
.fieldDescription {
|
|
99
99
|
font-size: 11px;
|
|
100
|
-
color: var(--theme-elevation-
|
|
100
|
+
color: var(--theme-elevation-650);
|
|
101
101
|
font-weight: 400;
|
|
102
102
|
margin-top: 2px;
|
|
103
103
|
}
|
|
@@ -131,7 +131,7 @@ $green: #16a34a;
|
|
|
131
131
|
border-radius: 6px;
|
|
132
132
|
border: 1px solid var(--theme-elevation-300);
|
|
133
133
|
font-size: 13px;
|
|
134
|
-
color: var(--theme-elevation-
|
|
134
|
+
color: var(--theme-elevation-650);
|
|
135
135
|
background-color: var(--theme-elevation-100);
|
|
136
136
|
width: 100%;
|
|
137
137
|
max-width: 400px;
|
|
@@ -179,7 +179,7 @@ $green: #16a34a;
|
|
|
179
179
|
|
|
180
180
|
.inlineDesc {
|
|
181
181
|
font-size: 12px;
|
|
182
|
-
color: var(--theme-elevation-
|
|
182
|
+
color: var(--theme-elevation-650);
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
// ─── BADGE ───
|
|
@@ -251,7 +251,7 @@ $green: #16a34a;
|
|
|
251
251
|
|
|
252
252
|
.featureDesc {
|
|
253
253
|
font-size: 12px;
|
|
254
|
-
color: var(--theme-elevation-
|
|
254
|
+
color: var(--theme-elevation-650);
|
|
255
255
|
line-height: 1.4;
|
|
256
256
|
}
|
|
257
257
|
|
|
@@ -271,7 +271,7 @@ $green: #16a34a;
|
|
|
271
271
|
|
|
272
272
|
.aiToggleDesc {
|
|
273
273
|
font-size: 12px;
|
|
274
|
-
color: var(--theme-elevation-
|
|
274
|
+
color: var(--theme-elevation-650);
|
|
275
275
|
margin-left: 8px;
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -290,7 +290,7 @@ $green: #16a34a;
|
|
|
290
290
|
background-color: transparent;
|
|
291
291
|
cursor: pointer;
|
|
292
292
|
font-size: 12px;
|
|
293
|
-
color: var(--theme-elevation-
|
|
293
|
+
color: var(--theme-elevation-650);
|
|
294
294
|
white-space: nowrap;
|
|
295
295
|
}
|
|
296
296
|
|
|
@@ -303,7 +303,7 @@ $green: #16a34a;
|
|
|
303
303
|
|
|
304
304
|
.slaHint {
|
|
305
305
|
font-size: 12px;
|
|
306
|
-
color: var(--theme-elevation-
|
|
306
|
+
color: var(--theme-elevation-650);
|
|
307
307
|
}
|
|
308
308
|
|
|
309
309
|
// ─── AI SUB FEATURES LABEL ───
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
.subtitle {
|
|
31
|
-
color: var(--theme-elevation-
|
|
31
|
+
color: var(--theme-elevation-650);
|
|
32
32
|
margin: 4px 0 0;
|
|
33
33
|
font-size: 14px;
|
|
34
34
|
}
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
.label {
|
|
81
81
|
font-size: 11px;
|
|
82
82
|
font-weight: 700;
|
|
83
|
-
color: var(--theme-elevation-
|
|
83
|
+
color: var(--theme-elevation-650);
|
|
84
84
|
margin-bottom: 3px;
|
|
85
85
|
}
|
|
86
86
|
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
font-size: 10px;
|
|
147
147
|
font-weight: 700;
|
|
148
148
|
text-transform: uppercase;
|
|
149
|
-
color: var(--theme-elevation-
|
|
149
|
+
color: var(--theme-elevation-650);
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
.kpiValue {
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
border: 1px solid var(--theme-elevation-300);
|
|
202
202
|
border-radius: 8px;
|
|
203
203
|
background: var(--theme-elevation-100);
|
|
204
|
-
color: var(--theme-elevation-
|
|
204
|
+
color: var(--theme-elevation-650);
|
|
205
205
|
font-size: 14px;
|
|
206
206
|
}
|
|
207
207
|
|
|
@@ -257,13 +257,13 @@
|
|
|
257
257
|
|
|
258
258
|
.tdSubject {
|
|
259
259
|
padding: 6px 8px;
|
|
260
|
-
color: var(--theme-elevation-
|
|
260
|
+
color: var(--theme-elevation-650);
|
|
261
261
|
font-size: 12px;
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
.tdDescription {
|
|
265
265
|
padding: 6px 8px;
|
|
266
|
-
color: var(--theme-elevation-
|
|
266
|
+
color: var(--theme-elevation-650);
|
|
267
267
|
font-size: 12px;
|
|
268
268
|
}
|
|
269
269
|
|
package/src/styles/_tokens.scss
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Design tokens — based on Payload CMS CSS variables
|
|
2
2
|
$text: var(--theme-text);
|
|
3
|
-
|
|
3
|
+
// elevation-650, not 500: Payload's dark theme skips the 500 step, leaving it
|
|
4
|
+
// at rgb(128,128,128) — 3.62:1 light / 4.03:1 dark, below WCAG AA in both.
|
|
5
|
+
$text-secondary: var(--theme-elevation-650);
|
|
4
6
|
$text-muted: var(--theme-elevation-400);
|
|
5
7
|
$bg: var(--theme-elevation-50);
|
|
6
8
|
$bg-card: var(--theme-elevation-100);
|
package/src/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { RateLimitStore } from './utils/rateLimiter'
|
|
2
|
+
import type { RetentionConfig } from './utils/retention'
|
|
2
3
|
import type { InboundEmailInput } from './utils/webhookSecurity'
|
|
3
4
|
import type { Payload, PayloadRequest } from 'payload'
|
|
4
5
|
|
|
@@ -151,6 +152,25 @@ export interface SupportPluginConfig {
|
|
|
151
152
|
/** Shared rate-limit storage. Defaults to process-local memory. */
|
|
152
153
|
rateLimitStore?: RateLimitStore | 'payload'
|
|
153
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Automatic retention of the two journals the plugin writes.
|
|
157
|
+
*
|
|
158
|
+
* The plugin registers a daily Payload Jobs task that deletes rows older
|
|
159
|
+
* than the configured number of days. Set a value to 0 to keep that journal
|
|
160
|
+
* forever (the manual `DELETE /api/support/purge-logs` endpoint stays
|
|
161
|
+
* available either way).
|
|
162
|
+
*
|
|
163
|
+
* Defaults: auth-logs 180 days, email-logs 365 days, cron `0 30 3 * * *`.
|
|
164
|
+
*
|
|
165
|
+
* `false` registers no task at all. That is the ONLY value that changes the
|
|
166
|
+
* schema footprint: registering a task turns Payload's job queue on, which
|
|
167
|
+
* adds the `payload-jobs` collection and the `payload-jobs-stats` global to
|
|
168
|
+
* an app that had no jobs — two tables you would have to migrate. Pass
|
|
169
|
+
* `false` if you would rather purge from your own cron, calling
|
|
170
|
+
* `DELETE /api/support/purge-logs`.
|
|
171
|
+
*/
|
|
172
|
+
retention?: RetentionConfig | false
|
|
173
|
+
|
|
154
174
|
/** Sequential ticket number formatting. */
|
|
155
175
|
ticketNumber?: { prefix?: string; padding?: number }
|
|
156
176
|
|
package/src/utils/push.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Payload } from 'payload'
|
|
|
2
2
|
import type { CollectionSlugs } from './slugs'
|
|
3
3
|
import { dbFind, dbDelete } from './db'
|
|
4
4
|
import webpush from 'web-push'
|
|
5
|
+
import { assertPublicHost, validatePushEndpoint } from './urlSafety'
|
|
5
6
|
|
|
6
7
|
let configured = false
|
|
7
8
|
function ensureVapid(): boolean {
|
|
@@ -43,6 +44,27 @@ export async function sendPushToUser(
|
|
|
43
44
|
for (const s of subs.docs) {
|
|
44
45
|
const row = s as { id: number | string; endpoint?: string; p256dh?: string; auth?: string }
|
|
45
46
|
if (!row.endpoint || !row.p256dh || !row.auth) continue
|
|
47
|
+
|
|
48
|
+
// SEND-time SSRF guard, the second of the two layers `utils/urlSafety`
|
|
49
|
+
// describes. It is not redundant with the write-time check: it also covers
|
|
50
|
+
// rows persisted BEFORE that check existed, and a name whose DNS record is
|
|
51
|
+
// flipped to a private address after the row was accepted. `web-push`
|
|
52
|
+
// builds its own `https.request`, so `safeFetch` cannot wrap it — this is
|
|
53
|
+
// the layer that stands in for it.
|
|
54
|
+
//
|
|
55
|
+
// A blocked row is SKIPPED, never deleted: `assertPublicHost` fails closed
|
|
56
|
+
// on a resolver timeout, and a hiccup must not purge a legitimate agent's
|
|
57
|
+
// subscription. Only a real 404/410 from the push service prunes below.
|
|
58
|
+
const check = validatePushEndpoint(row.endpoint)
|
|
59
|
+
if (!check.ok || !check.url) {
|
|
60
|
+
console.warn('[support] Push endpoint refused (unsafe URL):', check.reason)
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
if (!(await assertPublicHost(check.url.hostname))) {
|
|
64
|
+
console.warn('[support] Push endpoint refused (host resolves to a private address)')
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
46
68
|
try {
|
|
47
69
|
await webpush.sendNotification(
|
|
48
70
|
{ endpoint: row.endpoint, keys: { p256dh: row.p256dh, auth: row.auth } },
|
package/src/utils/rateLimiter.ts
CHANGED
|
@@ -8,13 +8,59 @@ export interface RateLimitStore {
|
|
|
8
8
|
reset(key: string, context?: unknown): Promise<void>
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Hard ceiling on the number of distinct keys held at once, all endpoints
|
|
13
|
+
* combined. Sized for a busy install (thousands of client IPs inside one
|
|
14
|
+
* 15-minute window) while capping the map at a few hundred kilobytes.
|
|
15
|
+
*/
|
|
16
|
+
export const MAX_MEMORY_RATE_LIMIT_KEYS = 10_000
|
|
17
|
+
|
|
11
18
|
/**
|
|
12
19
|
* Process-local fallback store. Applications running more than one process
|
|
13
20
|
* should provide a persistent RateLimitStore through the plugin options.
|
|
21
|
+
*
|
|
22
|
+
* BOUNDED, because the keys come from anonymous HTTP traffic: `/support/login`
|
|
23
|
+
* and `/support/chatbot` key their limiter on the client IP, and the IP is read
|
|
24
|
+
* from `x-forwarded-for`. A caller rotating that header wrote one PERMANENT
|
|
25
|
+
* entry per value into a map that had no ceiling, no expiry sweep and no
|
|
26
|
+
* eviction — only `reset()` ever removed anything, and the login path never
|
|
27
|
+
* calls it. Worse, every fresh key opened a fresh window, so the limiter did
|
|
28
|
+
* not even slow down the flood that was exhausting it.
|
|
29
|
+
*
|
|
30
|
+
* Two independent bounds, the same pair `endpoints/typing.ts` already applies
|
|
31
|
+
* to its own module-level map: `clientIpRateKey` caps the SIZE of a key, the
|
|
32
|
+
* ceiling below caps their NUMBER.
|
|
14
33
|
*/
|
|
15
34
|
export class MemoryRateLimitStore implements RateLimitStore {
|
|
16
35
|
private readonly entries = new Map<string, RateLimitEntry>()
|
|
17
36
|
|
|
37
|
+
constructor(private readonly maxKeys: number = MAX_MEMORY_RATE_LIMIT_KEYS) {}
|
|
38
|
+
|
|
39
|
+
/** Distinct keys currently held. Exposed so the ceiling can be asserted. */
|
|
40
|
+
get size(): number {
|
|
41
|
+
return this.entries.size
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Reclaims every window that has already closed. */
|
|
45
|
+
private sweepExpired(now: number): void {
|
|
46
|
+
for (const [key, entry] of this.entries) {
|
|
47
|
+
if (now > entry.resetAt) this.entries.delete(key)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Soonest-closing window first, so the ceiling drops the least useful entry. */
|
|
52
|
+
private evictOldest(): void {
|
|
53
|
+
let oldestKey: string | null = null
|
|
54
|
+
let oldestResetAt = Infinity
|
|
55
|
+
for (const [key, entry] of this.entries) {
|
|
56
|
+
if (entry.resetAt < oldestResetAt) {
|
|
57
|
+
oldestResetAt = entry.resetAt
|
|
58
|
+
oldestKey = key
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (oldestKey !== null) this.entries.delete(oldestKey)
|
|
62
|
+
}
|
|
63
|
+
|
|
18
64
|
async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
|
|
19
65
|
const now = Date.now()
|
|
20
66
|
const current = this.entries.get(key)
|
|
@@ -22,6 +68,12 @@ export class MemoryRateLimitStore implements RateLimitStore {
|
|
|
22
68
|
? { count: 1, resetAt: now + windowMs }
|
|
23
69
|
: { ...current, count: current.count + 1 }
|
|
24
70
|
|
|
71
|
+
// Only a key that is not already held can grow the map.
|
|
72
|
+
if (!current && this.entries.size >= this.maxKeys) {
|
|
73
|
+
this.sweepExpired(now)
|
|
74
|
+
if (this.entries.size >= this.maxKeys) this.evictOldest()
|
|
75
|
+
}
|
|
76
|
+
|
|
25
77
|
this.entries.set(key, next)
|
|
26
78
|
return next
|
|
27
79
|
}
|
|
@@ -134,6 +186,52 @@ export function principalRateKey(
|
|
|
134
186
|
return `${collection}:${String(user.id)}`
|
|
135
187
|
}
|
|
136
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Longest textual IPv6 address, `xxxx:` * 7 + an embedded IPv4 literal.
|
|
191
|
+
* Nothing legitimate is longer; the cap is what makes the key bounded.
|
|
192
|
+
*/
|
|
193
|
+
const MAX_IP_KEY_LENGTH = 45
|
|
194
|
+
/** Dotted-quad, or the hex/`::`/zone alphabet of an IPv6 literal. Shape only. */
|
|
195
|
+
const IPV4_PATTERN = /^(?:\d{1,3}\.){3}\d{1,3}$/
|
|
196
|
+
const IPV6_PATTERN = /^[0-9a-fA-F:.%]+$/
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Identity part of a rate-limit key for an ANONYMOUS caller.
|
|
200
|
+
*
|
|
201
|
+
* `x-forwarded-for` is attacker-controlled — the note in `endpoints/login.ts`
|
|
202
|
+
* has always said so, and Payload's account lock is what actually stops a
|
|
203
|
+
* brute-force. But the header was also used RAW as the limiter key, and that is
|
|
204
|
+
* a second, distinct problem: a key is a stored object. A caller rotating the
|
|
205
|
+
* header minted one unbounded, never-reclaimed entry per value; a value can be
|
|
206
|
+
* most of Node's 16 KB header budget. This validates the SHAPE and caps the
|
|
207
|
+
* LENGTH, so a forged header can still pick a bucket but can no longer invent
|
|
208
|
+
* an unbounded number of them, nor make any single one large.
|
|
209
|
+
*
|
|
210
|
+
* Anything that is not an IP literal falls back to the shared `unknown` bucket.
|
|
211
|
+
* That is the fail-closed direction: an install with no proxy already puts
|
|
212
|
+
* every caller there, and one behind a proxy never lands there legitimately.
|
|
213
|
+
*/
|
|
214
|
+
export function clientIpRateKey(req: { headers: { get(name: string): string | null } }): string {
|
|
215
|
+
const candidate = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
|
|
216
|
+
|| req.headers.get('x-real-ip')?.trim()
|
|
217
|
+
|| ''
|
|
218
|
+
return normalizeIpKey(candidate)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Exposed separately so a caller holding an already-extracted address can reuse it. */
|
|
222
|
+
export function normalizeIpKey(candidate: string): string {
|
|
223
|
+
if (!candidate || candidate.length > MAX_IP_KEY_LENGTH) return 'unknown'
|
|
224
|
+
const host = candidate.startsWith('[') && candidate.endsWith(']')
|
|
225
|
+
? candidate.slice(1, -1)
|
|
226
|
+
: candidate
|
|
227
|
+
if (!host) return 'unknown'
|
|
228
|
+
if (IPV4_PATTERN.test(host)) {
|
|
229
|
+
return host.split('.').every((octet) => Number(octet) <= 255) ? host : 'unknown'
|
|
230
|
+
}
|
|
231
|
+
if (host.includes(':') && IPV6_PATTERN.test(host)) return host.toLowerCase()
|
|
232
|
+
return 'unknown'
|
|
233
|
+
}
|
|
234
|
+
|
|
137
235
|
export class RateLimiter {
|
|
138
236
|
private readonly store: RateLimitStore
|
|
139
237
|
private readonly prefix: string
|