@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,270 @@
1
+ import { User } from 'firebase/auth';
2
+ import paths from '~/config/config.json';
3
+ import {
4
+ getDataApiKey,
5
+ getUserApiKey
6
+ } from '~/utils/auth';
7
+ import {
8
+ getUserReadOnlyCases,
9
+ updateUserData,
10
+ validateUserSession
11
+ } from '~/utils/permissions';
12
+ import {
13
+ CaseExportData,
14
+ ExtendedUserData,
15
+ FileData,
16
+ CaseData,
17
+ ReadOnlyCaseMetadata
18
+ } from '~/types';
19
+ import { deleteFile } from '../image-manage';
20
+ import { SignedForensicManifest } from '~/utils/SHA256';
21
+
22
+ const USER_WORKER_URL = paths.user_worker_url;
23
+ const DATA_WORKER_URL = paths.data_worker_url;
24
+
25
+ /**
26
+ * Check if user already has a read-only case with the same number
27
+ */
28
+ export async function checkReadOnlyCaseExists(
29
+ user: User,
30
+ caseNumber: string
31
+ ): Promise<ReadOnlyCaseMetadata | null> {
32
+ try {
33
+ // Use centralized function to get read-only cases
34
+ const readOnlyCases = await getUserReadOnlyCases(user);
35
+ return readOnlyCases.find(c => c.caseNumber === caseNumber) || null;
36
+
37
+ } catch (error) {
38
+ console.error('Error checking read-only case existence:', error);
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Create read-only case entry in user database
45
+ * Note: Only one read-only case is allowed at a time. This function will clear any existing
46
+ * read-only cases before adding the new one to prevent accumulation of multiple read-only cases.
47
+ */
48
+ export async function addReadOnlyCaseToUser(
49
+ user: User,
50
+ caseMetadata: ReadOnlyCaseMetadata
51
+ ): Promise<void> {
52
+ try {
53
+ // Validate user session
54
+ const sessionValidation = await validateUserSession(user);
55
+ if (!sessionValidation.valid) {
56
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
57
+ }
58
+
59
+ // Get current read-only cases
60
+ const currentReadOnlyCases = await getUserReadOnlyCases(user);
61
+
62
+ // IMPORTANT: Only allow one read-only case at a time
63
+ // Clear any existing read-only cases before adding the new one
64
+ if (currentReadOnlyCases.length > 0) {
65
+ const existingCaseNumbers = currentReadOnlyCases.map(c => c.caseNumber).join(', ');
66
+ console.log(`Clearing ${currentReadOnlyCases.length} existing read-only case(s) (${existingCaseNumbers}) before importing new case: ${caseMetadata.caseNumber}`);
67
+ }
68
+
69
+ // Update user data with the new read-only case (replacing any existing ones)
70
+ await updateUserData(user, {
71
+ readOnlyCases: [caseMetadata] // Only the new case
72
+ });
73
+
74
+ console.log(`Added new read-only case to user profile: ${caseMetadata.caseNumber}`);
75
+
76
+ } catch (error) {
77
+ console.error('Error adding read-only case to user:', error);
78
+ throw error;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Store case data in R2 storage
84
+ */
85
+ export async function storeCaseDataInR2(
86
+ user: User,
87
+ caseNumber: string,
88
+ caseData: CaseExportData,
89
+ importedFiles: FileData[],
90
+ originalImageIdMapping?: Map<string, string>,
91
+ forensicManifest?: SignedForensicManifest
92
+ ): Promise<void> {
93
+ try {
94
+ const apiKey = await getDataApiKey();
95
+
96
+ // Convert the mapping to a plain object for JSON serialization
97
+ const originalImageIds = originalImageIdMapping ?
98
+ Object.fromEntries(originalImageIdMapping) : undefined;
99
+
100
+ const forensicManifestMetadata = forensicManifest ? {
101
+ manifestVersion: forensicManifest.manifestVersion,
102
+ createdAt: forensicManifest.createdAt,
103
+ dataHash: forensicManifest.dataHash,
104
+ manifestHash: forensicManifest.manifestHash,
105
+ signature: forensicManifest.signature
106
+ } : undefined;
107
+
108
+ // Create the case data structure that matches normal cases
109
+ const r2CaseData = {
110
+ createdAt: new Date().toISOString(),
111
+ caseNumber: caseNumber,
112
+ files: importedFiles,
113
+ // Add read-only metadata
114
+ isReadOnly: true,
115
+ importedAt: new Date().toISOString(),
116
+ // Add original image ID mapping for confirmation linking
117
+ originalImageIds: originalImageIds,
118
+ // Add forensic manifest timestamp if available for confirmation exports
119
+ ...(forensicManifest?.createdAt && { forensicManifestCreatedAt: forensicManifest.createdAt }),
120
+ // Store full forensic manifest metadata for chain-of-custody validation
121
+ ...(forensicManifestMetadata && { forensicManifest: forensicManifestMetadata })
122
+ };
123
+
124
+ // Store in R2
125
+ const response = await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`, {
126
+ method: 'PUT',
127
+ headers: {
128
+ 'Content-Type': 'application/json',
129
+ 'X-Custom-Auth-Key': apiKey
130
+ },
131
+ body: JSON.stringify(r2CaseData)
132
+ });
133
+
134
+ if (!response.ok) {
135
+ throw new Error(`Failed to store case data: ${response.status}`);
136
+ }
137
+
138
+ } catch (error) {
139
+ console.error('Error storing case data in R2:', error);
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * List all read-only cases for a user
146
+ */
147
+ export async function listReadOnlyCases(user: User): Promise<ReadOnlyCaseMetadata[]> {
148
+ try {
149
+ const apiKey = await getUserApiKey();
150
+
151
+ const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
152
+ method: 'GET',
153
+ headers: {
154
+ 'Content-Type': 'application/json',
155
+ 'X-Custom-Auth-Key': apiKey
156
+ }
157
+ });
158
+
159
+ if (!response.ok) {
160
+ console.error('Failed to fetch user data:', response.status);
161
+ return [];
162
+ }
163
+
164
+ const userData: ExtendedUserData = await response.json();
165
+
166
+ return userData.readOnlyCases || [];
167
+
168
+ } catch (error) {
169
+ console.error('Error listing read-only cases:', error);
170
+ return [];
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Remove a read-only case (does not delete the actual case data, just removes from user's read-only list)
176
+ */
177
+ export async function removeReadOnlyCase(user: User, caseNumber: string): Promise<boolean> {
178
+ try {
179
+ const apiKey = await getUserApiKey();
180
+
181
+ // Get current user data
182
+ const response = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
183
+ method: 'GET',
184
+ headers: {
185
+ 'Content-Type': 'application/json',
186
+ 'X-Custom-Auth-Key': apiKey
187
+ }
188
+ });
189
+
190
+ if (!response.ok) {
191
+ throw new Error(`Failed to fetch user data: ${response.status}`);
192
+ }
193
+
194
+ const userData: ExtendedUserData = await response.json();
195
+
196
+ if (!userData.readOnlyCases) {
197
+ return false; // Nothing to remove
198
+ }
199
+
200
+ // Remove the case from the list
201
+ const initialLength = userData.readOnlyCases.length;
202
+ userData.readOnlyCases = userData.readOnlyCases.filter(c => c.caseNumber !== caseNumber);
203
+
204
+ if (userData.readOnlyCases.length === initialLength) {
205
+ return false; // Case wasn't found
206
+ }
207
+
208
+ // Update user data
209
+ const updateResponse = await fetch(`${USER_WORKER_URL}/${encodeURIComponent(user.uid)}`, {
210
+ method: 'PUT',
211
+ headers: {
212
+ 'Content-Type': 'application/json',
213
+ 'X-Custom-Auth-Key': apiKey
214
+ },
215
+ body: JSON.stringify(userData)
216
+ });
217
+
218
+ if (!updateResponse.ok) {
219
+ throw new Error(`Failed to update user data: ${updateResponse.status}`);
220
+ }
221
+
222
+ return true;
223
+
224
+ } catch (error) {
225
+ console.error('Error removing read-only case:', error);
226
+ return false;
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Completely delete a read-only case including all associated data (R2, Images, user references)
232
+ */
233
+ export async function deleteReadOnlyCase(user: User, caseNumber: string): Promise<boolean> {
234
+ try {
235
+ const dataApiKey = await getDataApiKey();
236
+
237
+ // Get case data first to get file IDs for deletion
238
+ const caseResponse = await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`, {
239
+ headers: { 'X-Custom-Auth-Key': dataApiKey }
240
+ });
241
+
242
+ if (caseResponse.ok) {
243
+ const caseData = await caseResponse.json() as CaseData;
244
+
245
+ // Delete all files using data worker
246
+ if (caseData.files && caseData.files.length > 0) {
247
+ await Promise.all(
248
+ caseData.files.map((file: FileData) =>
249
+ deleteFile(user, caseNumber, file.id, 'Read-only case clearing - API operation')
250
+ )
251
+ );
252
+ }
253
+
254
+ // Delete case file using data worker
255
+ await fetch(`${DATA_WORKER_URL}/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`, {
256
+ method: 'DELETE',
257
+ headers: { 'X-Custom-Auth-Key': dataApiKey }
258
+ });
259
+ }
260
+
261
+ // Remove from user's read-only case list (separate from regular cases)
262
+ await removeReadOnlyCase(user, caseNumber);
263
+
264
+ return true;
265
+
266
+ } catch (error) {
267
+ console.error('Error deleting read-only case:', error);
268
+ return false;
269
+ }
270
+ }
@@ -0,0 +1,189 @@
1
+ import { User } from 'firebase/auth';
2
+ import paths from '~/config/config.json';
3
+ import { getUserApiKey } from '~/utils/auth';
4
+ import { CaseExportData, ConfirmationImportData } from '~/types';
5
+ import { calculateSHA256Secure, ManifestSignatureVerificationResult } from '~/utils/SHA256';
6
+ import { verifyConfirmationSignature } from '~/utils/confirmation-signature';
7
+
8
+ const USER_WORKER_URL = paths.user_worker_url;
9
+
10
+ /**
11
+ * Remove forensic warning from content for hash validation (supports both JSON and CSV formats)
12
+ * This function ensures exact match with the content used during export hash generation
13
+ */
14
+ export function removeForensicWarning(content: string): string {
15
+ // Handle JSON forensic warnings (block comment format)
16
+ // /* CASE DATA WARNING
17
+ // * This file contains evidence data for forensic examination.
18
+ // * Any modification may compromise the integrity of the evidence.
19
+ // * Handle according to your organization's chain of custody procedures.
20
+ // *
21
+ // * File generated: YYYY-MM-DDTHH:mm:ss.sssZ
22
+ // */
23
+ const jsonForensicWarningRegex = /^\/\*\s*CASE\s+DATA\s+WARNING[\s\S]*?\*\/\s*\r?\n*/;
24
+
25
+ // Handle CSV forensic warnings (quoted string format at the beginning of file)
26
+ // CRITICAL: The CSV forensic warning is ONLY the first quoted line, followed by two newlines
27
+ // Format: "CASE DATA WARNING: This file contains evidence data for forensic examination. Any modification may compromise the integrity of the evidence. Handle according to your organization's chain of custody procedures."\n\n
28
+ //
29
+ // After removal, what remains should be the csvWithHash content:
30
+ // # Striae Case Export - Generated: ...
31
+ // # Case: ...
32
+ // # Total Files: ...
33
+ // # SHA256 Hash: ...
34
+ // # Verification: ...
35
+ //
36
+ // [actual CSV data]
37
+ // More robust regex to handle various line endings and exact format from generation
38
+ const csvForensicWarningRegex = /^"CASE DATA WARNING: This file contains evidence data for forensic examination\. Any modification may compromise the integrity of the evidence\. Handle according to your organization's chain of custody procedures\."(?:\r?\n){2}/;
39
+
40
+ let cleaned = content;
41
+
42
+ // Try JSON format first
43
+ if (jsonForensicWarningRegex.test(content)) {
44
+ cleaned = content.replace(jsonForensicWarningRegex, '');
45
+ }
46
+ // Try CSV format with exact pattern match
47
+ else if (csvForensicWarningRegex.test(content)) {
48
+ cleaned = content.replace(csvForensicWarningRegex, '');
49
+ }
50
+ // Fallback: try broader CSV pattern in case of slight format differences
51
+ else if (content.startsWith('"CASE DATA WARNING:')) {
52
+ // Find the end of the first quoted string followed by newlines
53
+ const match = content.match(/^"[^"]*"(?:\r?\n)+/);
54
+ if (match) {
55
+ cleaned = content.substring(match[0].length);
56
+ }
57
+ }
58
+
59
+ // Additional cleanup: remove any leading whitespace that might remain
60
+ // This ensures we match exactly what the generation functions produce with protectForensicData: false
61
+ cleaned = cleaned.replace(/^\s+/, '');
62
+
63
+ return cleaned;
64
+ }
65
+
66
+ /**
67
+ * Validate that a user exists in the database by UID and is not the current user
68
+ */
69
+ export async function validateExporterUid(exporterUid: string, currentUser: User): Promise<{ exists: boolean; isSelf: boolean }> {
70
+ try {
71
+ const apiKey = await getUserApiKey();
72
+ const response = await fetch(`${USER_WORKER_URL}/${exporterUid}`, {
73
+ method: 'GET',
74
+ headers: {
75
+ 'X-Custom-Auth-Key': apiKey
76
+ }
77
+ });
78
+
79
+ const exists = response.status === 200;
80
+ const isSelf = exporterUid === currentUser.uid;
81
+
82
+ return { exists, isSelf };
83
+ } catch (error) {
84
+ console.error('Error validating exporter UID:', error);
85
+ return { exists: false, isSelf: false };
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Check if file is a confirmation data import
91
+ */
92
+ export function isConfirmationDataFile(filename: string): boolean {
93
+ return filename.startsWith('confirmation-data') && filename.endsWith('.json');
94
+ }
95
+
96
+ /**
97
+ * Validate confirmation data file hash
98
+ */
99
+ export async function validateConfirmationHash(jsonContent: string, expectedHash: string): Promise<boolean> {
100
+ try {
101
+ // Validate input parameters
102
+ if (!expectedHash || typeof expectedHash !== 'string') {
103
+ console.error('validateConfirmationHash: expectedHash is invalid:', expectedHash);
104
+ return false;
105
+ }
106
+
107
+ // Create data without hash for validation
108
+ const data = JSON.parse(jsonContent);
109
+ const dataWithoutHash = {
110
+ ...data,
111
+ metadata: {
112
+ ...data.metadata,
113
+ hash: undefined
114
+ }
115
+ };
116
+ delete dataWithoutHash.metadata.hash;
117
+ delete dataWithoutHash.metadata.signature;
118
+ delete dataWithoutHash.metadata.signatureVersion;
119
+
120
+ const contentForHash = JSON.stringify(dataWithoutHash, null, 2);
121
+ const actualHash = await calculateSHA256Secure(contentForHash);
122
+
123
+ if (!actualHash) {
124
+ console.error('validateConfirmationHash: failed to calculate hash');
125
+ return false;
126
+ }
127
+
128
+ return actualHash.toUpperCase() === expectedHash.toUpperCase();
129
+ } catch (error) {
130
+ console.error('validateConfirmationHash: validation failed:', error);
131
+ return false;
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Validate imported case data integrity (optional verification)
137
+ */
138
+ export function validateCaseIntegrity(
139
+ caseData: CaseExportData,
140
+ imageFiles: { [filename: string]: Blob }
141
+ ): { isValid: boolean; issues: string[] } {
142
+ const issues: string[] = [];
143
+
144
+ // Check if all referenced images exist
145
+ for (const fileEntry of caseData.files) {
146
+ const filename = fileEntry.fileData.originalFilename;
147
+ if (!imageFiles[filename]) {
148
+ issues.push(`Missing image file: ${filename}`);
149
+ }
150
+ }
151
+
152
+ // Check if there are extra images not referenced in case data
153
+ const referencedFiles = new Set(caseData.files.map(f => f.fileData.originalFilename));
154
+ for (const filename of Object.keys(imageFiles)) {
155
+ if (!referencedFiles.has(filename)) {
156
+ issues.push(`Unreferenced image file: ${filename}`);
157
+ }
158
+ }
159
+
160
+ // Validate metadata completeness
161
+ if (!caseData.metadata.caseNumber) {
162
+ issues.push('Missing case number in metadata');
163
+ }
164
+
165
+ if (!caseData.metadata.exportDate) {
166
+ issues.push('Missing export date in metadata');
167
+ }
168
+
169
+ // Validate annotation data
170
+ for (const fileEntry of caseData.files) {
171
+ if (fileEntry.hasAnnotations && !fileEntry.annotations) {
172
+ issues.push(`File ${fileEntry.fileData.originalFilename} marked as having annotations but no annotation data found`);
173
+ }
174
+ }
175
+
176
+ return {
177
+ isValid: issues.length === 0,
178
+ issues
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Validate confirmation data file signature.
184
+ */
185
+ export async function validateConfirmationSignatureFile(
186
+ confirmationData: Partial<ConfirmationImportData>
187
+ ): Promise<ManifestSignatureVerificationResult> {
188
+ return verifyConfirmationSignature(confirmationData);
189
+ }