@striae-org/striae 3.0.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 (223) hide show
  1. package/.env.example +100 -0
  2. package/LICENSE +190 -0
  3. package/NOTICE +18 -0
  4. package/README.md +133 -0
  5. package/app/components/actions/case-export/core-export.ts +328 -0
  6. package/app/components/actions/case-export/data-processing.ts +167 -0
  7. package/app/components/actions/case-export/download-handlers.ts +900 -0
  8. package/app/components/actions/case-export/index.ts +41 -0
  9. package/app/components/actions/case-export/metadata-helpers.ts +107 -0
  10. package/app/components/actions/case-export/types-constants.ts +56 -0
  11. package/app/components/actions/case-export/validation-utils.ts +25 -0
  12. package/app/components/actions/case-export.ts +4 -0
  13. package/app/components/actions/case-import/annotation-import.ts +35 -0
  14. package/app/components/actions/case-import/confirmation-import.ts +363 -0
  15. package/app/components/actions/case-import/image-operations.ts +61 -0
  16. package/app/components/actions/case-import/index.ts +39 -0
  17. package/app/components/actions/case-import/orchestrator.ts +420 -0
  18. package/app/components/actions/case-import/storage-operations.ts +270 -0
  19. package/app/components/actions/case-import/validation.ts +189 -0
  20. package/app/components/actions/case-import/zip-processing.ts +413 -0
  21. package/app/components/actions/case-manage.ts +524 -0
  22. package/app/components/actions/case-review.ts +4 -0
  23. package/app/components/actions/confirm-export.ts +351 -0
  24. package/app/components/actions/generate-pdf.ts +210 -0
  25. package/app/components/actions/image-manage.ts +385 -0
  26. package/app/components/actions/notes-manage.ts +33 -0
  27. package/app/components/actions/signout.module.css +15 -0
  28. package/app/components/actions/signout.tsx +50 -0
  29. package/app/components/audit/user-audit-viewer.tsx +975 -0
  30. package/app/components/audit/user-audit.module.css +568 -0
  31. package/app/components/auth/auth-provider.tsx +78 -0
  32. package/app/components/auth/mfa-enrollment.module.css +268 -0
  33. package/app/components/auth/mfa-enrollment.tsx +398 -0
  34. package/app/components/auth/mfa-verification.module.css +251 -0
  35. package/app/components/auth/mfa-verification.tsx +295 -0
  36. package/app/components/button/button.module.css +63 -0
  37. package/app/components/button/button.tsx +46 -0
  38. package/app/components/canvas/box-annotations/box-annotations.module.css +170 -0
  39. package/app/components/canvas/box-annotations/box-annotations.tsx +634 -0
  40. package/app/components/canvas/canvas.module.css +314 -0
  41. package/app/components/canvas/canvas.tsx +449 -0
  42. package/app/components/canvas/confirmation/confirmation.module.css +187 -0
  43. package/app/components/canvas/confirmation/confirmation.tsx +214 -0
  44. package/app/components/colors/colors.module.css +59 -0
  45. package/app/components/colors/colors.tsx +68 -0
  46. package/app/components/form/base-form.tsx +21 -0
  47. package/app/components/form/form-button.tsx +28 -0
  48. package/app/components/form/form-field.tsx +53 -0
  49. package/app/components/form/form-message.tsx +17 -0
  50. package/app/components/form/form-toggle.tsx +23 -0
  51. package/app/components/form/form.module.css +427 -0
  52. package/app/components/form/index.ts +6 -0
  53. package/app/components/icon/icon.module.css +3 -0
  54. package/app/components/icon/icon.tsx +27 -0
  55. package/app/components/icon/icons.svg +102 -0
  56. package/app/components/icon/manifest.json +110 -0
  57. package/app/components/sidebar/case-export/case-export.module.css +386 -0
  58. package/app/components/sidebar/case-export/case-export.tsx +317 -0
  59. package/app/components/sidebar/case-import/case-import.module.css +626 -0
  60. package/app/components/sidebar/case-import/case-import.tsx +404 -0
  61. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +72 -0
  62. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +72 -0
  63. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +71 -0
  64. package/app/components/sidebar/case-import/components/ExistingCaseSection.tsx +40 -0
  65. package/app/components/sidebar/case-import/components/FileSelector.tsx +161 -0
  66. package/app/components/sidebar/case-import/components/ProgressSection.tsx +46 -0
  67. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +101 -0
  68. package/app/components/sidebar/case-import/hooks/useImportExecution.ts +152 -0
  69. package/app/components/sidebar/case-import/hooks/useImportState.ts +88 -0
  70. package/app/components/sidebar/case-import/index.ts +18 -0
  71. package/app/components/sidebar/case-import/utils/file-validation.ts +43 -0
  72. package/app/components/sidebar/cases/case-sidebar.tsx +827 -0
  73. package/app/components/sidebar/cases/cases-modal.module.css +166 -0
  74. package/app/components/sidebar/cases/cases-modal.tsx +201 -0
  75. package/app/components/sidebar/cases/cases.module.css +713 -0
  76. package/app/components/sidebar/files/files-modal.module.css +209 -0
  77. package/app/components/sidebar/files/files-modal.tsx +239 -0
  78. package/app/components/sidebar/hash/hash-utility.module.css +366 -0
  79. package/app/components/sidebar/hash/hash-utility.tsx +982 -0
  80. package/app/components/sidebar/notes/notes-modal.tsx +51 -0
  81. package/app/components/sidebar/notes/notes-sidebar.tsx +491 -0
  82. package/app/components/sidebar/notes/notes.module.css +360 -0
  83. package/app/components/sidebar/sidebar-container.tsx +149 -0
  84. package/app/components/sidebar/sidebar.module.css +321 -0
  85. package/app/components/sidebar/sidebar.tsx +215 -0
  86. package/app/components/sidebar/upload/image-upload-zone.module.css +123 -0
  87. package/app/components/sidebar/upload/image-upload-zone.tsx +330 -0
  88. package/app/components/theme-provider/theme-provider.tsx +131 -0
  89. package/app/components/theme-provider/theme.ts +155 -0
  90. package/app/components/toast/toast.module.css +137 -0
  91. package/app/components/toast/toast.tsx +56 -0
  92. package/app/components/toolbar/toolbar-color-selector.module.css +171 -0
  93. package/app/components/toolbar/toolbar-color-selector.tsx +129 -0
  94. package/app/components/toolbar/toolbar.module.css +42 -0
  95. package/app/components/toolbar/toolbar.tsx +167 -0
  96. package/app/components/user/delete-account.module.css +274 -0
  97. package/app/components/user/delete-account.tsx +471 -0
  98. package/app/components/user/inactivity-warning.module.css +145 -0
  99. package/app/components/user/inactivity-warning.tsx +84 -0
  100. package/app/components/user/manage-profile.module.css +190 -0
  101. package/app/components/user/manage-profile.tsx +253 -0
  102. package/app/components/user/mfa-phone-update.tsx +739 -0
  103. package/app/config-example/admin-service.json +13 -0
  104. package/app/config-example/config.json +17 -0
  105. package/app/config-example/firebase.ts +21 -0
  106. package/app/config-example/inactivity.ts +13 -0
  107. package/app/config-example/meta-config.json +6 -0
  108. package/app/contexts/auth.context.ts +12 -0
  109. package/app/entry.client.tsx +12 -0
  110. package/app/entry.server.tsx +44 -0
  111. package/app/hooks/useInactivityTimeout.ts +110 -0
  112. package/app/root.tsx +170 -0
  113. package/app/routes/_index.tsx +16 -0
  114. package/app/routes/auth/emailActionHandler.module.css +232 -0
  115. package/app/routes/auth/emailActionHandler.tsx +405 -0
  116. package/app/routes/auth/emailVerification.tsx +120 -0
  117. package/app/routes/auth/login.module.css +523 -0
  118. package/app/routes/auth/login.tsx +654 -0
  119. package/app/routes/auth/passwordReset.module.css +274 -0
  120. package/app/routes/auth/passwordReset.tsx +154 -0
  121. package/app/routes/auth/route.ts +16 -0
  122. package/app/routes/mobile-prevented/mobilePrevented.module.css +47 -0
  123. package/app/routes/mobile-prevented/mobilePrevented.tsx +26 -0
  124. package/app/routes/mobile-prevented/route.ts +14 -0
  125. package/app/routes/striae/striae.module.css +30 -0
  126. package/app/routes/striae/striae.tsx +417 -0
  127. package/app/services/audit-export.service.ts +755 -0
  128. package/app/services/audit.service.ts +1454 -0
  129. package/app/services/firebase-errors.ts +106 -0
  130. package/app/services/firebase.ts +15 -0
  131. package/app/styles/legal-pages.module.css +113 -0
  132. package/app/styles/root.module.css +146 -0
  133. package/app/tailwind.css +225 -0
  134. package/app/types/annotations.ts +45 -0
  135. package/app/types/audit.ts +301 -0
  136. package/app/types/case.ts +90 -0
  137. package/app/types/export.ts +8 -0
  138. package/app/types/file.ts +30 -0
  139. package/app/types/import.ts +107 -0
  140. package/app/types/index.ts +24 -0
  141. package/app/types/user.ts +38 -0
  142. package/app/utils/SHA256.ts +461 -0
  143. package/app/utils/annotation-timestamp.ts +25 -0
  144. package/app/utils/audit-export-signature.ts +117 -0
  145. package/app/utils/auth-action-settings.ts +48 -0
  146. package/app/utils/auth.ts +34 -0
  147. package/app/utils/batch-operations.ts +135 -0
  148. package/app/utils/confirmation-signature.ts +193 -0
  149. package/app/utils/data-operations.ts +871 -0
  150. package/app/utils/device-detection.ts +5 -0
  151. package/app/utils/html-sanitizer.ts +80 -0
  152. package/app/utils/id-generator.ts +36 -0
  153. package/app/utils/meta.ts +48 -0
  154. package/app/utils/mfa-phone.ts +97 -0
  155. package/app/utils/mfa.ts +79 -0
  156. package/app/utils/password-policy.ts +28 -0
  157. package/app/utils/permissions.ts +562 -0
  158. package/app/utils/signature-utils.ts +160 -0
  159. package/app/utils/style.ts +83 -0
  160. package/app/utils/version.ts +5 -0
  161. package/firebase.json +11 -0
  162. package/functions/[[path]].ts +10 -0
  163. package/package.json +138 -0
  164. package/postcss.config.js +6 -0
  165. package/public/.well-known/publickey.info@striae.org.asc +17 -0
  166. package/public/.well-known/security.txt +7 -0
  167. package/public/_headers +28 -0
  168. package/public/_routes.json +13 -0
  169. package/public/assets/striae.jpg +0 -0
  170. package/public/clear.jpg +0 -0
  171. package/public/favicon.ico +0 -0
  172. package/public/favicon.svg +9 -0
  173. package/public/icon-256.png +0 -0
  174. package/public/icon-512.png +0 -0
  175. package/public/logo-dark.png +0 -0
  176. package/public/manifest.json +25 -0
  177. package/public/oin-badge.png +0 -0
  178. package/public/shortcut.png +0 -0
  179. package/public/social-image.png +0 -0
  180. package/public/striae-ascii.txt +10 -0
  181. package/scripts/deploy-all.sh +100 -0
  182. package/scripts/deploy-config.sh +940 -0
  183. package/scripts/deploy-pages.sh +34 -0
  184. package/scripts/deploy-worker-secrets.sh +215 -0
  185. package/scripts/dev.cjs +23 -0
  186. package/scripts/install-workers.sh +88 -0
  187. package/scripts/run-eslint.cjs +35 -0
  188. package/scripts/update-compatibility-dates.cjs +124 -0
  189. package/scripts/update-markdown-versions.cjs +43 -0
  190. package/tailwind.config.ts +22 -0
  191. package/tsconfig.json +33 -0
  192. package/vite.config.ts +35 -0
  193. package/worker-configuration.d.ts +7490 -0
  194. package/workers/audit-worker/package.json +17 -0
  195. package/workers/audit-worker/src/audit-worker.example.ts +195 -0
  196. package/workers/audit-worker/worker-configuration.d.ts +7448 -0
  197. package/workers/audit-worker/wrangler.jsonc.example +29 -0
  198. package/workers/data-worker/package.json +17 -0
  199. package/workers/data-worker/src/data-worker.example.ts +267 -0
  200. package/workers/data-worker/src/signature-utils.ts +79 -0
  201. package/workers/data-worker/src/signing-payload-utils.ts +290 -0
  202. package/workers/data-worker/worker-configuration.d.ts +7448 -0
  203. package/workers/data-worker/wrangler.jsonc.example +30 -0
  204. package/workers/image-worker/package.json +17 -0
  205. package/workers/image-worker/src/image-worker.example.ts +180 -0
  206. package/workers/image-worker/worker-configuration.d.ts +7447 -0
  207. package/workers/image-worker/wrangler.jsonc.example +22 -0
  208. package/workers/keys-worker/package.json +17 -0
  209. package/workers/keys-worker/src/keys.example.ts +66 -0
  210. package/workers/keys-worker/src/keys.ts +66 -0
  211. package/workers/keys-worker/worker-configuration.d.ts +7447 -0
  212. package/workers/keys-worker/wrangler.jsonc.example +22 -0
  213. package/workers/pdf-worker/package.json +17 -0
  214. package/workers/pdf-worker/src/format-striae.ts +534 -0
  215. package/workers/pdf-worker/src/pdf-worker.example.ts +119 -0
  216. package/workers/pdf-worker/src/report-types.ts +69 -0
  217. package/workers/pdf-worker/worker-configuration.d.ts +7448 -0
  218. package/workers/pdf-worker/wrangler.jsonc.example +26 -0
  219. package/workers/user-worker/package.json +17 -0
  220. package/workers/user-worker/src/user-worker.example.ts +636 -0
  221. package/workers/user-worker/worker-configuration.d.ts +7448 -0
  222. package/workers/user-worker/wrangler.jsonc.example +29 -0
  223. package/wrangler.toml.example +8 -0
