@striae-org/striae 4.1.0 → 4.2.1

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 (124) hide show
  1. package/.env.example +8 -0
  2. package/LICENSE +1 -1
  3. package/app/components/actions/case-export/core-export.ts +14 -8
  4. package/app/components/actions/case-export/data-processing.ts +1 -0
  5. package/app/components/actions/case-export/download-handlers.ts +7 -0
  6. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  7. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  8. package/app/components/actions/case-import/orchestrator.ts +78 -32
  9. package/app/components/actions/case-import/storage-operations.ts +97 -8
  10. package/app/components/actions/case-import/zip-processing.ts +159 -86
  11. package/app/components/actions/case-manage.ts +463 -8
  12. package/app/components/actions/confirm-export.ts +9 -2
  13. package/app/components/actions/image-manage.ts +77 -44
  14. package/app/components/audit/user-audit-viewer.tsx +19 -8
  15. package/app/components/audit/user-audit.module.css +21 -0
  16. package/app/components/audit/viewer/audit-entries-list.tsx +12 -2
  17. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  18. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  19. package/app/components/audit/viewer/use-audit-viewer-data.ts +24 -1
  20. package/app/components/audit/viewer/use-audit-viewer-export.ts +1 -1
  21. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  22. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  23. package/app/components/canvas/canvas.module.css +64 -54
  24. package/app/components/canvas/canvas.tsx +14 -16
  25. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  26. package/app/components/canvas/confirmation/confirmation.tsx +12 -14
  27. package/app/components/colors/colors.module.css +4 -3
  28. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  29. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  30. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  31. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  32. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  33. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  34. package/app/components/navbar/navbar.module.css +447 -0
  35. package/app/components/navbar/navbar.tsx +402 -0
  36. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  37. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  38. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  39. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  40. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  41. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  42. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  43. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  44. package/app/components/sidebar/cases/case-sidebar.tsx +68 -588
  45. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  46. package/app/components/sidebar/cases/cases-modal.tsx +82 -43
  47. package/app/components/sidebar/cases/cases.module.css +82 -21
  48. package/app/components/sidebar/files/files-modal.module.css +1 -0
  49. package/app/components/sidebar/files/files-modal.tsx +49 -52
  50. package/app/components/sidebar/notes/addl-notes-modal.tsx +82 -0
  51. package/app/components/sidebar/notes/{notes-sidebar.tsx → notes-editor-form.tsx} +187 -138
  52. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  53. package/app/components/sidebar/notes/notes-editor-modal.tsx +64 -0
  54. package/app/components/sidebar/notes/notes.module.css +170 -1
  55. package/app/components/sidebar/sidebar-container.tsx +16 -28
  56. package/app/components/sidebar/sidebar.module.css +5 -69
  57. package/app/components/sidebar/sidebar.tsx +27 -125
  58. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  59. package/app/components/user/inactivity-warning.module.css +1 -0
  60. package/app/components/user/inactivity-warning.tsx +15 -2
  61. package/app/components/user/manage-profile.tsx +23 -10
  62. package/app/{tailwind.css → global.css} +1 -3
  63. package/app/hooks/useOverlayDismiss.ts +54 -4
  64. package/app/root.tsx +1 -1
  65. package/app/routes/auth/login.tsx +785 -774
  66. package/app/routes/striae/striae.module.css +10 -3
  67. package/app/routes/striae/striae.tsx +475 -30
  68. package/app/services/audit/audit.service.ts +173 -27
  69. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  70. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  71. package/app/services/audit/builders/index.ts +1 -0
  72. package/app/types/audit.ts +4 -1
  73. package/app/types/case.ts +29 -0
  74. package/app/types/import.ts +3 -0
  75. package/app/utils/data/confirmation-summary/summary-core.ts +279 -0
  76. package/app/utils/data/data-operations.ts +17 -861
  77. package/app/utils/data/index.ts +11 -1
  78. package/app/utils/data/operations/batch-operations.ts +113 -0
  79. package/app/utils/data/operations/case-operations.ts +168 -0
  80. package/app/utils/data/operations/confirmation-summary-operations.ts +301 -0
  81. package/app/utils/data/operations/file-annotation-operations.ts +196 -0
  82. package/app/utils/data/operations/index.ts +7 -0
  83. package/app/utils/data/operations/signing-operations.ts +225 -0
  84. package/app/utils/data/operations/types.ts +42 -0
  85. package/app/utils/data/operations/validation-operations.ts +48 -0
  86. package/app/utils/data/permissions.ts +16 -1
  87. package/app/utils/forensics/audit-export-signature.ts +5 -1
  88. package/app/utils/forensics/confirmation-signature.ts +3 -0
  89. package/app/utils/forensics/export-verification.ts +426 -22
  90. package/functions/api/_shared/firebase-auth.ts +2 -7
  91. package/functions/api/image/[[path]].ts +20 -23
  92. package/functions/api/pdf/[[path]].ts +27 -8
  93. package/package.json +7 -12
  94. package/scripts/deploy-primershear-emails.sh +2 -1
  95. package/worker-configuration.d.ts +3 -3
  96. package/workers/audit-worker/package.json +1 -1
  97. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  98. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  99. package/workers/data-worker/package.json +1 -1
  100. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  101. package/workers/data-worker/wrangler.jsonc.example +1 -1
  102. package/workers/image-worker/package.json +1 -1
  103. package/workers/image-worker/src/image-worker.example.ts +16 -5
  104. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  105. package/workers/image-worker/wrangler.jsonc.example +1 -1
  106. package/workers/keys-worker/package.json +1 -1
  107. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  108. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  109. package/workers/pdf-worker/package.json +1 -1
  110. package/workers/pdf-worker/src/formats/format-striae.ts +9 -14
  111. package/workers/pdf-worker/src/pdf-worker.example.ts +37 -58
  112. package/workers/pdf-worker/src/report-types.ts +3 -3
  113. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  114. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  115. package/workers/user-worker/package.json +1 -1
  116. package/workers/user-worker/src/user-worker.example.ts +17 -0
  117. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  118. package/workers/user-worker/wrangler.jsonc.example +1 -1
  119. package/wrangler.toml.example +1 -1
  120. package/NOTICE +0 -13
  121. package/app/components/sidebar/notes/notes-modal.tsx +0 -53
  122. package/postcss.config.js +0 -6
  123. package/public/.well-known/keybase.txt +0 -56
  124. package/tailwind.config.ts +0 -22
