@striae-org/striae 5.4.2 → 5.4.4

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 (52) hide show
  1. package/app/components/actions/case-export/download-handlers.ts +1 -1
  2. package/app/components/actions/case-export/metadata-helpers.ts +2 -4
  3. package/app/components/actions/case-import/zip-processing.ts +3 -3
  4. package/app/components/mobile-warning/mobile-warning.module.css +80 -0
  5. package/app/components/mobile-warning/mobile-warning.tsx +108 -0
  6. package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
  7. package/app/config-example/config.json +2 -2
  8. package/app/root.tsx +2 -0
  9. package/app/services/audit/audit-file-type.ts +0 -1
  10. package/app/services/audit/audit.service.ts +1 -1
  11. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
  12. package/app/services/audit/index.ts +0 -1
  13. package/app/types/audit.ts +1 -1
  14. package/app/utils/auth/auth-action-settings.ts +1 -1
  15. package/app/utils/data/permissions.ts +17 -15
  16. package/app/utils/forensics/audit-export-signature.ts +4 -4
  17. package/app/utils/forensics/export-verification.ts +3 -11
  18. package/package.json +2 -2
  19. package/workers/audit-worker/package.json +1 -1
  20. package/workers/audit-worker/src/audit-worker.example.ts +1 -1
  21. package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
  22. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  23. package/workers/data-worker/package.json +17 -17
  24. package/workers/data-worker/src/encryption-utils.ts +1 -1
  25. package/workers/data-worker/src/signing-payload-utils.ts +4 -4
  26. package/workers/data-worker/wrangler.jsonc.example +1 -1
  27. package/workers/image-worker/package.json +1 -1
  28. package/workers/image-worker/src/auth.ts +7 -0
  29. package/workers/image-worker/src/handlers/delete-image.ts +26 -0
  30. package/workers/image-worker/src/handlers/mint-signed-url.ts +83 -0
  31. package/workers/image-worker/src/handlers/serve-image.ts +65 -0
  32. package/workers/image-worker/src/handlers/upload-image.ts +62 -0
  33. package/workers/image-worker/src/image-worker.example.ts +3 -707
  34. package/workers/image-worker/src/router.ts +53 -0
  35. package/workers/image-worker/src/security/key-registry.ts +193 -0
  36. package/workers/image-worker/src/security/signed-url.ts +163 -0
  37. package/workers/image-worker/src/types.ts +68 -0
  38. package/workers/image-worker/src/utils/content-disposition.ts +33 -0
  39. package/workers/image-worker/src/utils/path-utils.ts +50 -0
  40. package/workers/image-worker/src/utils/storage-metadata.ts +27 -0
  41. package/workers/image-worker/wrangler.jsonc.example +1 -1
  42. package/workers/pdf-worker/package.json +1 -1
  43. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  44. package/workers/user-worker/package.json +1 -1
  45. package/workers/user-worker/src/handlers/user-routes.ts +23 -34
  46. package/workers/user-worker/src/user-worker.example.ts +17 -23
  47. package/workers/user-worker/wrangler.jsonc.example +1 -1
  48. package/wrangler.toml.example +1 -1
  49. package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
  50. package/app/services/audit/audit-export-csv.ts +0 -130
  51. package/app/services/audit/audit-export-report.ts +0 -205
  52. package/app/services/audit/audit-export.service.ts +0 -333