@@ -0,0 +1,900 @@
1
+ import { User } from 'firebase/auth';
2
+ import { FileData, AllCasesExportData, CaseExportData, ExportOptions } from '~/types';
3
+ import { getImageUrl } from '../image-manage';
4
+ import { generateForensicManifestSecure, calculateSHA256Secure } from '~/utils/SHA256';
5
+ import { signForensicManifest } from '~/utils/data-operations';
6
+ import { ExportFormat, formatDateForFilename, CSV_HEADERS } from './types-constants';
7
+ import { protectExcelWorksheet, addForensicDataWarning } from './metadata-helpers';
8
+ import { generateMetadataRows, generateCSVContent, processFileDataForTabular } from './data-processing';
9
+ import { exportCaseData } from './core-export';
10
+ import { auditService } from '~/services/audit.service';
11
+
12
+ /**
13
+ * Generate export filename with embedded ID to prevent collisions
14
+ * Format: {originalFilename}-{id}.{extension}
15
+ * Example: "evidence.jpg" with ID "abc123" becomes "evidence-abc123.jpg"
16
+ */
17
+ function generateExportFilename(originalFilename: string, id: string): string {
18
+ const lastDotIndex = originalFilename.lastIndexOf('.');
19
+
20
+ if (lastDotIndex === -1) {
21
+ // No extension found
22
+ return `${originalFilename}-${id}`;
23
+ }
24
+
25
+ const basename = originalFilename.substring(0, lastDotIndex);
26
+ const extension = originalFilename.substring(lastDotIndex);
27
+
28
+ return `${basename}-${id}${extension}`;
29
+ }
30
+
31
+ /**
32
+ * Download all cases data as JSON file
33
+ */
34
+ export async function downloadAllCasesAsJSON(user: User, exportData: AllCasesExportData): Promise<void> {
35
+ const startTime = Date.now();
36
+
37
+ try {
38
+ // Start audit workflow
39
+ const workflowId = auditService.startWorkflow('all-cases');
40
+
41
+ const dataStr = JSON.stringify(exportData, null, 2);
42
+
43
+ // Calculate hash for integrity verification
44
+ const hash = await calculateSHA256Secure(dataStr);
45
+
46
+ // Create final export with hash included
47
+ const finalExportData = {
48
+ ...exportData,
49
+ metadata: {
50
+ ...exportData.metadata,
51
+ hash: hash.toUpperCase(),
52
+ integrityNote: 'Verify by recalculating SHA256 of this entire JSON content'
53
+ }
54
+ };
55
+
56
+ const finalDataStr = JSON.stringify(finalExportData, null, 2);
57
+ const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(finalDataStr);
58
+
59
+ const exportFileName = `striae-all-cases-export-${formatDateForFilename(new Date())}.json`;
60
+
61
+ const linkElement = document.createElement('a');
62
+ linkElement.setAttribute('href', dataUri);
63
+ linkElement.setAttribute('download', exportFileName);
64
+ linkElement.click();
65
+
66
+ // Log successful export audit event
67
+ const endTime = Date.now();
68
+ await auditService.logCaseExport(
69
+ user,
70
+ 'all-cases',
71
+ exportFileName,
72
+ 'success',
73
+ [],
74
+ {
75
+ processingTimeMs: endTime - startTime,
76
+ fileSizeBytes: finalDataStr.length,
77
+ validationStepsCompleted: exportData.cases.length,
78
+ validationStepsFailed: exportData.cases.filter(c => c.summary?.exportError).length
79
+ },
80
+ 'json',
81
+ false // JSON format is not protected
82
+ );
83
+
84
+ // End audit workflow
85
+ auditService.endWorkflow();
86
+
87
+ } catch (error) {
88
+ console.error('Download failed:', error);
89
+
90
+ // Log failed export audit event
91
+ const endTime = Date.now();
92
+ await auditService.logCaseExport(
93
+ user,
94
+ 'all-cases',
95
+ 'striae-all-cases-export.json',
96
+ 'failure',
97
+ [error instanceof Error ? error.message : 'Unknown error'],
98
+ {
99
+ processingTimeMs: endTime - startTime,
100
+ fileSizeBytes: 0,
101
+ validationStepsCompleted: 0,
102
+ validationStepsFailed: 1
103
+ },
104
+ 'json',
105
+ false
106
+ );
107
+
108
+ // End audit workflow
109
+ auditService.endWorkflow();
110
+
111
+ throw new Error('Failed to download all cases export file');
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Download all cases data as Excel file with multiple worksheets
117
+ */
118
+ export async function downloadAllCasesAsCSV(user: User, exportData: AllCasesExportData, protectForensicData: boolean = true): Promise<void> {
119
+ const startTime = Date.now();
120
+
121
+ try {
122
+ // Start audit workflow
123
+ const workflowId = auditService.startWorkflow('all-cases');
124
+
125
+ // Dynamic import of XLSX to avoid bundle size issues
126
+ const XLSX = await import('xlsx');
127
+
128
+ const workbook = XLSX.utils.book_new();
129
+ let exportPassword: string | undefined;
130
+
131
+ // Create summary worksheet
132
+ const summaryDataRows = [
133
+ ['Export Date', new Date().toISOString()],
134
+ ['Exported By (Email)', exportData.metadata.exportedBy || 'N/A'],
135
+ ['Exported By (UID)', exportData.metadata.exportedByUid || 'N/A'],
136
+ ['Exported By (Name)', exportData.metadata.exportedByName || 'N/A'],
137
+ ['Exported By (Company)', exportData.metadata.exportedByCompany || 'N/A'],
138
+ ['Striae Export Schema Version', '1.0'],
139
+ ['Total Cases', exportData.cases.length],
140
+ ['Successful Exports', exportData.cases.filter(c => !c.summary?.exportError).length],
141
+ ['Failed Exports', exportData.cases.filter(c => c.summary?.exportError).length],
142
+ ['Total Files (All Cases)', exportData.metadata.totalFiles],
143
+ ['Total Annotations (All Cases)', exportData.metadata.totalAnnotations],
144
+ ['Total Confirmations (All Cases)', exportData.metadata.totalConfirmations || 0],
145
+ ['Total Confirmations Requested (All Cases)', exportData.metadata.totalConfirmationsRequested || 0]
146
+ ];
147
+
148
+ // XLSX files are inherently protected, no hash validation needed
149
+ const summaryData = [
150
+ protectForensicData ? ['CASE DATA - PROTECTED EXPORT'] : ['Striae - All Cases Export Summary'],
151
+ protectForensicData ? ['WARNING: This workbook contains evidence data and is protected from editing.'] : [''],
152
+ [''],
153
+ ...summaryDataRows,
154
+ [''],
155
+ ['Case Details'],
156
+ [
157
+ 'Case Number',
158
+ 'Case Created Date',
159
+ 'Export Status',
160
+ 'Export Date',
161
+ 'Exported By (Email)',
162
+ 'Exported By (UID)',
163
+ 'Exported By (Name)',
164
+ 'Exported By (Company)',
165
+ 'Schema Version',
166
+ 'Total Files',
167
+ 'Files with Annotations',
168
+ 'Files without Annotations',
169
+ 'Total Box Annotations',
170
+ 'Files with Confirmations',
171
+ 'Files with Confirmations Requested',
172
+ 'Last Modified',
173
+ 'Earliest Annotation Date',
174
+ 'Latest Annotation Date',
175
+ 'Export Error'
176
+ ],
177
+ ...exportData.cases.map(caseData => [
178
+ caseData.metadata.caseNumber,
179
+ caseData.metadata.caseCreatedDate,
180
+ caseData.summary?.exportError ? 'Failed' : 'Success',
181
+ caseData.metadata.exportDate,
182
+ caseData.metadata.exportedBy || 'N/A',
183
+ caseData.metadata.exportedByUid || 'N/A',
184
+ caseData.metadata.exportedByName || 'N/A',
185
+ caseData.metadata.exportedByCompany || 'N/A',
186
+ caseData.metadata.striaeExportSchemaVersion,
187
+ caseData.metadata.totalFiles,
188
+ caseData.summary?.filesWithAnnotations || 0,
189
+ caseData.summary?.filesWithoutAnnotations || 0,
190
+ caseData.summary?.totalBoxAnnotations || 0,
191
+ caseData.summary?.filesWithConfirmations || 0,
192
+ caseData.summary?.filesWithConfirmationsRequested || 0,
193
+ caseData.summary?.lastModified || '',
194
+ caseData.summary?.earliestAnnotationDate || '',
195
+ caseData.summary?.latestAnnotationDate || '',
196
+ caseData.summary?.exportError || ''
197
+ ])
198
+ ];
199
+
200
+ const summaryWorksheet = XLSX.utils.aoa_to_sheet(summaryData);
201
+
202
+ // Protect summary worksheet if forensic protection is enabled
203
+ if (protectForensicData) {
204
+ exportPassword = protectExcelWorksheet(summaryWorksheet);
205
+ }
206
+
207
+ XLSX.utils.book_append_sheet(workbook, summaryWorksheet, 'Summary');
208
+
209
+ // Create a worksheet for each case
210
+ exportData.cases.forEach((caseData, index) => {
211
+ if (caseData.summary?.exportError) {
212
+ // For failed cases, create a simple error sheet
213
+ const errorData = [
214
+ [`Case ${caseData.metadata.caseNumber} - Export Failed`],
215
+ [''],
216
+ ['Error:', caseData.summary.exportError],
217
+ ['Case Number:', caseData.metadata.caseNumber],
218
+ ['Total Files:', caseData.metadata.totalFiles]
219
+ ];
220
+ const errorWorksheet = XLSX.utils.aoa_to_sheet(errorData);
221
+
222
+ if (protectForensicData && exportPassword) {
223
+ protectExcelWorksheet(errorWorksheet, exportPassword);
224
+ }
225
+
226
+ XLSX.utils.book_append_sheet(workbook, errorWorksheet, `Case_${caseData.metadata.caseNumber}_Error`);
227
+ return;
228
+ }
229
+
230
+ // For successful cases, create detailed worksheets
231
+ const metadataRows = generateMetadataRows(caseData);
232
+
233
+ // Create case details with headers
234
+ const caseDetailsData = [
235
+ protectForensicData
236
+ ? [`CASE DATA - ${caseData.metadata.caseNumber} - PROTECTED`]
237
+ : [`Case ${caseData.metadata.caseNumber} - Detailed Export`],
238
+ protectForensicData ? ['WARNING: This worksheet is protected to maintain data integrity.'] : [''],
239
+ [''],
240
+ ...metadataRows.slice(2, -1), // Skip title and "File Details" header
241
+ [''],
242
+ ['File Details'],
243
+ CSV_HEADERS
244
+ ];
245
+
246
+ // Add file data if available
247
+ if (caseData.files && caseData.files.length > 0) {
248
+ const fileRows: any[][] = [];
249
+
250
+ caseData.files.forEach(fileEntry => {
251
+ const processedRows = processFileDataForTabular(fileEntry);
252
+ fileRows.push(...processedRows);
253
+ });
254
+
255
+ caseDetailsData.push(...fileRows);
256
+ } else {
257
+ caseDetailsData.push(['No detailed file data available for this case']);
258
+ }
259
+
260
+ const caseWorksheet = XLSX.utils.aoa_to_sheet(caseDetailsData);
261
+
262
+ // Protect worksheet if forensic protection is enabled
263
+ if (protectForensicData && exportPassword) {
264
+ protectExcelWorksheet(caseWorksheet, exportPassword);
265
+ }
266
+
267
+ // Clean sheet name for Excel compatibility
268
+ const sheetName = `Case_${caseData.metadata.caseNumber}`.replace(/[\\\/\?\*\[\]]/g, '_').substring(0, 31);
269
+ XLSX.utils.book_append_sheet(workbook, caseWorksheet, sheetName);
270
+ });
271
+
272
+ // Set workbook protection if forensic protection is enabled
273
+ if (protectForensicData && exportPassword) {
274
+ workbook.Props = {
275
+ Title: 'Striae Case Export - Protected',
276
+ Subject: 'Case Data Export',
277
+ Author: exportData.metadata.exportedBy || 'Striae',
278
+ Comments: `This workbook contains protected case data. Modification may compromise evidence integrity. Worksheets are password protected.`,
279
+ Company: 'Striae'
280
+ };
281
+ }
282
+
283
+ // Generate Excel file
284
+ const excelBuffer = XLSX.write(workbook, {
285
+ bookType: 'xlsx',
286
+ type: 'array',
287
+ bookSST: true, // Shared string table for better compression
288
+ cellStyles: true
289
+ });
290
+
291
+ // Create blob and download
292
+ const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
293
+ const url = window.URL.createObjectURL(blob);
294
+
295
+ const protectionSuffix = protectForensicData ? '-protected' : '';
296
+ const exportFileName = `striae-all-cases-detailed${protectionSuffix}-${formatDateForFilename(new Date())}.xlsx`;
297
+
298
+ const linkElement = document.createElement('a');
299
+ linkElement.href = url;
300
+ linkElement.download = exportFileName;
301
+ linkElement.click();
302
+
303
+ // Clean up
304
+ window.URL.revokeObjectURL(url);
305
+
306
+ const passwordInfo = protectForensicData && exportPassword ? ` (Password: ${exportPassword})` : '';
307
+
308
+ // Log successful export audit event
309
+ const endTime = Date.now();
310
+ await auditService.logCaseExport(
311
+ user,
312
+ 'all-cases',
313
+ exportFileName,
314
+ 'success',
315
+ [],
316
+ {
317
+ processingTimeMs: endTime - startTime,
318
+ fileSizeBytes: blob.size,
319
+ validationStepsCompleted: exportData.cases.length,
320
+ validationStepsFailed: exportData.cases.filter(c => c.summary?.exportError).length
321
+ },
322
+ 'xlsx',
323
+ protectForensicData
324
+ );
325
+
326
+ // End audit workflow
327
+ auditService.endWorkflow();
328
+
329
+ } catch (error) {
330
+ console.error('Excel export failed:', error);
331
+
332
+ // Log failed export audit event
333
+ const endTime = Date.now();
334
+ await auditService.logCaseExport(
335
+ user,
336
+ 'all-cases',
337
+ 'striae-all-cases-detailed.xlsx',
338
+ 'failure',
339
+ [error instanceof Error ? error.message : 'Unknown error'],
340
+ {
341
+ processingTimeMs: endTime - startTime,
342
+ fileSizeBytes: 0,
343
+ validationStepsCompleted: 0,
344
+ validationStepsFailed: 1
345
+ },
346
+ 'xlsx',
347
+ protectForensicData
348
+ );
349
+
350
+ // End audit workflow
351
+ auditService.endWorkflow();
352
+
353
+ throw new Error('Failed to export Excel file');
354
+ }
355
+ }
356
+
357
+ /**
358
+ * Download case data as JSON file with forensic protection options
359
+ */
360
+ export async function downloadCaseAsJSON(
361
+ user: User,
362
+ exportData: CaseExportData,
363
+ options: ExportOptions = { protectForensicData: true }
364
+ ): Promise<void> {
365
+ const startTime = Date.now();
366
+
367
+ try {
368
+ // Start audit workflow
369
+ const workflowId = auditService.startWorkflow(exportData.metadata.caseNumber);
370
+
371
+ const jsonContent = await generateJSONContent(exportData, options.includeUserInfo, options.protectForensicData);
372
+ const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(jsonContent);
373
+
374
+ const protectionSuffix = options.protectForensicData ? '-protected' : '';
375
+ const exportFileName = `striae-case-${exportData.metadata.caseNumber}-export${protectionSuffix}-${formatDateForFilename(new Date())}.json`;
376
+
377
+ const linkElement = document.createElement('a');
378
+ linkElement.setAttribute('href', dataUri);
379
+ linkElement.setAttribute('download', exportFileName);
380
+
381
+ if (options.protectForensicData) {
382
+ linkElement.setAttribute('data-forensic-protected', 'true');
383
+ }
384
+
385
+ linkElement.click();
386
+
387
+ // Log successful export audit event
388
+ const endTime = Date.now();
389
+ await auditService.logCaseExport(
390
+ user,
391
+ exportData.metadata.caseNumber,
392
+ exportFileName,
393
+ 'success',
394
+ [],
395
+ {
396
+ processingTimeMs: endTime - startTime,
397
+ fileSizeBytes: jsonContent.length,
398
+ validationStepsCompleted: exportData.files?.length || 0,
399
+ validationStepsFailed: 0
400
+ },
401
+ 'json',
402
+ options.protectForensicData || false
403
+ );
404
+
405
+ // End audit workflow
406
+ auditService.endWorkflow();
407
+
408
+ } catch (error) {
409
+ console.error('JSON export failed:', error);
410
+
411
+ // Log failed export audit event
412
+ const endTime = Date.now();
413
+ await auditService.logCaseExport(
414
+ user,
415
+ exportData.metadata.caseNumber,
416
+ `striae-case-${exportData.metadata.caseNumber}-export.json`,
417
+ 'failure',
418
+ [error instanceof Error ? error.message : 'Unknown error'],
419
+ {
420
+ processingTimeMs: endTime - startTime,
421
+ fileSizeBytes: 0,
422
+ validationStepsCompleted: 0,
423
+ validationStepsFailed: 1
424
+ },
425
+ 'json',
426
+ options.protectForensicData || false
427
+ );
428
+
429
+ // End audit workflow
430
+ auditService.endWorkflow();
431
+
432
+ throw new Error('Failed to download JSON export file');
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Download case data as comprehensive CSV file with forensic protection options
438
+ */
439
+ export async function downloadCaseAsCSV(
440
+ user: User,
441
+ exportData: CaseExportData,
442
+ options: ExportOptions = { protectForensicData: true }
443
+ ): Promise<void> {
444
+ const startTime = Date.now();
445
+
446
+ try {
447
+ // Start audit workflow
448
+ const workflowId = auditService.startWorkflow(exportData.metadata.caseNumber);
449
+
450
+ const csvContent = await generateCSVContent(exportData, options.protectForensicData);
451
+ const dataUri = 'data:text/csv;charset=utf-8,' + encodeURIComponent(csvContent);
452
+
453
+ const protectionSuffix = options.protectForensicData ? '-protected' : '';
454
+ const exportFileName = `striae-case-${exportData.metadata.caseNumber}-detailed${protectionSuffix}-${formatDateForFilename(new Date())}.csv`;
455
+
456
+ const linkElement = document.createElement('a');
457
+ linkElement.setAttribute('href', dataUri);
458
+ linkElement.setAttribute('download', exportFileName);
459
+
460
+ if (options.protectForensicData) {
461
+ linkElement.setAttribute('data-forensic-protected', 'true');
462
+ }
463
+
464
+ linkElement.click();
465
+
466
+ // Log successful export audit event
467
+ const endTime = Date.now();
468
+ await auditService.logCaseExport(
469
+ user,
470
+ exportData.metadata.caseNumber,
471
+ exportFileName,
472
+ 'success',
473
+ [],
474
+ {
475
+ processingTimeMs: endTime - startTime,
476
+ fileSizeBytes: csvContent.length,
477
+ validationStepsCompleted: exportData.files?.length || 0,
478
+ validationStepsFailed: 0
479
+ },
480
+ 'csv',
481
+ options.protectForensicData || false
482
+ );
483
+
484
+ // End audit workflow
485
+ auditService.endWorkflow();
486
+
487
+ } catch (error) {
488
+ console.error('CSV export failed:', error);
489
+
490
+ // Log failed export audit event
491
+ const endTime = Date.now();
492
+ await auditService.logCaseExport(
493
+ user,
494
+ exportData.metadata.caseNumber,
495
+ `striae-case-${exportData.metadata.caseNumber}-detailed.csv`,
496
+ 'failure',
497
+ [error instanceof Error ? error.message : 'Unknown error'],
498
+ {
499
+ processingTimeMs: endTime - startTime,
500
+ fileSizeBytes: 0,
501
+ validationStepsCompleted: 0,
502
+ validationStepsFailed: 1
503
+ },
504
+ 'csv',
505
+ options.protectForensicData || false
506
+ );
507
+
508
+ // End audit workflow
509
+ auditService.endWorkflow();
510
+
511
+ throw new Error('Failed to export CSV file');
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Download case data as ZIP file including images with forensic protection options
517
+ */
518
+ export async function downloadCaseAsZip(
519
+ user: User,
520
+ caseNumber: string,
521
+ format: ExportFormat,
522
+ onProgress?: (progress: number) => void,
523
+ options: ExportOptions = { protectForensicData: true }
524
+ ): Promise<void> {
525
+ const startTime = Date.now();
526
+ let manifestSignatureKeyId: string | undefined;
527
+ let manifestSigned = false;
528
+
529
+ try {
530
+ // Start audit workflow
531
+ const workflowId = auditService.startWorkflow(caseNumber);
532
+
533
+ onProgress?.(10);
534
+
535
+ // Get case data
536
+ const exportData = await exportCaseData(user, caseNumber);
537
+ onProgress?.(30);
538
+
539
+ // Create ZIP
540
+ const JSZip = (await import('jszip')).default;
541
+ const zip = new JSZip();
542
+
543
+ // Add data file with forensic protection if enabled
544
+ if (format === 'json') {
545
+ const jsonContent = generateJSONContent(exportData, options.includeUserInfo, options.protectForensicData);
546
+ zip.file(`${caseNumber}_data.json`, jsonContent);
547
+ } else {
548
+ const csvContent = generateCSVContent(exportData, options.protectForensicData);
549
+ zip.file(`${caseNumber}_data.csv`, csvContent);
550
+ }
551
+ onProgress?.(50);
552
+
553
+ // Add images and collect them for manifest generation
554
+ const imageFolder = zip.folder('images');
555
+ const imageFiles: { [filename: string]: Blob } = {};
556
+ if (imageFolder && exportData.files) {
557
+ for (let i = 0; i < exportData.files.length; i++) {
558
+ const file = exportData.files[i];
559
+ try {
560
+ const imageBlob = await fetchImageAsBlob(user, file.fileData, caseNumber);
561
+ if (imageBlob) {
562
+ // Generate export filename with embedded ID to prevent collisions
563
+ const exportFilename = generateExportFilename(file.fileData.originalFilename, file.fileData.id);
564
+ imageFolder.file(exportFilename, imageBlob);
565
+ imageFiles[exportFilename] = imageBlob;
566
+ }
567
+ } catch (error) {
568
+ console.warn(`Failed to fetch image ${file.fileData.originalFilename}:`, error);
569
+ }
570
+ onProgress?.(50 + (i / exportData.files.length) * 30);
571
+ }
572
+ }
573
+
574
+ // Add forensic metadata file if protection is enabled
575
+ if (options.protectForensicData) {
576
+ // CRITICAL: Get the content that will be used for hash calculation
577
+ // This MUST match exactly what gets saved in the actual data file
578
+ // So we use the same includeUserInfo setting for both
579
+ const contentForHash = format === 'json'
580
+ ? await generateJSONContent(exportData, options.includeUserInfo, false) // Raw content without warnings but same includeUserInfo
581
+ : await generateCSVContent(exportData, false); // Raw content without warnings
582
+
583
+ // Generate comprehensive forensic manifest with individual file hashes using secure SHA256
584
+ const forensicManifest = await generateForensicManifestSecure(contentForHash, imageFiles);
585
+
586
+ // Request server-side signature to prevent tamper-by-rehash attacks
587
+ const signingResult = await signForensicManifest(user, caseNumber, forensicManifest);
588
+ manifestSignatureKeyId = signingResult.signature.keyId;
589
+ manifestSigned = true;
590
+
591
+ const signedForensicManifest = {
592
+ ...forensicManifest,
593
+ manifestVersion: signingResult.manifestVersion,
594
+ signature: signingResult.signature
595
+ };
596
+
597
+ // Add dedicated forensic manifest file for validation
598
+ zip.file('FORENSIC_MANIFEST.json', JSON.stringify(signedForensicManifest, null, 2));
599
+
600
+ // Add read-only instruction file
601
+ const instructionContent = `EVIDENCE ARCHIVE - READ ONLY
602
+
603
+ This ZIP archive contains evidence data exported from Striae.
604
+
605
+ IMPORTANT WARNINGS:
606
+ - This archive is intended for READ-ONLY access
607
+ - Do not modify, rename, or delete any files in this archive
608
+ - Any modifications may compromise evidence integrity
609
+ - Maintain proper chain of custody procedures
610
+
611
+ Archive Contents:
612
+ - ${caseNumber}_data.${format}: Complete case data in ${format.toUpperCase()} format
613
+ - images/: Original image files with annotations
614
+ - FORENSIC_MANIFEST.json: File integrity validation manifest
615
+ - README.txt: General information about this export
616
+
617
+ Case Information:
618
+ - Case Number: ${exportData.metadata.caseNumber}
619
+ - Export Date: ${new Date().toISOString()}
620
+ - Exported By: ${exportData.metadata.exportedBy || 'Unknown'}
621
+ - Total Files: ${exportData.metadata.totalFiles}
622
+ - Total Annotations: ${(exportData.summary?.filesWithAnnotations || 0) + (exportData.summary?.totalBoxAnnotations || 0)}
623
+ - Total Confirmations: ${exportData.summary?.filesWithConfirmations || 0}
624
+ - Confirmations Requested: ${exportData.summary?.filesWithConfirmationsRequested || 0}
625
+
626
+ For questions about this export, contact your Striae system administrator.
627
+ `;
628
+
629
+ zip.file('READ_ONLY_INSTRUCTIONS.txt', instructionContent);
630
+
631
+ // Add README
632
+ const readme = generateZipReadme(exportData, options.protectForensicData);
633
+ zip.file('README.txt', readme);
634
+ onProgress?.(85);
635
+
636
+ // Generate ZIP blob
637
+ const zipBlob = await zip.generateAsync({
638
+ type: 'blob',
639
+ compression: 'DEFLATE',
640
+ compressionOptions: { level: 6 }
641
+ });
642
+ onProgress?.(95);
643
+
644
+ // Download
645
+ const url = URL.createObjectURL(zipBlob);
646
+ const protectionSuffix = options.protectForensicData ? '-protected' : '';
647
+ const exportFileName = `striae-case-${caseNumber}-export${protectionSuffix}-${formatDateForFilename(new Date())}.zip`;
648
+
649
+ const linkElement = document.createElement('a');
650
+ linkElement.href = url;
651
+ linkElement.setAttribute('download', exportFileName);
652
+
653
+ if (options.protectForensicData) {
654
+ linkElement.setAttribute('title', 'Evidence archive with forensic protection enabled');
655
+ }
656
+
657
+ linkElement.click();
658
+
659
+ URL.revokeObjectURL(url);
660
+ onProgress?.(100);
661
+
662
+ // Log successful export audit event (forensic protected case)
663
+ const endTime = Date.now();
664
+ await auditService.logCaseExport(
665
+ user,
666
+ caseNumber,
667
+ exportFileName,
668
+ 'success',
669
+ [],
670
+ {
671
+ processingTimeMs: endTime - startTime,
672
+ fileSizeBytes: zipBlob.size,
673
+ validationStepsCompleted: exportData.files?.length || 0,
674
+ validationStepsFailed: 0
675
+ },
676
+ 'zip',
677
+ options.protectForensicData || false,
678
+ {
679
+ present: true,
680
+ valid: true,
681
+ keyId: manifestSignatureKeyId
682
+ }
683
+ );
684
+
685
+ // End audit workflow
686
+ auditService.endWorkflow();
687
+
688
+ return; // Exit early as we've handled the forensic case
689
+ }
690
+
691
+ // Add README (standard or enhanced for forensic)
692
+ const readme = generateZipReadme(exportData, options.protectForensicData);
693
+ zip.file('README.txt', readme);
694
+ onProgress?.(85);
695
+
696
+ // Generate ZIP blob for non-forensic case
697
+ const zipBlob = await zip.generateAsync({
698
+ type: 'blob',
699
+ compression: 'DEFLATE',
700
+ compressionOptions: { level: 6 }
701
+ });
702
+ onProgress?.(95); // Download
703
+ const url = URL.createObjectURL(zipBlob);
704
+ const protectionSuffix = options.protectForensicData ? '-protected' : '';
705
+ const exportFileName = `striae-case-${caseNumber}-export${protectionSuffix}-${formatDateForFilename(new Date())}.zip`;
706
+
707
+ const linkElement = document.createElement('a');
708
+ linkElement.href = url;
709
+ linkElement.setAttribute('download', exportFileName);
710
+
711
+ if (options.protectForensicData) {
712
+ linkElement.setAttribute('data-forensic-protected', 'true');
713
+ }
714
+
715
+ linkElement.click();
716
+
717
+ URL.revokeObjectURL(url);
718
+ onProgress?.(100);
719
+
720
+ // Log successful export audit event (standard case)
721
+ const endTime = Date.now();
722
+ await auditService.logCaseExport(
723
+ user,
724
+ caseNumber,
725
+ exportFileName,
726
+ 'success',
727
+ [],
728
+ {
729
+ processingTimeMs: endTime - startTime,
730
+ fileSizeBytes: zipBlob.size,
731
+ validationStepsCompleted: exportData.files?.length || 0,
732
+ validationStepsFailed: 0
733
+ },
734
+ 'zip',
735
+ options.protectForensicData || false
736
+ );
737
+
738
+ // End audit workflow
739
+ auditService.endWorkflow();
740
+
741
+ } catch (error) {
742
+ console.error('ZIP export failed:', error);
743
+
744
+ // Log failed export audit event
745
+ const endTime = Date.now();
746
+ await auditService.logCaseExport(
747
+ user,
748
+ caseNumber,
749
+ `striae-case-${caseNumber}-export.zip`,
750
+ 'failure',
751
+ [error instanceof Error ? error.message : 'Unknown error'],
752
+ {
753
+ processingTimeMs: endTime - startTime,
754
+ fileSizeBytes: 0,
755
+ validationStepsCompleted: 0,
756
+ validationStepsFailed: 1
757
+ },
758
+ 'zip',
759
+ options.protectForensicData || false,
760
+ options.protectForensicData
761
+ ? {
762
+ present: manifestSigned,
763
+ valid: manifestSigned,
764
+ keyId: manifestSignatureKeyId
765
+ }
766
+ : undefined
767
+ );
768
+
769
+ // End audit workflow
770
+ auditService.endWorkflow();
771
+
772
+ throw new Error('Failed to export ZIP file');
773
+ }
774
+ }
775
+
776
+ /**
777
+ * Helper function to fetch image as blob
778
+ */
779
+ async function fetchImageAsBlob(user: User, fileData: FileData, caseNumber: string): Promise<Blob | null> {
780
+ try {
781
+ const imageUrl = await getImageUrl(user, fileData, caseNumber, 'Export Package');
782
+ if (!imageUrl) return null;
783
+
784
+ const response = await fetch(imageUrl);
785
+ if (!response.ok) return null;
786
+
787
+ return await response.blob();
788
+ } catch (error) {
789
+ console.error('Failed to fetch image blob:', error);
790
+ return null;
791
+ }
792
+ }
793
+
794
+ /**
795
+ * Generate README content for ZIP export with optional forensic protection
796
+ */
797
+ function generateZipReadme(exportData: CaseExportData, protectForensicData: boolean = true): string {
798
+ const totalFiles = exportData.files?.length || 0;
799
+ const filesWithAnnotations = exportData.summary?.filesWithAnnotations || 0;
800
+ const totalBoxAnnotations = exportData.summary?.totalBoxAnnotations || 0;
801
+ const totalAnnotations = filesWithAnnotations + totalBoxAnnotations;
802
+ const filesWithConfirmations = exportData.summary?.filesWithConfirmations || 0;
803
+ const filesWithConfirmationsRequested = exportData.summary?.filesWithConfirmationsRequested || 0;
804
+
805
+ const baseContent = `Striae Case Export
806
+ ==================
807
+
808
+ Case Number: ${exportData.metadata.caseNumber}
809
+ Case Created Date: ${exportData.metadata.caseCreatedDate}
810
+ Export Date: ${exportData.metadata.exportDate}
811
+ Exported By (Email): ${exportData.metadata.exportedBy || 'N/A'}
812
+ Exported By (UID): ${exportData.metadata.exportedByUid || 'N/A'}
813
+ Exported By (Name): ${exportData.metadata.exportedByName || 'N/A'}
814
+ Exported By (Company): ${exportData.metadata.exportedByCompany || 'N/A'}
815
+ Striae Export Schema Version: ${exportData.metadata.striaeExportSchemaVersion}
816
+
817
+ Summary:
818
+ - Total Files: ${totalFiles}
819
+ - Files with Annotations: ${filesWithAnnotations}
820
+ - Files without Annotations: ${totalFiles - filesWithAnnotations}
821
+ - Total Box Annotations: ${totalBoxAnnotations}
822
+ - Total Annotations: ${totalAnnotations}
823
+ - Files with Confirmations: ${filesWithConfirmations}
824
+ - Files with Confirmations Requested: ${filesWithConfirmationsRequested}
825
+ - Earliest Annotation Date: ${exportData.summary?.earliestAnnotationDate || 'N/A'}
826
+ - Latest Annotation Date: ${exportData.summary?.latestAnnotationDate || 'N/A'}
827
+
828
+ Contents:
829
+ - ${exportData.metadata.caseNumber}_data.json/.csv: Case data and annotations
830
+ - images/: Original uploaded images
831
+ - README.txt: This file`;
832
+
833
+ const forensicAddition = `
834
+ - FORENSIC_MANIFEST.json: File integrity validation manifest
835
+ - READ_ONLY_INSTRUCTIONS.txt: Important evidence handling guidelines
836
+
837
+ EVIDENCE NOTICE:
838
+ ================
839
+ This export contains evidence data. Any modification may compromise
840
+ evidence integrity and chain of custody. Handle according to your organization's
841
+ forensic procedures and maintain proper documentation.`;
842
+
843
+ const footer = `
844
+
845
+ Generated by Striae - A Firearms Examiner's Comparison Companion
846
+ https://www.striae.org`;
847
+
848
+ return protectForensicData ? baseContent + forensicAddition + footer : baseContent + footer;
849
+ }
850
+
851
+ /**
852
+ * Generate JSON content for case export with forensic protection options
853
+ */
854
+ async function generateJSONContent(
855
+ exportData: CaseExportData,
856
+ includeUserInfo: boolean = true,
857
+ protectForensicData: boolean = true
858
+ ): Promise<string> {
859
+ let jsonData = { ...exportData };
860
+
861
+ // Remove sensitive user info if not included
862
+ if (!includeUserInfo) {
863
+ if (jsonData.metadata.exportedBy) {
864
+ jsonData.metadata.exportedBy = '[User Info Excluded]';
865
+ }
866
+ if (jsonData.metadata.exportedByUid) {
867
+ jsonData.metadata.exportedByUid = '[User Info Excluded]';
868
+ }
869
+ if (jsonData.metadata.exportedByName) {
870
+ jsonData.metadata.exportedByName = '[User Info Excluded]';
871
+ }
872
+ if (jsonData.metadata.exportedByCompany) {
873
+ jsonData.metadata.exportedByCompany = '[User Info Excluded]';
874
+ }
875
+ }
876
+
877
+ const jsonString = JSON.stringify(jsonData, null, 2);
878
+
879
+ // Calculate hash for integrity verification
880
+ const hash = await calculateSHA256Secure(jsonString);
881
+
882
+ // Add hash to metadata
883
+ const finalJsonData = {
884
+ ...jsonData,
885
+ metadata: {
886
+ ...jsonData.metadata,
887
+ hash: hash.toUpperCase(),
888
+ integrityNote: 'Verify by recalculating SHA256 of this entire JSON content'
889
+ }
890
+ };
891
+
892
+ const finalJsonString = JSON.stringify(finalJsonData, null, 2);
893
+
894
+ // Add forensic protection warning if enabled
895
+ if (protectForensicData) {
896
+ return addForensicDataWarning(finalJsonString, 'json');
897
+ }
898
+
899
+ return finalJsonString;
900
+ }