@@ -1,862 +1,18 @@
1
1
  /**
2
- * Centralized data worker operations for case and file management
3
- * Provides consistent API key management, error handling, and validation
4
- * for all interactions with the data worker microservice
5
- */
6
-
7
- import type { User } from 'firebase/auth';
8
- import { type CaseData, type AnnotationData, type ConfirmationImportData } from '~/types';
9
- import { fetchDataApi } from '../api';
10
- import { validateUserSession, canAccessCase, canModifyCase } from './permissions';
11
- import {
12
- type ForensicManifestData,
13
- type ForensicManifestSignature,
14
- FORENSIC_MANIFEST_VERSION
15
- } from '../forensics/SHA256';
16
- import { CONFIRMATION_SIGNATURE_VERSION } from '../forensics/confirmation-signature';
17
- import {
18
- AUDIT_EXPORT_SIGNATURE_VERSION,
19
- type AuditExportSigningPayload,
20
- isValidAuditExportSigningPayload
21
- } from '../forensics/audit-export-signature';
22
-
23
- // ============================================================================
24
- // INTERFACES AND TYPES
25
- // ============================================================================
26
-
27
- export interface DataAccessResult {
28
- allowed: boolean;
29
- reason?: string;
30
- }
31
-
32
- export interface FileUpdate {
33
- fileId: string;
34
- annotations: AnnotationData;
35
- }
36
-
37
- export interface BatchUpdateResult {
38
- successful: string[];
39
- failed: { fileId: string; error: string }[];
40
- }
41
-
42
- export interface DataOperationOptions {
43
- includeTimestamp?: boolean;
44
- retryCount?: number;
45
- skipValidation?: boolean;
46
- }
47
-
48
- export interface ManifestSigningResponse {
49
- manifestVersion: string;
50
- signature: ForensicManifestSignature;
51
- }
52
-
53
- export interface ConfirmationSigningResponse {
54
- signatureVersion: string;
55
- signature: ForensicManifestSignature;
56
- }
57
-
58
- export interface AuditExportSigningResponse {
59
- signatureVersion: string;
60
- signature: ForensicManifestSignature;
61
- }
62
-
63
- // Higher-order function type for data operations
64
- export type DataOperation<T> = (user: User, ...args: unknown[]) => Promise<T>;
65
-
66
- // ============================================================================
67
- // CORE CASE DATA OPERATIONS
68
- // ============================================================================
69
-
70
- /**
71
- * Get case data from R2 storage with validation and error handling
72
- * @param user - Authenticated user
73
- * @param caseNumber - Case identifier
74
- * @param options - Optional configuration for the operation
75
- */
76
- export const getCaseData = async (
77
- user: User,
78
- caseNumber: string,
79
- options: DataOperationOptions = {}
80
- ): Promise<CaseData | null> => {
81
- try {
82
- // Validate user session
83
- const sessionValidation = await validateUserSession(user);
84
- if (!sessionValidation.valid) {
85
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
86
- }
87
-
88
- // Validate case access unless explicitly skipped.
89
- if (options.skipValidation !== true) {
90
- const accessCheck = await canAccessCase(user, caseNumber);
91
- if (!accessCheck.allowed) {
92
- return null; // Case doesn't exist or user doesn't have access
93
- }
94
- }
95
-
96
- // Validate case number format
97
- if (!caseNumber || typeof caseNumber !== 'string' || caseNumber.trim() === '') {
98
- throw new Error('Invalid case number provided');
99
- }
100
-
101
- const response = await fetchDataApi(
102
- user,
103
- `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
104
- {
105
- method: 'GET'
106
- }
107
- );
108
-
109
- if (response.status === 404) {
110
- return null; // Case not found
111
- }
112
-
113
- if (!response.ok) {
114
- throw new Error(`Failed to fetch case data: ${response.status} ${response.statusText}`);
115
- }
116
-
117
- const caseData = await response.json() as CaseData;
118
- return caseData;
119
-
120
- } catch (error) {
121
- console.error(`Error fetching case data for ${caseNumber}:`, error);
122
- throw error;
123
- }
124
- };
125
-
126
- /**
127
- * Update case data in R2 storage with validation and timestamps
128
- * @param user - Authenticated user
129
- * @param caseNumber - Case identifier
130
- * @param caseData - Case data to save
131
- * @param options - Optional configuration
132
- */
133
- export const updateCaseData = async (
134
- user: User,
135
- caseNumber: string,
136
- caseData: CaseData,
137
- options: DataOperationOptions = {}
138
- ): Promise<void> => {
139
- try {
140
- // Validate user session
141
- const sessionValidation = await validateUserSession(user);
142
- if (!sessionValidation.valid) {
143
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
144
- }
145
-
146
- // Check modification permissions
147
- const modifyCheck = await canModifyCase(user, caseNumber);
148
- if (!modifyCheck.allowed) {
149
- throw new Error(`Modification denied: ${modifyCheck.reason}`);
150
- }
151
-
152
- // Validate inputs
153
- if (!caseNumber || typeof caseNumber !== 'string') {
154
- throw new Error('Invalid case number provided');
155
- }
156
-
157
- if (!caseData || typeof caseData !== 'object') {
158
- throw new Error('Invalid case data provided');
159
- }
160
-
161
- // Add timestamp if requested (default: true)
162
- const dataToSave = options.includeTimestamp !== false ? {
163
- ...caseData,
164
- updatedAt: new Date().toISOString()
165
- } : caseData;
166
-
167
- const response = await fetchDataApi(
168
- user,
169
- `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
170
- {
171
- method: 'PUT',
172
- headers: {
173
- 'Content-Type': 'application/json'
174
- },
175
- body: JSON.stringify(dataToSave)
176
- }
177
- );
178
-
179
- if (!response.ok) {
180
- throw new Error(`Failed to update case data: ${response.status} ${response.statusText}`);
181
- }
182
-
183
- } catch (error) {
184
- console.error(`Error updating case data for ${caseNumber}:`, error);
185
- throw error;
186
- }
187
- };
188
-
189
- /**
190
- * Delete case data from R2 storage with validation
191
- * @param user - Authenticated user
192
- * @param caseNumber - Case identifier
193
- */
194
- export const deleteCaseData = async (
195
- user: User,
196
- caseNumber: string,
197
- options: DataOperationOptions = {}
198
- ): Promise<void> => {
199
- try {
200
- // Validate user session
201
- const sessionValidation = await validateUserSession(user);
202
- if (!sessionValidation.valid) {
203
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
204
- }
205
-
206
- // Check modification permissions if validation is not explicitly disabled
207
- if (options.skipValidation !== true) {
208
- const modifyCheck = await canModifyCase(user, caseNumber);
209
- if (!modifyCheck.allowed) {
210
- throw new Error(`Delete denied: ${modifyCheck.reason}`);
211
- }
212
- }
213
-
214
- const response = await fetchDataApi(
215
- user,
216
- `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
217
- {
218
- method: 'DELETE'
219
- }
220
- );
221
-
222
- if (!response.ok && response.status !== 404) {
223
- throw new Error(`Failed to delete case data: ${response.status} ${response.statusText}`);
224
- }
225
-
226
- } catch (error) {
227
- console.error(`Error deleting case data for ${caseNumber}:`, error);
228
- throw error;
229
- }
230
- };
231
-
232
- // ============================================================================
233
- // FILE ANNOTATION OPERATIONS
234
- // ============================================================================
235
-
236
- /**
237
- * Get file annotation data from R2 storage
238
- * @param user - Authenticated user
239
- * @param caseNumber - Case identifier
240
- * @param fileId - File identifier
241
- */
242
- export const getFileAnnotations = async (
243
- user: User,
244
- caseNumber: string,
245
- fileId: string
246
- ): Promise<AnnotationData | null> => {
247
- try {
248
- // Validate user session
249
- const sessionValidation = await validateUserSession(user);
250
- if (!sessionValidation.valid) {
251
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
252
- }
253
-
254
- // Check case access
255
- const accessCheck = await canAccessCase(user, caseNumber);
256
- if (!accessCheck.allowed) {
257
- throw new Error(`Access denied: ${accessCheck.reason}`);
258
- }
259
-
260
- // Validate inputs
261
- if (!fileId || typeof fileId !== 'string') {
262
- throw new Error('Invalid file ID provided');
263
- }
264
-
265
- const response = await fetchDataApi(
266
- user,
267
- `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
268
- {
269
- method: 'GET'
270
- }
271
- );
272
-
273
- if (response.status === 404) {
274
- return null; // No annotations found
275
- }
276
-
277
- if (!response.ok) {
278
- throw new Error(`Failed to fetch file annotations: ${response.status} ${response.statusText}`);
279
- }
280
-
281
- return await response.json() as AnnotationData;
282
-
283
- } catch (error) {
284
- console.error(`Error fetching annotations for ${caseNumber}/${fileId}:`, error);
285
- return null; // Return null for graceful handling
286
- }
287
- };
288
-
289
- /**
290
- * Save file annotation data to R2 storage
291
- * @param user - Authenticated user
292
- * @param caseNumber - Case identifier
293
- * @param fileId - File identifier
294
- * @param annotationData - Annotation data to save
295
- * @param options - Optional configuration
296
- */
297
- export const saveFileAnnotations = async (
298
- user: User,
299
- caseNumber: string,
300
- fileId: string,
301
- annotationData: AnnotationData,
302
- options: DataOperationOptions = {}
303
- ): Promise<void> => {
304
- try {
305
- // Validate user session
306
- const sessionValidation = await validateUserSession(user);
307
- if (!sessionValidation.valid) {
308
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
309
- }
310
-
311
- // Check modification permissions if validation is not explicitly disabled
312
- if (options.skipValidation !== true) {
313
- const modifyCheck = await canModifyCase(user, caseNumber);
314
- if (!modifyCheck.allowed) {
315
- throw new Error(`Modification denied: ${modifyCheck.reason}`);
316
- }
317
- }
318
-
319
- // Validate inputs
320
- if (!fileId || typeof fileId !== 'string') {
321
- throw new Error('Invalid file ID provided');
322
- }
323
-
324
- if (!annotationData || typeof annotationData !== 'object') {
325
- throw new Error('Invalid annotation data provided');
326
- }
327
-
328
- // Enforce immutability once confirmation data exists on an image.
329
- const existingResponse = await fetchDataApi(
330
- user,
331
- `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
332
- {
333
- method: 'GET'
334
- }
335
- );
336
-
337
- if (existingResponse.ok) {
338
- const existingAnnotations = await existingResponse.json() as AnnotationData;
339
- if (existingAnnotations?.confirmationData) {
340
- throw new Error('Cannot modify annotations for a confirmed image');
341
- }
342
- } else if (existingResponse.status !== 404) {
343
- throw new Error(`Failed to verify existing annotations: ${existingResponse.status} ${existingResponse.statusText}`);
344
- }
345
-
346
- // Add timestamp to annotation data
347
- const dataToSave = {
348
- ...annotationData,
349
- updatedAt: new Date().toISOString()
350
- };
351
-
352
- const response = await fetchDataApi(
353
- user,
354
- `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
355
- {
356
- method: 'PUT',
357
- headers: {
358
- 'Content-Type': 'application/json'
359
- },
360
- body: JSON.stringify(dataToSave)
361
- }
362
- );
363
-
364
- if (!response.ok) {
365
- throw new Error(`Failed to save file annotations: ${response.status} ${response.statusText}`);
366
- }
367
-
368
- } catch (error) {
369
- console.error(`Error saving annotations for ${caseNumber}/${fileId}:`, error);
370
- throw error;
371
- }
372
- };
373
-
374
- /**
375
- * Delete file annotation data from R2 storage
376
- * @param user - Authenticated user
377
- * @param caseNumber - Case identifier
378
- * @param fileId - File identifier
379
- * @param options - Additional options for the operation
380
- */
381
- export const deleteFileAnnotations = async (
382
- user: User,
383
- caseNumber: string,
384
- fileId: string,
385
- options: { skipValidation?: boolean } = {}
386
- ): Promise<void> => {
387
- try {
388
- // Validate user session
389
- const sessionValidation = await validateUserSession(user);
390
- if (!sessionValidation.valid) {
391
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
392
- }
393
-
394
- // Check modification permissions if validation is not explicitly disabled
395
- if (options.skipValidation !== true) {
396
- const modifyCheck = await canModifyCase(user, caseNumber);
397
- if (!modifyCheck.allowed) {
398
- throw new Error(`Delete denied: ${modifyCheck.reason}`);
399
- }
400
- }
401
-
402
- const response = await fetchDataApi(
403
- user,
404
- `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/${encodeURIComponent(fileId)}/data.json`,
405
- {
406
- method: 'DELETE'
407
- }
408
- );
409
-
410
- if (!response.ok && response.status !== 404) {
411
- throw new Error(`Failed to delete file annotations: ${response.status} ${response.statusText}`);
412
- }
413
-
414
- } catch (error) {
415
- console.error(`Error deleting annotations for ${caseNumber}/${fileId}:`, error);
416
- throw error;
417
- }
418
- };
419
-
420
- // ============================================================================
421
- // BATCH OPERATIONS
422
- // ============================================================================
423
-
424
- /**
425
- * Update multiple files with annotation data in a single operation
426
- * @param user - Authenticated user
427
- * @param caseNumber - Case identifier
428
- * @param updates - Array of file updates to apply
429
- */
430
- export const batchUpdateFiles = async (
431
- user: User,
432
- caseNumber: string,
433
- updates: FileUpdate[],
434
- options: DataOperationOptions = {}
435
- ): Promise<BatchUpdateResult> => {
436
- const result: BatchUpdateResult = {
437
- successful: [],
438
- failed: []
439
- };
440
-
441
- try {
442
- // Validate session and permissions once for the batch
443
- const sessionValidation = await validateUserSession(user);
444
- if (!sessionValidation.valid) {
445
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
446
- }
447
-
448
- // Check modification permissions
449
- const modifyCheck = await canModifyCase(user, caseNumber);
450
- if (!modifyCheck.allowed) {
451
- throw new Error(`Batch update denied: ${modifyCheck.reason}`);
452
- }
453
-
454
- const perFileOptions: DataOperationOptions = {
455
- ...options,
456
- skipValidation: true
457
- };
458
-
459
- // Process each file update
460
- for (const update of updates) {
461
- try {
462
- await saveFileAnnotations(user, caseNumber, update.fileId, update.annotations, perFileOptions);
463
- result.successful.push(update.fileId);
464
- } catch (error) {
465
- result.failed.push({
466
- fileId: update.fileId,
467
- error: error instanceof Error ? error.message : 'Unknown error'
468
- });
469
- }
470
- }
471
-
472
- return result;
473
-
474
- } catch (error) {
475
- // If validation fails, mark all as failed
476
- for (const update of updates) {
477
- result.failed.push({
478
- fileId: update.fileId,
479
- error: error instanceof Error ? error.message : 'Batch operation failed'
480
- });
481
- }
482
- return result;
483
- }
484
- };
485
-
486
- /**
487
- * Duplicate case data from one case to another (for case renaming operations)
488
- * @param user - Authenticated user
489
- * @param fromCaseNumber - Source case number
490
- * @param toCaseNumber - Destination case number
491
- */
492
- export const duplicateCaseData = async (
493
- user: User,
494
- fromCaseNumber: string,
495
- toCaseNumber: string,
496
- options: { skipDestinationCheck?: boolean } = {}
497
- ): Promise<void> => {
498
- try {
499
- // For rename operations, we skip the destination check since the case doesn't exist yet
500
- if (!options.skipDestinationCheck) {
501
- // Check if user has permission to create/modify the destination case
502
- const accessResult = await canModifyCase(user, toCaseNumber);
503
- if (!accessResult.allowed) {
504
- throw new Error(`User does not have permission to create or modify case ${toCaseNumber}: ${accessResult.reason || 'Access denied'}`);
505
- }
506
- }
507
-
508
- // Get source case data
509
- const sourceCaseData = await getCaseData(user, fromCaseNumber);
510
- if (!sourceCaseData) {
511
- throw new Error(`Source case ${fromCaseNumber} not found`);
512
- }
513
-
514
- // Update case number in the data
515
- const newCaseData = {
516
- ...sourceCaseData,
517
- caseNumber: toCaseNumber,
518
- updatedAt: new Date().toISOString()
519
- };
520
-
521
- // Save to new location
522
- await updateCaseData(
523
- user,
524
- toCaseNumber,
525
- newCaseData
526
- );
527
-
528
- // Copy file annotations if they exist
529
- if (sourceCaseData.files && sourceCaseData.files.length > 0) {
530
- const updates: FileUpdate[] = [];
531
-
532
- for (const file of sourceCaseData.files) {
533
- const annotations = await getFileAnnotations(user, fromCaseNumber, file.id);
534
- if (annotations) {
535
- updates.push({
536
- fileId: file.id,
537
- annotations
538
- });
539
- }
540
- }
541
-
542
- if (updates.length > 0) {
543
- await batchUpdateFiles(
544
- user,
545
- toCaseNumber,
546
- updates
547
- );
548
- }
549
- }
550
-
551
- } catch (error) {
552
- console.error(`Error duplicating case data from ${fromCaseNumber} to ${toCaseNumber}:`, error);
553
- throw error;
554
- }
555
- };
556
-
557
- // ============================================================================
558
- // VALIDATION AND UTILITY FUNCTIONS
559
- // ============================================================================
560
-
561
- /**
562
- * Validate data access permissions for a user and case
563
- * @param user - Authenticated user
564
- * @param caseNumber - Case identifier
565
- */
566
- export const validateDataAccess = async (
567
- user: User,
568
- caseNumber: string
569
- ): Promise<DataAccessResult> => {
570
- try {
571
- // Session validation
572
- const sessionValidation = await validateUserSession(user);
573
- if (!sessionValidation.valid) {
574
- return { allowed: false, reason: sessionValidation.reason };
575
- }
576
-
577
- // Case access validation
578
- const accessCheck = await canAccessCase(user, caseNumber);
579
- if (!accessCheck.allowed) {
580
- return { allowed: false, reason: accessCheck.reason };
581
- }
582
-
583
- return { allowed: true };
584
-
585
- } catch (error) {
586
- console.error('Error validating data access:', error);
587
- return { allowed: false, reason: 'Access validation failed' };
588
- }
589
- };
590
-
591
- /**
592
- * Higher-order function for consistent data operation patterns
593
- * Wraps operations with standard validation and error handling
594
- * @param operation - The data operation to wrap
595
- */
596
- export const withDataOperation = <T>(
597
- operation: DataOperation<T>
598
- ) => async (user: User, ...args: unknown[]): Promise<T> => {
599
- try {
600
- // Standard session validation
601
- const sessionValidation = await validateUserSession(user);
602
- if (!sessionValidation.valid) {
603
- throw new Error(`Operation failed: ${sessionValidation.reason}`);
604
- }
605
-
606
- // Execute the operation
607
- return await operation(user, ...args);
608
-
609
- } catch (error) {
610
- console.error('Data operation failed:', error);
611
- throw error;
612
- }
613
- };
614
-
615
- /**
616
- * Check if a case exists in storage
617
- * @param user - Authenticated user
618
- * @param caseNumber - Case identifier
619
- */
620
- export const caseExists = async (
621
- user: User,
622
- caseNumber: string
623
- ): Promise<boolean> => {
624
- try {
625
- const caseData = await getCaseData(user, caseNumber);
626
- return caseData !== null;
627
- } catch (error) {
628
- // If we get an access denied error, the case might exist but user can't access it
629
- if (error instanceof Error && error.message.includes('Access denied')) {
630
- return false; // For existence checking, treat access denied as "doesn't exist for this user"
631
- }
632
- console.error(`Error checking case existence for ${caseNumber}:`, error);
633
- return false;
634
- }
635
- };
636
-
637
- /**
638
- * Check if a file has annotations
639
- * @param user - Authenticated user
640
- * @param caseNumber - Case identifier
641
- * @param fileId - File identifier
642
- */
643
- export const fileHasAnnotations = async (
644
- user: User,
645
- caseNumber: string,
646
- fileId: string
647
- ): Promise<boolean> => {
648
- try {
649
- const annotations = await getFileAnnotations(user, caseNumber, fileId);
650
- return annotations !== null;
651
- } catch (error) {
652
- console.error(`Error checking annotations for ${caseNumber}/${fileId}:`, error);
653
- return false;
654
- }
655
- };
656
-
657
- /**
658
- * Request a server-side signature for a forensic manifest.
659
- * The signature is generated by the data worker using a private key secret.
660
- */
661
- export const signForensicManifest = async (
662
- user: User,
663
- caseNumber: string,
664
- manifest: ForensicManifestData
665
- ): Promise<ManifestSigningResponse> => {
666
- try {
667
- const sessionValidation = await validateUserSession(user);
668
- if (!sessionValidation.valid) {
669
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
670
- }
671
-
672
- const accessCheck = await canAccessCase(user, caseNumber);
673
- if (!accessCheck.allowed) {
674
- throw new Error(`Manifest signing denied: ${accessCheck.reason}`);
675
- }
676
-
677
- const response = await fetchDataApi(user, '/api/forensic/sign-manifest', {
678
- method: 'POST',
679
- headers: {
680
- 'Content-Type': 'application/json'
681
- },
682
- body: JSON.stringify({
683
- userId: user.uid,
684
- caseNumber,
685
- manifest
686
- })
687
- });
688
-
689
- const responseData = await response.json().catch(() => null) as {
690
- success?: boolean;
691
- error?: string;
692
- manifestVersion?: string;
693
- signature?: ForensicManifestSignature;
694
- } | null;
695
-
696
- if (!response.ok) {
697
- throw new Error(
698
- responseData?.error ||
699
- `Failed to sign forensic manifest: ${response.status} ${response.statusText}`
700
- );
701
- }
702
-
703
- if (!responseData?.success || !responseData.signature || !responseData.manifestVersion) {
704
- throw new Error('Invalid manifest signing response from data worker');
705
- }
706
-
707
- if (responseData.manifestVersion !== FORENSIC_MANIFEST_VERSION) {
708
- throw new Error(
709
- `Unexpected manifest version from signer: ${responseData.manifestVersion}`
710
- );
711
- }
712
-
713
- return {
714
- manifestVersion: responseData.manifestVersion,
715
- signature: responseData.signature
716
- };
717
- } catch (error) {
718
- console.error(`Error signing forensic manifest for ${caseNumber}:`, error);
719
- throw error;
720
- }
721
- };
722
-
723
- /**
724
- * Request a server-side signature for confirmation export data.
725
- * The signature is generated by the data worker using a private key secret.
726
- */
727
- export const signConfirmationData = async (
728
- user: User,
729
- caseNumber: string,
730
- confirmationData: ConfirmationImportData
731
- ): Promise<ConfirmationSigningResponse> => {
732
- try {
733
- const sessionValidation = await validateUserSession(user);
734
- if (!sessionValidation.valid) {
735
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
736
- }
737
-
738
- const accessCheck = await canAccessCase(user, caseNumber);
739
- if (!accessCheck.allowed) {
740
- throw new Error(`Confirmation signing denied: ${accessCheck.reason}`);
741
- }
742
-
743
- const response = await fetchDataApi(user, '/api/forensic/sign-confirmation', {
744
- method: 'POST',
745
- headers: {
746
- 'Content-Type': 'application/json'
747
- },
748
- body: JSON.stringify({
749
- userId: user.uid,
750
- caseNumber,
751
- confirmationData,
752
- signatureVersion: CONFIRMATION_SIGNATURE_VERSION
753
- })
754
- });
755
-
756
- const responseData = await response.json().catch(() => null) as {
757
- success?: boolean;
758
- error?: string;
759
- signatureVersion?: string;
760
- signature?: ForensicManifestSignature;
761
- } | null;
762
-
763
- if (!response.ok) {
764
- throw new Error(
765
- responseData?.error ||
766
- `Failed to sign confirmation data: ${response.status} ${response.statusText}`
767
- );
768
- }
769
-
770
- if (!responseData?.success || !responseData.signature || !responseData.signatureVersion) {
771
- throw new Error('Invalid confirmation signing response from data worker');
772
- }
773
-
774
- if (responseData.signatureVersion !== CONFIRMATION_SIGNATURE_VERSION) {
775
- throw new Error(
776
- `Unexpected confirmation signature version from signer: ${responseData.signatureVersion}`
777
- );
778
- }
779
-
780
- return {
781
- signatureVersion: responseData.signatureVersion,
782
- signature: responseData.signature
783
- };
784
- } catch (error) {
785
- console.error(`Error signing confirmation data for ${caseNumber}:`, error);
786
- throw error;
787
- }
788
- };
789
-
790
- /**
791
- * Request a server-side signature for audit export metadata.
792
- * The signature is generated by the data worker using a private key secret.
793
- */
794
- export const signAuditExportData = async (
795
- user: User,
796
- auditExport: AuditExportSigningPayload,
797
- options: { caseNumber?: string } = {}
798
- ): Promise<AuditExportSigningResponse> => {
799
- try {
800
- const sessionValidation = await validateUserSession(user);
801
- if (!sessionValidation.valid) {
802
- throw new Error(`Session validation failed: ${sessionValidation.reason}`);
803
- }
804
-
805
- if (!isValidAuditExportSigningPayload(auditExport)) {
806
- throw new Error('Invalid audit export payload for signing');
807
- }
808
-
809
- const caseNumber = options.caseNumber;
810
- if (caseNumber) {
811
- const accessCheck = await canAccessCase(user, caseNumber);
812
- if (!accessCheck.allowed) {
813
- throw new Error(`Audit export signing denied: ${accessCheck.reason}`);
814
- }
815
- }
816
-
817
- const response = await fetchDataApi(user, '/api/forensic/sign-audit-export', {
818
- method: 'POST',
819
- headers: {
820
- 'Content-Type': 'application/json'
821
- },
822
- body: JSON.stringify({
823
- userId: user.uid,
824
- caseNumber,
825
- auditExport,
826
- signatureVersion: AUDIT_EXPORT_SIGNATURE_VERSION
827
- })
828
- });
829
-
830
- const responseData = await response.json().catch(() => null) as {
831
- success?: boolean;
832
- error?: string;
833
- signatureVersion?: string;
834
- signature?: ForensicManifestSignature;
835
- } | null;
836
-
837
- if (!response.ok) {
838
- throw new Error(
839
- responseData?.error ||
840
- `Failed to sign audit export data: ${response.status} ${response.statusText}`
841
- );
842
- }
843
-
844
- if (!responseData?.success || !responseData.signature || !responseData.signatureVersion) {
845
- throw new Error('Invalid audit export signing response from data worker');
846
- }
847
-
848
- if (responseData.signatureVersion !== AUDIT_EXPORT_SIGNATURE_VERSION) {
849
- throw new Error(
850
- `Unexpected audit export signature version from signer: ${responseData.signatureVersion}`
851
- );
852
- }
853
-
854
- return {
855
- signatureVersion: responseData.signatureVersion,
856
- signature: responseData.signature
857
- };
858
- } catch (error) {
859
- console.error('Error signing audit export data:', error);
860
- throw error;
861
- }
862
- };
2
+ * Centralized data worker operations for case and file management.
3
+ *
4
+ * This module remains the public compatibility surface for existing imports.
5
+ * Implementation details are split across domain modules in ./operations.
6
+ */
7
+
8
+ export * from './operations';
9
+
10
+ export {
11
+ getConfirmationSummaryTelemetry,
12
+ resetConfirmationSummaryTelemetry,
13
+ type CaseConfirmationSummary,
14
+ type ConfirmationSummaryEnsureOptions,
15
+ type ConfirmationSummaryTelemetry,
16
+ type FileConfirmationSummary,
17
+ type UserConfirmationSummaryDocument
18
+ } from './confirmation-summary/summary-core';