@@ -13,7 +13,20 @@ import type {
13
13
  function createJsonResponse(data: unknown, headers: ResponseHeaders, status: number = 200): Response {
14
14
  return new Response(JSON.stringify(data), {
15
15
  status,
16
- headers
16
+ headers: {
17
+ ...headers,
18
+ 'Content-Type': 'application/json; charset=utf-8'
19
+ }
20
+ });
21
+ }
22
+
23
+ function createTextResponse(message: string, headers: ResponseHeaders, status: number): Response {
24
+ return new Response(message, {
25
+ status,
26
+ headers: {
27
+ ...headers,
28
+ 'Content-Type': 'text/plain; charset=utf-8'
29
+ }
17
30
  });
18
31
  }
19
32
 
@@ -25,10 +38,7 @@ export async function handleGetUser(
25
38
  try {
26
39
  const userData = await readUserRecord(env, userUid);
27
40
  if (userData === null) {
28
- return new Response('User not found', {
29
- status: 404,
30
- headers: corsHeaders
31
- });
41
+ return createTextResponse('User not found', corsHeaders, 404);
32
42
  }
33
43
 
34
44
  return createJsonResponse(userData, corsHeaders);
@@ -36,10 +46,7 @@ export async function handleGetUser(
36
46
  const errorMessage = error instanceof Error ? error.message : 'Unknown user data read error';
37
47
  console.error('Failed to get user data:', { uid: userUid, reason: errorMessage });
38
48
 
39
- return new Response('Failed to get user data', {
40
- status: 500,
41
- headers: corsHeaders
42
- });
49
+ return createTextResponse('Failed to get user data', corsHeaders, 500);
43
50
  }
44
51
  }
45
52
 
@@ -91,10 +98,7 @@ export async function handleAddUser(
91
98
 
92
99
  return createJsonResponse(userData, corsHeaders, existingUser !== null ? 200 : 201);
93
100
  } catch {
94
- return new Response('Failed to save user data', {
95
- status: 500,
96
- headers: corsHeaders
97
- });
101
+ return createTextResponse('Failed to save user data', corsHeaders, 500);
98
102
  }
99
103
  }
100
104
 
@@ -115,10 +119,7 @@ export async function handleDeleteUser(
115
119
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
116
120
 
117
121
  if (errorMessage === 'User not found') {
118
- return new Response('User not found', {
119
- status: 404,
120
- headers: corsHeaders
121
- });
122
+ return createTextResponse('User not found', corsHeaders, 404);
122
123
  }
123
124
 
124
125
  return createJsonResponse({
@@ -135,7 +136,7 @@ export function handleDeleteUserWithProgress(
135
136
  ): Response {
136
137
  const sseHeaders: ResponseHeaders = {
137
138
  ...corsHeaders,
138
- 'Content-Type': 'text/event-stream',
139
+ 'Content-Type': 'text/event-stream; charset=utf-8',
139
140
  'Cache-Control': 'no-cache, no-transform',
140
141
  Connection: 'keep-alive'
141
142
  };
@@ -188,10 +189,7 @@ export async function handleAddCases(
188
189
  const { cases = [] }: AddCasesRequest = await request.json();
189
190
  const userData = await readUserRecord(env, userUid);
190
191
  if (!userData) {
191
- return new Response('User not found', {
192
- status: 404,
193
- headers: corsHeaders
194
- });
192
+ return createTextResponse('User not found', corsHeaders, 404);
195
193
  }
196
194
 
197
195
  const existingCases = userData.cases || [];
@@ -205,10 +203,7 @@ export async function handleAddCases(
205
203
 
206
204
  return createJsonResponse(userData, corsHeaders);
207
205
  } catch {
208
- return new Response('Failed to add cases', {
209
- status: 500,
210
- headers: corsHeaders
211
- });
206
+ return createTextResponse('Failed to add cases', corsHeaders, 500);
212
207
  }
213
208
  }
214
209
 
@@ -222,10 +217,7 @@ export async function handleDeleteCases(
222
217
  const { casesToDelete }: DeleteCasesRequest = await request.json();
223
218
  const userData = await readUserRecord(env, userUid);
224
219
  if (!userData) {
225
- return new Response('User not found', {
226
- status: 404,
227
- headers: corsHeaders
228
- });
220
+ return createTextResponse('User not found', corsHeaders, 404);
229
221
  }
230
222
 
231
223
  userData.cases = userData.cases.filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
@@ -234,9 +226,6 @@ export async function handleDeleteCases(
234
226
 
235
227
  return createJsonResponse(userData, corsHeaders);
236
228
  } catch {
237
- return new Response('Failed to delete cases', {
238
- status: 500,
239
- headers: corsHeaders
240
- });
229
+ return createTextResponse('Failed to delete cases', corsHeaders, 500);
241
230
  }
242
231
  }
@@ -13,10 +13,19 @@ import type { Env } from './types';
13
13
  const corsHeaders: Record<string, string> = {
14
14
  'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
15
15
  'Access-Control-Allow-Methods': 'GET, PUT, DELETE, OPTIONS',
16
- 'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key',
17
- 'Content-Type': 'application/json'
16
+ 'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key'
18
17
  };
19
18
 
19
+ function createTextResponse(message: string, status: number, headers: Record<string, string>): Response {
20
+ return new Response(message, {
21
+ status,
22
+ headers: {
23
+ ...headers,
24
+ 'Content-Type': 'text/plain; charset=utf-8'
25
+ }
26
+ });
27
+ }
28
+
20
29
  export default {
21
30
  async fetch(request: Request, env: Env): Promise<Response> {
22
31
  if (request.method === 'OPTIONS') {
@@ -39,7 +48,7 @@ export default {
39
48
  const isCasesEndpoint = parts[2] === USER_CASES_SEGMENT;
40
49
 
41
50
  if (!userUid) {
42
- return new Response('Not Found', { status: 404 });
51
+ return createTextResponse('Not Found', 404, corsHeaders);
43
52
  }
44
53
 
45
54
  // Handle regular cases endpoint
@@ -47,10 +56,7 @@ export default {
47
56
  switch (request.method) {
48
57
  case 'PUT': return handleAddCases(request, env, userUid, corsHeaders);
49
58
  case 'DELETE': return handleDeleteCases(request, env, userUid, corsHeaders);
50
- default: return new Response('Method not allowed', {
51
- status: 405,
52
- headers: corsHeaders
53
- });
59
+ default: return createTextResponse('Method not allowed', 405, corsHeaders);
54
60
  }
55
61
  }
56
62
 
@@ -64,31 +70,19 @@ export default {
64
70
  case 'DELETE': return streamProgress
65
71
  ? handleDeleteUserWithProgress(env, userUid, corsHeaders)
66
72
  : handleDeleteUser(env, userUid, corsHeaders);
67
- default: return new Response('Method not allowed', {
68
- status: 405,
69
- headers: corsHeaders
70
- });
73
+ default: return createTextResponse('Method not allowed', 405, corsHeaders);
71
74
  }
72
75
  } catch (error) {
73
76
  const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
74
77
  if (errorMessage === 'Unauthorized') {
75
- return new Response('Forbidden', {
76
- status: 403,
77
- headers: corsHeaders
78
- });
78
+ return createTextResponse('Forbidden', 403, corsHeaders);
79
79
  }
80
80
 
81
81
  if (errorMessage === 'User KV encryption is not fully configured') {
82
- return new Response(errorMessage, {
83
- status: 500,
84
- headers: corsHeaders
85
- });
82
+ return createTextResponse(errorMessage, 500, corsHeaders);
86
83
  }
87
84
 
88
- return new Response('Internal Server Error', {
89
- status: 500,
90
- headers: corsHeaders
91
- });
85
+ return createTextResponse('Internal Server Error', 500, corsHeaders);
92
86
  }
93
87
  }
94
88
  };
@@ -2,7 +2,7 @@
2
2
  "name": "USER_WORKER_NAME",
3
3
  "account_id": "ACCOUNT_ID",
4
4
  "main": "src/user-worker.ts",
5
- "compatibility_date": "2026-03-31",
5
+ "compatibility_date": "2026-04-02",
6
6
  "compatibility_flags": [
7
7
  "nodejs_compat"
8
8
  ],
@@ -1,6 +1,6 @@
1
1
  #:schema node_modules/wrangler/config-schema.json
2
2
  name = "PAGES_PROJECT_NAME"
3
- compatibility_date = "2026-03-31"
3
+ compatibility_date = "2026-04-02"
4
4
  compatibility_flags = ["nodejs_compat"]
5
5
  pages_build_output_dir = "./build/client"
6
6
 
@@ -1,176 +0,0 @@
1
- import { useCallback } from 'react';
2
- import type { Dispatch, SetStateAction } from 'react';
3
- import type { User } from 'firebase/auth';
4
- import { auditExportService } from '~/services/audit';
5
- import type { AuditTrail, ValidationAuditEntry } from '~/types';
6
-
7
- interface UseAuditViewerExportParams {
8
- user: User | null;
9
- effectiveCaseNumber?: string;
10
- filteredEntries: ValidationAuditEntry[];
11
- auditTrail: AuditTrail | null;
12
- setError: Dispatch<SetStateAction<string>>;
13
- }
14
-
15
- export const useAuditViewerExport = ({
16
- user,
17
- effectiveCaseNumber,
18
- filteredEntries,
19
- auditTrail,
20
- setError
21
- }: UseAuditViewerExportParams) => {
22
- const resolveExportContext = useCallback(() => {
23
- if (!user) {
24
- return null;
25
- }
26
-
27
- const identifier = effectiveCaseNumber || user.uid;
28
- const scopeType: 'case' | 'user' = effectiveCaseNumber ? 'case' : 'user';
29
-
30
- return {
31
- identifier,
32
- scopeType,
33
- context: {
34
- user,
35
- scopeType,
36
- scopeIdentifier: identifier,
37
- caseNumber: effectiveCaseNumber || undefined
38
- } as const
39
- };
40
- }, [user, effectiveCaseNumber]);
41
-
42
- const handleExportCSV = useCallback(async () => {
43
- const exportContextData = resolveExportContext();
44
- if (!exportContextData) {
45
- return;
46
- }
47
-
48
- const filename = auditExportService.generateFilename(
49
- exportContextData.scopeType,
50
- exportContextData.identifier,
51
- 'csv'
52
- );
53
-
54
- try {
55
- if (auditTrail && effectiveCaseNumber) {
56
- await auditExportService.exportAuditTrailToCSV(auditTrail, filename, exportContextData.context);
57
- } else {
58
- await auditExportService.exportToCSV(filteredEntries, filename, exportContextData.context);
59
- }
60
- } catch (exportError) {
61
- console.error('Export failed:', exportError);
62
- setError('Failed to export audit trail to CSV');
63
- }
64
- }, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
65
-
66
- const handleExportJSON = useCallback(async () => {
67
- const exportContextData = resolveExportContext();
68
- if (!exportContextData) {
69
- return;
70
- }
71
-
72
- const filename = auditExportService.generateFilename(
73
- exportContextData.scopeType,
74
- exportContextData.identifier,
75
- 'json'
76
- );
77
-
78
- try {
79
- if (auditTrail && effectiveCaseNumber) {
80
- await auditExportService.exportAuditTrailToJSON(auditTrail, filename, exportContextData.context);
81
- } else {
82
- await auditExportService.exportToJSON(filteredEntries, filename, exportContextData.context);
83
- }
84
- } catch (exportError) {
85
- console.error('Export failed:', exportError);
86
- setError('Failed to export audit trail to JSON');
87
- }
88
- }, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
89
-
90
- const handleGenerateReport = useCallback(async () => {
91
- const exportContextData = resolveExportContext();
92
- if (!exportContextData) {
93
- return;
94
- }
95
-
96
- const resolvedUser = exportContextData.context.user;
97
-
98
- const filename = `${exportContextData.scopeType}-audit-report-${exportContextData.identifier}-${new Date().toISOString().split('T')[0]}.txt`;
99
-
100
- try {
101
- let reportContent: string;
102
-
103
- if (auditTrail && effectiveCaseNumber) {
104
- reportContent = await auditExportService.generateReportSummary(auditTrail, exportContextData.context);
105
- } else {
106
- const totalEntries = filteredEntries.length;
107
- const successfulActions = filteredEntries.filter(entry => entry.result === 'success').length;
108
- const failedActions = filteredEntries.filter(entry => entry.result === 'failure').length;
109
-
110
- const actionCounts = filteredEntries.reduce((accumulator, entry) => {
111
- accumulator[entry.action] = (accumulator[entry.action] || 0) + 1;
112
- return accumulator;
113
- }, {} as Record<string, number>);
114
-
115
- const detectedDateRange = filteredEntries.length > 0
116
- ? {
117
- earliest: new Date(Math.min(...filteredEntries.map(entry => new Date(entry.timestamp).getTime()))),
118
- latest: new Date(Math.max(...filteredEntries.map(entry => new Date(entry.timestamp).getTime())))
119
- }
120
- : null;
121
-
122
- reportContent = `${effectiveCaseNumber ? 'CASE' : 'USER'} AUDIT REPORT
123
- Generated: ${new Date().toISOString()}
124
- ${effectiveCaseNumber ? `Case: ${effectiveCaseNumber}` : `User: ${resolvedUser.email}`}
125
- ${effectiveCaseNumber ? '' : `User ID: ${resolvedUser.uid}`}
126
-
127
- === SUMMARY ===
128
- Total Actions: ${totalEntries}
129
- Successful: ${successfulActions}
130
- Failed: ${failedActions}
131
- Success Rate: ${totalEntries > 0 ? ((successfulActions / totalEntries) * 100).toFixed(1) : 0}%
132
-
133
- ${detectedDateRange ? `Date Range: ${detectedDateRange.earliest.toLocaleDateString()} - ${detectedDateRange.latest.toLocaleDateString()}` : 'No entries found'}
134
-
135
- === ACTION BREAKDOWN ===
136
- ${Object.entries(actionCounts)
137
- .sort(([, actionCountA], [, actionCountB]) => actionCountB - actionCountA)
138
- .map(([action, count]) => `${action}: ${count}`)
139
- .join('\n')}
140
-
141
- === RECENT ACTIVITIES ===
142
- ${filteredEntries.slice(0, 10).map(entry =>
143
- `${new Date(entry.timestamp).toLocaleString()} | ${entry.action} | ${entry.result}${entry.details.caseNumber ? ` | Case: ${entry.details.caseNumber}` : ''}`
144
- ).join('\n')}
145
-
146
- Generated by Striae
147
- `;
148
-
149
- reportContent = await auditExportService.appendSignedReportIntegrity(
150
- reportContent,
151
- exportContextData.context,
152
- totalEntries
153
- );
154
- }
155
-
156
- const blob = new Blob([reportContent], { type: 'text/plain' });
157
- const url = URL.createObjectURL(blob);
158
- const anchor = document.createElement('a');
159
- anchor.href = url;
160
- anchor.download = filename;
161
- document.body.appendChild(anchor);
162
- anchor.click();
163
- document.body.removeChild(anchor);
164
- URL.revokeObjectURL(url);
165
- } catch (reportError) {
166
- console.error('Report generation failed:', reportError);
167
- setError('Failed to generate audit report');
168
- }
169
- }, [resolveExportContext, auditTrail, effectiveCaseNumber, filteredEntries, setError]);
170
-
171
- return {
172
- handleExportCSV,
173
- handleExportJSON,
174
- handleGenerateReport
175
- };
176
- };
@@ -1,130 +0,0 @@
1
- import { type ValidationAuditEntry } from '~/types';
2
-
3
- export const AUDIT_CSV_ENTRY_HEADERS = [
4
- 'Timestamp',
5
- 'User Email',
6
- 'Action',
7
- 'Result',
8
- 'File Name',
9
- 'File Type',
10
- 'Case Number',
11
- 'Confirmation ID',
12
- 'Original Examiner UID',
13
- 'Reviewing Examiner UID',
14
- 'File ID',
15
- 'Original Filename',
16
- 'File Size (MB)',
17
- 'MIME Type',
18
- 'Upload Method',
19
- 'Delete Reason',
20
- 'Annotation ID',
21
- 'Annotation Type',
22
- 'Annotation Tool',
23
- 'Session ID',
24
- 'User Agent',
25
- 'Processing Time (ms)',
26
- 'Hash Valid',
27
- 'Validation Errors',
28
- 'Security Issues',
29
- 'Workflow Phase',
30
- 'Profile Field',
31
- 'Old Value',
32
- 'New Value',
33
- 'Badge/ID',
34
- 'Total Confirmations In File',
35
- 'Confirmations Successfully Imported',
36
- 'Validation Steps Failed',
37
- 'Case Name',
38
- 'Total Files',
39
- 'MFA Method',
40
- 'Security Incident Type',
41
- 'Security Severity',
42
- 'Confirmed Files'
43
- ];
44
-
45
- const formatForCSV = (value?: string | number | null): string => {
46
- if (value === undefined || value === null) return '';
47
- const str = String(value);
48
- if (str.includes(',') || str.includes('"') || str.includes('\n')) {
49
- return `"${str.replace(/"/g, '""')}"`;
50
- }
51
- return str;
52
- };
53
-
54
- const getSecurityIssues = (entry: ValidationAuditEntry): string => {
55
- const securityChecks = entry.details.securityChecks;
56
- if (!securityChecks) {
57
- return '';
58
- }
59
-
60
- const issues = [];
61
-
62
- if (securityChecks.selfConfirmationPrevented === true) {
63
- issues.push('selfConfirmationPrevented');
64
- }
65
-
66
- if (securityChecks.fileIntegrityValid === false) {
67
- issues.push('fileIntegrityValid');
68
- }
69
-
70
- if (securityChecks.exporterUidValidated === false) {
71
- issues.push('exporterUidValidated');
72
- }
73
-
74
- return issues.join('; ');
75
- };
76
-
77
- export const entryToCSVRow = (entry: ValidationAuditEntry): string => {
78
- const fileDetails = entry.details.fileDetails;
79
- const annotationDetails = entry.details.annotationDetails;
80
- const sessionDetails = entry.details.sessionDetails;
81
- const userProfileDetails = entry.details.userProfileDetails;
82
- const caseDetails = entry.details.caseDetails;
83
- const performanceMetrics = entry.details.performanceMetrics;
84
- const securityDetails = entry.details.securityDetails;
85
- const securityIssues = getSecurityIssues(entry);
86
-
87
- const values = [
88
- formatForCSV(entry.timestamp),
89
- formatForCSV(entry.userEmail),
90
- formatForCSV(entry.action),
91
- formatForCSV(entry.result),
92
- formatForCSV(entry.details.fileName),
93
- formatForCSV(entry.details.fileType),
94
- formatForCSV(entry.details.caseNumber),
95
- formatForCSV(entry.details.confirmationId),
96
- formatForCSV(entry.details.originalExaminerUid),
97
- formatForCSV(entry.details.reviewingExaminerUid),
98
- formatForCSV(fileDetails?.fileId),
99
- formatForCSV(fileDetails?.originalFileName),
100
- fileDetails?.fileSize ? (fileDetails.fileSize / 1024 / 1024).toFixed(2) : '',
101
- formatForCSV(fileDetails?.mimeType),
102
- formatForCSV(fileDetails?.uploadMethod),
103
- formatForCSV(fileDetails?.deleteReason),
104
- formatForCSV(annotationDetails?.annotationId),
105
- formatForCSV(annotationDetails?.annotationType),
106
- formatForCSV(annotationDetails?.tool),
107
- formatForCSV(sessionDetails?.sessionId),
108
- formatForCSV(sessionDetails?.userAgent),
109
- performanceMetrics?.processingTimeMs || '',
110
- entry.details.hashValid !== undefined ? (entry.details.hashValid ? 'Yes' : 'No') : '',
111
- formatForCSV(entry.details.validationErrors?.join('; ')),
112
- formatForCSV(securityIssues),
113
- formatForCSV(entry.details.workflowPhase),
114
- formatForCSV(userProfileDetails?.profileField),
115
- formatForCSV(userProfileDetails?.oldValue),
116
- formatForCSV(userProfileDetails?.newValue),
117
- formatForCSV(userProfileDetails?.badgeId),
118
- caseDetails?.totalAnnotations?.toString() || '',
119
- performanceMetrics?.validationStepsCompleted?.toString() || '',
120
- performanceMetrics?.validationStepsFailed?.toString() || '',
121
- formatForCSV(caseDetails?.newCaseName || caseDetails?.oldCaseName),
122
- caseDetails?.totalFiles?.toString() || '',
123
- formatForCSV(securityDetails?.mfaMethod),
124
- formatForCSV(securityDetails?.incidentType),
125
- formatForCSV(securityDetails?.severity),
126
- formatForCSV(caseDetails?.confirmedFileNames?.join('; '))
127
- ];
128
-
129
- return values.join(',');
130
- };