@striae-org/striae 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.env.example +8 -0
  2. package/app/components/actions/case-export/core-export.ts +14 -8
  3. package/app/components/actions/case-export/data-processing.ts +1 -0
  4. package/app/components/actions/case-export/download-handlers.ts +7 -0
  5. package/app/components/actions/case-export/metadata-helpers.ts +2 -1
  6. package/app/components/actions/case-import/confirmation-import.ts +12 -2
  7. package/app/components/actions/case-import/orchestrator.ts +78 -32
  8. package/app/components/actions/case-import/storage-operations.ts +97 -8
  9. package/app/components/actions/case-import/zip-processing.ts +159 -86
  10. package/app/components/actions/case-manage.ts +430 -8
  11. package/app/components/actions/confirm-export.ts +9 -2
  12. package/app/components/actions/image-manage.ts +77 -44
  13. package/app/components/audit/user-audit-viewer.tsx +19 -8
  14. package/app/components/audit/user-audit.module.css +21 -0
  15. package/app/components/audit/viewer/audit-entries-list.tsx +7 -0
  16. package/app/components/audit/viewer/audit-filters-panel.tsx +1 -0
  17. package/app/components/audit/viewer/audit-viewer-utils.ts +2 -0
  18. package/app/components/audit/viewer/use-audit-viewer-data.ts +21 -1
  19. package/app/components/canvas/box-annotations/box-annotations.module.css +22 -18
  20. package/app/components/canvas/box-annotations/box-annotations.tsx +15 -0
  21. package/app/components/canvas/canvas.module.css +64 -54
  22. package/app/components/canvas/canvas.tsx +14 -16
  23. package/app/components/canvas/confirmation/confirmation.module.css +1 -0
  24. package/app/components/canvas/confirmation/confirmation.tsx +6 -12
  25. package/app/components/navbar/case-modals/archive-case-modal.module.css +110 -0
  26. package/app/components/navbar/case-modals/archive-case-modal.tsx +129 -0
  27. package/app/components/navbar/case-modals/open-case-modal.module.css +81 -0
  28. package/app/components/navbar/case-modals/open-case-modal.tsx +120 -0
  29. package/app/components/navbar/case-modals/rename-case-modal.module.css +81 -0
  30. package/app/components/navbar/case-modals/rename-case-modal.tsx +107 -0
  31. package/app/components/navbar/navbar.module.css +447 -0
  32. package/app/components/navbar/navbar.tsx +377 -0
  33. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +1 -0
  34. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +15 -16
  35. package/app/components/sidebar/case-export/case-export.module.css +1 -0
  36. package/app/components/sidebar/case-export/case-export.tsx +8 -46
  37. package/app/components/sidebar/case-import/case-import.module.css +23 -0
  38. package/app/components/sidebar/case-import/case-import.tsx +64 -16
  39. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +20 -1
  40. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +15 -0
  41. package/app/components/sidebar/cases/case-sidebar.tsx +25 -519
  42. package/app/components/sidebar/cases/cases-modal.module.css +1 -0
  43. package/app/components/sidebar/cases/cases-modal.tsx +6 -8
  44. package/app/components/sidebar/cases/cases.module.css +62 -21
  45. package/app/components/sidebar/files/files-modal.module.css +1 -0
  46. package/app/components/sidebar/files/files-modal.tsx +12 -13
  47. package/app/components/sidebar/notes/notes-editor-modal.module.css +49 -0
  48. package/app/components/sidebar/notes/notes-editor-modal.tsx +66 -0
  49. package/app/components/sidebar/notes/notes-modal.tsx +7 -8
  50. package/app/components/sidebar/notes/notes-sidebar.tsx +199 -113
  51. package/app/components/sidebar/notes/notes.module.css +153 -0
  52. package/app/components/sidebar/sidebar-container.tsx +15 -28
  53. package/app/components/sidebar/sidebar.module.css +5 -69
  54. package/app/components/sidebar/sidebar.tsx +24 -125
  55. package/app/components/sidebar/upload/image-upload-zone.module.css +13 -13
  56. package/app/components/user/inactivity-warning.module.css +1 -0
  57. package/app/components/user/inactivity-warning.tsx +15 -2
  58. package/app/components/user/manage-profile.tsx +23 -10
  59. package/app/hooks/useOverlayDismiss.ts +52 -4
  60. package/app/routes/auth/login.tsx +785 -774
  61. package/app/routes/striae/striae.module.css +10 -3
  62. package/app/routes/striae/striae.tsx +469 -30
  63. package/app/services/audit/audit.service.ts +173 -27
  64. package/app/services/audit/builders/audit-event-builders-case-file.ts +43 -0
  65. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -0
  66. package/app/services/audit/builders/index.ts +1 -0
  67. package/app/types/audit.ts +3 -1
  68. package/app/types/case.ts +29 -0
  69. package/app/types/import.ts +3 -0
  70. package/app/utils/data/permissions.ts +16 -1
  71. package/app/utils/forensics/audit-export-signature.ts +5 -1
  72. package/app/utils/forensics/confirmation-signature.ts +3 -0
  73. package/app/utils/forensics/export-verification.ts +497 -22
  74. package/package.json +3 -3
  75. package/scripts/deploy-primershear-emails.sh +2 -1
  76. package/worker-configuration.d.ts +1 -1
  77. package/workers/audit-worker/worker-configuration.d.ts +7448 -11323
  78. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  79. package/workers/data-worker/worker-configuration.d.ts +7448 -11323
  80. package/workers/data-worker/wrangler.jsonc.example +1 -1
  81. package/workers/image-worker/worker-configuration.d.ts +7447 -11322
  82. package/workers/image-worker/wrangler.jsonc.example +1 -1
  83. package/workers/keys-worker/worker-configuration.d.ts +7447 -11322
  84. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  85. package/workers/pdf-worker/src/formats/format-striae.ts +8 -7
  86. package/workers/pdf-worker/worker-configuration.d.ts +7448 -11323
  87. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  88. package/workers/user-worker/worker-configuration.d.ts +7448 -11323
  89. package/workers/user-worker/wrangler.jsonc.example +1 -1
  90. package/wrangler.toml.example +1 -1
  91. package/public/.well-known/keybase.txt +0 -56
@@ -9,6 +9,7 @@ import type {
9
9
  AuditResult,
10
10
  PerformanceMetrics
11
11
  } from '~/types';
12
+ import { getCaseData, getUserData } from '~/utils/data';
12
13
  import { generateWorkflowId } from '~/utils/common';
13
14
  import {
14
15
  fetchAuditEntriesForUser,
@@ -26,6 +27,7 @@ import {
26
27
  buildAnnotationCreateAuditParams,
27
28
  buildAnnotationDeleteAuditParams,
28
29
  buildAnnotationEditAuditParams,
30
+ buildCaseArchiveAuditParams,
29
31
  buildCaseCreationAuditParams,
30
32
  buildCaseDeletionAuditParams,
31
33
  buildCaseExportAuditParams,
@@ -137,6 +139,67 @@ export class AuditService {
137
139
  }
138
140
  }
139
141
 
142
+ private normalizeBadgeId(badgeId?: string): string | undefined {
143
+ const normalized = badgeId?.trim();
144
+ return normalized ? normalized : undefined;
145
+ }
146
+
147
+ private applyBadgeIdToParams(
148
+ params: CreateAuditEntryParams,
149
+ badgeId?: string
150
+ ): CreateAuditEntryParams {
151
+ const normalizedBadgeId = this.normalizeBadgeId(badgeId);
152
+ if (!normalizedBadgeId) {
153
+ return params;
154
+ }
155
+
156
+ return {
157
+ ...params,
158
+ userProfileDetails: {
159
+ ...(params.userProfileDetails || {}),
160
+ badgeId: normalizedBadgeId
161
+ }
162
+ };
163
+ }
164
+
165
+ private async resolveBadgeIdForUser(user: User): Promise<string | undefined> {
166
+ const cachedBadgeId = this.userBadgeIdByUserId.get(user.uid);
167
+ if (cachedBadgeId) {
168
+ return cachedBadgeId;
169
+ }
170
+
171
+ try {
172
+ const userData = await getUserData(user);
173
+ const resolvedBadgeId = this.normalizeBadgeId(userData?.badgeId);
174
+
175
+ if (resolvedBadgeId) {
176
+ this.userBadgeIdByUserId.set(user.uid, resolvedBadgeId);
177
+ }
178
+
179
+ return resolvedBadgeId;
180
+ } catch (error) {
181
+ console.error('🚨 Audit: Failed to resolve badge ID for user:', error);
182
+ return undefined;
183
+ }
184
+ }
185
+
186
+ private async logEventForUser(user: User, params: CreateAuditEntryParams): Promise<void> {
187
+ const resolvedBadgeId = await this.resolveBadgeIdForUser(user);
188
+ await this.logEvent(this.applyBadgeIdToParams(params, resolvedBadgeId));
189
+ }
190
+
191
+ private async logEventForOptionalUser(
192
+ user: User | null,
193
+ params: CreateAuditEntryParams
194
+ ): Promise<void> {
195
+ if (!user) {
196
+ await this.logEvent(params);
197
+ return;
198
+ }
199
+
200
+ await this.logEventForUser(user, params);
201
+ }
202
+
140
203
  /**
141
204
  * Log case export event
142
205
  */
@@ -155,7 +218,7 @@ export class AuditService {
155
218
  keyId?: string;
156
219
  }
157
220
  ): Promise<void> {
158
- await this.logEvent(
221
+ await this.logEventForUser(user,
159
222
  buildCaseExportAuditParams({
160
223
  user,
161
224
  caseNumber,
@@ -188,7 +251,7 @@ export class AuditService {
188
251
  keyId?: string;
189
252
  }
190
253
  ): Promise<void> {
191
- await this.logEvent(
254
+ await this.logEventForUser(user,
192
255
  buildCaseImportAuditParams({
193
256
  user,
194
257
  caseNumber,
@@ -219,7 +282,7 @@ export class AuditService {
219
282
  originalImageFileName?: string,
220
283
  badgeId?: string
221
284
  ): Promise<void> {
222
- await this.logEvent(
285
+ await this.logEventForUser(user,
223
286
  buildConfirmationCreationAuditParams({
224
287
  user,
225
288
  caseNumber,
@@ -253,7 +316,7 @@ export class AuditService {
253
316
  keyId?: string;
254
317
  }
255
318
  ): Promise<void> {
256
- await this.logEvent(
319
+ await this.logEventForUser(user,
257
320
  buildConfirmationExportAuditParams({
258
321
  user,
259
322
  caseNumber,
@@ -286,9 +349,10 @@ export class AuditService {
286
349
  present: boolean;
287
350
  valid: boolean;
288
351
  keyId?: string;
289
- }
352
+ },
353
+ reviewerBadgeId?: string // Badge/ID number of the reviewing examiner who exported the file
290
354
  ): Promise<void> {
291
- await this.logEvent(
355
+ await this.logEventForUser(user,
292
356
  buildConfirmationImportAuditParams({
293
357
  user,
294
358
  caseNumber,
@@ -298,6 +362,7 @@ export class AuditService {
298
362
  confirmationsImported,
299
363
  errors,
300
364
  reviewingExaminerUid,
365
+ reviewerBadgeId,
301
366
  performanceMetrics,
302
367
  exporterUidValidated,
303
368
  totalConfirmationsInFile,
@@ -318,7 +383,7 @@ export class AuditService {
318
383
  caseNumber: string,
319
384
  caseName: string
320
385
  ): Promise<void> {
321
- await this.logEvent(
386
+ await this.logEventForUser(user,
322
387
  buildCaseCreationAuditParams({
323
388
  user,
324
389
  caseNumber,
@@ -336,7 +401,7 @@ export class AuditService {
336
401
  oldName: string,
337
402
  newName: string
338
403
  ): Promise<void> {
339
- await this.logEvent(
404
+ await this.logEventForUser(user,
340
405
  buildCaseRenameAuditParams({
341
406
  user,
342
407
  caseNumber,
@@ -356,7 +421,7 @@ export class AuditService {
356
421
  deleteReason: string,
357
422
  backupCreated: boolean = false
358
423
  ): Promise<void> {
359
- await this.logEvent(
424
+ await this.logEventForUser(user,
360
425
  buildCaseDeletionAuditParams({
361
426
  user,
362
427
  caseNumber,
@@ -367,6 +432,35 @@ export class AuditService {
367
432
  );
368
433
  }
369
434
 
435
+ /**
436
+ * Log case archive event
437
+ */
438
+ public async logCaseArchive(
439
+ user: User,
440
+ caseNumber: string,
441
+ caseName: string,
442
+ archiveReason: string,
443
+ result: AuditResult = 'success',
444
+ errors: string[] = [],
445
+ totalFiles?: number,
446
+ archivedAt?: string,
447
+ processingTimeMs?: number
448
+ ): Promise<void> {
449
+ await this.logEventForUser(user,
450
+ buildCaseArchiveAuditParams({
451
+ user,
452
+ caseNumber,
453
+ caseName,
454
+ archiveReason,
455
+ result,
456
+ errors,
457
+ totalFiles,
458
+ archivedAt,
459
+ processingTimeMs
460
+ })
461
+ );
462
+ }
463
+
370
464
  /**
371
465
  * Log file upload event
372
466
  */
@@ -381,7 +475,7 @@ export class AuditService {
381
475
  processingTime?: number,
382
476
  fileId?: string
383
477
  ): Promise<void> {
384
- await this.logEvent(
478
+ await this.logEventForUser(user,
385
479
  buildFileUploadAuditParams({
386
480
  user,
387
481
  fileName,
@@ -408,7 +502,7 @@ export class AuditService {
408
502
  fileId?: string,
409
503
  originalFileName?: string
410
504
  ): Promise<void> {
411
- await this.logEvent(
505
+ await this.logEventForUser(user,
412
506
  buildFileDeletionAuditParams({
413
507
  user,
414
508
  fileName,
@@ -435,7 +529,7 @@ export class AuditService {
435
529
  accessReason?: string,
436
530
  originalFileName?: string
437
531
  ): Promise<void> {
438
- await this.logEvent(
532
+ await this.logEventForUser(user,
439
533
  buildFileAccessAuditParams({
440
534
  user,
441
535
  fileName,
@@ -463,7 +557,7 @@ export class AuditService {
463
557
  imageFileId?: string,
464
558
  originalImageFileName?: string
465
559
  ): Promise<void> {
466
- await this.logEvent(
560
+ await this.logEventForUser(user,
467
561
  buildAnnotationCreateAuditParams({
468
562
  user,
469
563
  annotationId,
@@ -490,7 +584,7 @@ export class AuditService {
490
584
  imageFileId?: string,
491
585
  originalImageFileName?: string
492
586
  ): Promise<void> {
493
- await this.logEvent(
587
+ await this.logEventForUser(user,
494
588
  buildAnnotationEditAuditParams({
495
589
  user,
496
590
  annotationId,
@@ -516,7 +610,7 @@ export class AuditService {
516
610
  imageFileId?: string,
517
611
  originalImageFileName?: string
518
612
  ): Promise<void> {
519
- await this.logEvent(
613
+ await this.logEventForUser(user,
520
614
  buildAnnotationDeleteAuditParams({
521
615
  user,
522
616
  annotationId,
@@ -538,7 +632,7 @@ export class AuditService {
538
632
  loginMethod: 'firebase' | 'sso' | 'api-key' | 'manual',
539
633
  userAgent?: string
540
634
  ): Promise<void> {
541
- await this.logEvent(
635
+ await this.logEventForUser(user,
542
636
  buildUserLoginAuditParams({
543
637
  user,
544
638
  sessionId,
@@ -557,7 +651,7 @@ export class AuditService {
557
651
  sessionDuration: number,
558
652
  logoutReason: 'user-initiated' | 'timeout' | 'security' | 'error'
559
653
  ): Promise<void> {
560
- await this.logEvent(
654
+ await this.logEventForUser(user,
561
655
  buildUserLogoutAuditParams({
562
656
  user,
563
657
  sessionId,
@@ -580,7 +674,7 @@ export class AuditService {
580
674
  errors: string[] = [],
581
675
  badgeId?: string
582
676
  ): Promise<void> {
583
- await this.logEvent(
677
+ await this.logEventForUser(user,
584
678
  buildUserProfileUpdateAuditParams({
585
679
  user,
586
680
  profileField,
@@ -701,7 +795,7 @@ export class AuditService {
701
795
  userAgent?: string,
702
796
  sessionId?: string
703
797
  ): Promise<void> {
704
- await this.logEvent(
798
+ await this.logEventForUser(user,
705
799
  buildUserRegistrationAuditParams({
706
800
  user,
707
801
  firstName,
@@ -727,7 +821,7 @@ export class AuditService {
727
821
  userAgent?: string,
728
822
  errors: string[] = []
729
823
  ): Promise<void> {
730
- await this.logEvent(
824
+ await this.logEventForUser(user,
731
825
  buildMfaEnrollmentAuditParams({
732
826
  user,
733
827
  phoneNumber,
@@ -753,7 +847,7 @@ export class AuditService {
753
847
  userAgent?: string,
754
848
  errors: string[] = []
755
849
  ): Promise<void> {
756
- await this.logEvent(
850
+ await this.logEventForUser(user,
757
851
  buildMfaAuthenticationAuditParams({
758
852
  user,
759
853
  mfaMethod,
@@ -778,7 +872,7 @@ export class AuditService {
778
872
  userAgent?: string,
779
873
  errors: string[] = []
780
874
  ): Promise<void> {
781
- await this.logEvent(
875
+ await this.logEventForUser(user,
782
876
  buildEmailVerificationAuditParams({
783
877
  user,
784
878
  result,
@@ -828,7 +922,7 @@ export class AuditService {
828
922
  sessionId?: string,
829
923
  userAgent?: string
830
924
  ): Promise<void> {
831
- await this.logEvent(
925
+ await this.logEventForUser(user,
832
926
  buildMarkEmailVerificationSuccessfulAuditParams({
833
927
  user,
834
928
  reason,
@@ -852,7 +946,7 @@ export class AuditService {
852
946
  sourceFileId?: string,
853
947
  sourceFileName?: string
854
948
  ): Promise<void> {
855
- await this.logEvent(
949
+ await this.logEventForUser(user,
856
950
  buildPDFGenerationAuditParams({
857
951
  user,
858
952
  fileName,
@@ -878,7 +972,7 @@ export class AuditService {
878
972
  targetResource?: string,
879
973
  blockedBySystem: boolean = true
880
974
  ): Promise<void> {
881
- await this.logEvent(
975
+ await this.logEventForOptionalUser(user,
882
976
  buildSecurityViolationAuditParams({
883
977
  user,
884
978
  incidentType,
@@ -898,6 +992,7 @@ export class AuditService {
898
992
  * Get audit entries for display (public method for components)
899
993
  */
900
994
  public async getAuditEntriesForUser(userId: string, params?: {
995
+ requestingUser?: User;
901
996
  startDate?: string;
902
997
  endDate?: string;
903
998
  caseNumber?: string;
@@ -911,7 +1006,7 @@ export class AuditService {
911
1006
  userId,
912
1007
  ...params
913
1008
  };
914
- return await this.getAuditEntries(queryParams);
1009
+ return await this.getAuditEntries(queryParams, params?.requestingUser);
915
1010
  }
916
1011
 
917
1012
  /**
@@ -943,8 +1038,59 @@ export class AuditService {
943
1038
  /**
944
1039
  * Get audit entries based on query parameters
945
1040
  */
946
- private async getAuditEntries(params: AuditQueryParams): Promise<ValidationAuditEntry[]> {
1041
+ private applyDateRangeFilter(
1042
+ entries: ValidationAuditEntry[],
1043
+ startDate?: string,
1044
+ endDate?: string
1045
+ ): ValidationAuditEntry[] {
1046
+ const startMs = startDate ? new Date(startDate).getTime() : Number.NEGATIVE_INFINITY;
1047
+ const endMs = endDate ? new Date(endDate).getTime() : Number.POSITIVE_INFINITY;
1048
+
1049
+ return entries.filter((entry) => {
1050
+ const timestampMs = new Date(entry.timestamp).getTime();
1051
+ return timestampMs >= startMs && timestampMs <= endMs;
1052
+ });
1053
+ }
1054
+
1055
+ private async getBundledArchivedCaseAuditEntries(
1056
+ params: AuditQueryParams,
1057
+ requestingUser?: User
1058
+ ): Promise<ValidationAuditEntry[] | null> {
1059
+ if (!requestingUser || !params.caseNumber) {
1060
+ return null;
1061
+ }
1062
+
1063
+ const caseData = await getCaseData(requestingUser, params.caseNumber);
1064
+ if (!caseData?.isReadOnly || caseData.archived !== true) {
1065
+ return null;
1066
+ }
1067
+
1068
+ const bundledEntries = caseData.bundledAuditTrail?.entries;
1069
+ if (!Array.isArray(bundledEntries)) {
1070
+ return [];
1071
+ }
1072
+
1073
+ const sortedEntries = sortAuditEntriesNewestFirst(bundledEntries);
1074
+ const dateFilteredEntries = this.applyDateRangeFilter(
1075
+ sortedEntries,
1076
+ params.startDate,
1077
+ params.endDate
1078
+ );
1079
+ const filteredEntries = applyAuditEntryFilters(dateFilteredEntries, {
1080
+ ...params,
1081
+ userId: undefined
1082
+ });
1083
+
1084
+ return applyAuditPagination(filteredEntries, params);
1085
+ }
1086
+
1087
+ private async getAuditEntries(params: AuditQueryParams, requestingUser?: User): Promise<ValidationAuditEntry[]> {
947
1088
  try {
1089
+ const bundledArchivedEntries = await this.getBundledArchivedCaseAuditEntries(params, requestingUser);
1090
+ if (bundledArchivedEntries) {
1091
+ return bundledArchivedEntries;
1092
+ }
1093
+
948
1094
  // If userId is provided, fetch from server
949
1095
  if (params.userId) {
950
1096
  const serverEntries = await fetchAuditEntriesForUser({
@@ -88,6 +88,49 @@ export const buildCaseDeletionAuditParams = (
88
88
  };
89
89
  };
90
90
 
91
+ interface BuildCaseArchiveAuditParamsInput {
92
+ user: User;
93
+ caseNumber: string;
94
+ caseName: string;
95
+ archiveReason: string;
96
+ result?: AuditResult;
97
+ errors?: string[];
98
+ totalFiles?: number;
99
+ archivedAt?: string;
100
+ processingTimeMs?: number;
101
+ }
102
+
103
+ export const buildCaseArchiveAuditParams = (
104
+ input: BuildCaseArchiveAuditParamsInput
105
+ ): CreateAuditEntryParams => {
106
+ const result = input.result || 'success';
107
+ const archivedAt = input.archivedAt || new Date().toISOString();
108
+
109
+ return {
110
+ userId: input.user.uid,
111
+ userEmail: input.user.email || '',
112
+ action: 'case-archive',
113
+ result,
114
+ fileName: `${input.caseNumber}.case`,
115
+ fileType: 'case-package',
116
+ validationErrors: input.errors || [],
117
+ caseNumber: input.caseNumber,
118
+ workflowPhase: 'casework',
119
+ caseDetails: {
120
+ newCaseName: input.caseName,
121
+ deleteReason: input.archiveReason,
122
+ totalFiles: input.totalFiles,
123
+ lastModified: archivedAt,
124
+ },
125
+ performanceMetrics: input.processingTimeMs
126
+ ? {
127
+ processingTimeMs: input.processingTimeMs,
128
+ fileSizeBytes: 0,
129
+ }
130
+ : undefined,
131
+ };
132
+ };
133
+
91
134
  interface BuildFileUploadAuditParamsInput {
92
135
  user: User;
93
136
  fileName: string;
@@ -220,6 +220,7 @@ interface BuildConfirmationImportAuditParamsInput {
220
220
  confirmationsImported: number;
221
221
  errors?: string[];
222
222
  reviewingExaminerUid?: string;
223
+ reviewerBadgeId?: string;
223
224
  performanceMetrics?: PerformanceMetrics;
224
225
  exporterUidValidated?: boolean;
225
226
  totalConfirmationsInFile?: number;
@@ -269,6 +270,7 @@ export const buildConfirmationImportAuditParams = (
269
270
  },
270
271
  originalExaminerUid: input.user.uid,
271
272
  reviewingExaminerUid: input.reviewingExaminerUid,
273
+ reviewerBadgeId: input.reviewerBadgeId,
272
274
  caseDetails: input.totalConfirmationsInFile !== undefined
273
275
  ? {
274
276
  totalAnnotations: input.totalConfirmationsInFile
@@ -9,6 +9,7 @@ export {
9
9
  } from './audit-event-builders-workflow';
10
10
 
11
11
  export {
12
+ buildCaseArchiveAuditParams,
12
13
  buildCaseCreationAuditParams,
13
14
  buildCaseDeletionAuditParams,
14
15
  buildCaseRenameAuditParams,
@@ -3,7 +3,7 @@
3
3
 
4
4
  export type AuditAction =
5
5
  // Case Management Actions
6
- | 'case-create' | 'case-rename' | 'case-delete'
6
+ | 'case-create' | 'case-rename' | 'case-delete' | 'case-archive'
7
7
  // Confirmation Workflow Actions
8
8
  | 'case-export' | 'case-import' | 'confirmation-create' | 'confirmation-export' | 'confirmation-import'
9
9
  // File Operations
@@ -59,6 +59,7 @@ export interface AuditDetails {
59
59
  // Context & Workflow
60
60
  originalExaminerUid?: string;
61
61
  reviewingExaminerUid?: string;
62
+ reviewerBadgeId?: string;
62
63
  workflowPhase?: WorkflowPhase;
63
64
 
64
65
  // Performance & Metrics
@@ -160,6 +161,7 @@ export interface CreateAuditEntryParams {
160
161
  performanceMetrics?: PerformanceMetrics;
161
162
  originalExaminerUid?: string;
162
163
  reviewingExaminerUid?: string;
164
+ reviewerBadgeId?: string;
163
165
  // Extended detail fields
164
166
  caseDetails?: CaseAuditDetails;
165
167
  fileDetails?: FileAuditDetails;
package/app/types/case.ts CHANGED
@@ -1,14 +1,30 @@
1
1
  import { type FileData } from './file';
2
2
  import { type AnnotationData, type ConfirmationData } from './annotations';
3
+ import { type ValidationAuditEntry } from './audit';
3
4
 
4
5
  // Case-related types and interfaces
5
6
 
6
7
  export type CaseActionType = 'loaded' | 'created' | 'deleted' | null;
7
8
 
9
+ export interface BundledAuditTrailData {
10
+ source: 'archive-bundle';
11
+ importedAt: string;
12
+ exportTimestamp?: string;
13
+ totalEntries?: number;
14
+ entries: ValidationAuditEntry[];
15
+ }
16
+
8
17
  export interface CaseData {
9
18
  createdAt: string;
10
19
  caseNumber: string;
11
20
  files: FileData[];
21
+ isReadOnly?: boolean;
22
+ archived?: boolean;
23
+ archivedAt?: string;
24
+ archivedBy?: string;
25
+ archivedByDisplay?: string;
26
+ archiveReason?: string;
27
+ bundledAuditTrail?: BundledAuditTrailData;
12
28
  }
13
29
 
14
30
  export interface ReadOnlyCaseData extends CaseData {
@@ -23,11 +39,17 @@ export interface CaseExportData {
23
39
  metadata: {
24
40
  caseNumber: string;
25
41
  caseCreatedDate: string;
42
+ archived?: boolean;
43
+ archivedAt?: string;
44
+ archivedBy?: string;
45
+ archivedByDisplay?: string;
46
+ archiveReason?: string;
26
47
  exportDate: string;
27
48
  exportedBy: string | null;
28
49
  exportedByUid: string;
29
50
  exportedByName: string;
30
51
  exportedByCompany: string;
52
+ exportedByBadgeId?: string;
31
53
  striaeExportSchemaVersion: string;
32
54
  totalFiles: number;
33
55
  };
@@ -56,6 +78,7 @@ export interface AllCasesExportData {
56
78
  exportedByUid: string;
57
79
  exportedByName: string;
58
80
  exportedByCompany: string;
81
+ exportedByBadgeId?: string;
59
82
  striaeExportSchemaVersion: string;
60
83
  totalCases: number;
61
84
  totalFiles: number;
@@ -84,7 +107,13 @@ export interface CaseDataWithConfirmations {
84
107
  caseNumber: string;
85
108
  files: FileData[];
86
109
  isReadOnly?: boolean;
110
+ archived?: boolean;
111
+ archivedAt?: string;
112
+ archivedBy?: string;
113
+ archivedByDisplay?: string;
114
+ archiveReason?: string;
87
115
  importedAt?: string;
88
116
  originalImageIds?: { [originalId: string]: string };
89
117
  confirmations?: CaseConfirmations;
118
+ bundledAuditTrail?: BundledAuditTrailData;
90
119
  }
@@ -51,6 +51,7 @@ export interface ConfirmationImportData {
51
51
  exportedByUid: string;
52
52
  exportedByName: string;
53
53
  exportedByCompany: string;
54
+ exportedByBadgeId?: string;
54
55
  totalConfirmations: number;
55
56
  version: string;
56
57
  hash: string;
@@ -79,9 +80,11 @@ export interface ConfirmationImportData {
79
80
 
80
81
  export interface CaseImportPreview {
81
82
  caseNumber: string;
83
+ archived?: boolean;
82
84
  exportedBy: string | null;
83
85
  exportedByName: string | null;
84
86
  exportedByCompany: string | null;
87
+ exportedByBadgeId?: string | null;
85
88
  exportDate: string;
86
89
  totalFiles: number;
87
90
  caseCreatedDate?: string;
@@ -1,7 +1,7 @@
1
1
  import type { User } from 'firebase/auth';
2
2
  import type { UserData, ExtendedUserData, UserLimits, ReadOnlyCaseMetadata } from '~/types';
3
3
  import paths from '~/config/config.json';
4
- import { fetchUserApi } from '../api';
4
+ import { fetchDataApi, fetchUserApi } from '../api';
5
5
 
6
6
  const MAX_CASES_REVIEW = paths.max_cases_review;
7
7
  const MAX_FILES_PER_CASE_REVIEW = paths.max_files_per_case_review;
@@ -403,6 +403,21 @@ export const canModifyCase = async (user: User, caseNumber: string): Promise<Per
403
403
  return { allowed: false, reason: 'User data not found' };
404
404
  }
405
405
 
406
+ const archiveCheckResponse = await fetchDataApi(
407
+ user,
408
+ `/${encodeURIComponent(user.uid)}/${encodeURIComponent(caseNumber)}/data.json`,
409
+ {
410
+ method: 'GET'
411
+ }
412
+ );
413
+
414
+ if (archiveCheckResponse.ok) {
415
+ const caseData = await archiveCheckResponse.json() as { archived?: boolean };
416
+ if (caseData.archived) {
417
+ return { allowed: false, reason: 'Archived cases are immutable and read-only' };
418
+ }
419
+ }
420
+
406
421
  // Check if user owns the case (regular cases)
407
422
  if (userData.cases && userData.cases.some(c => c.caseNumber === caseNumber)) {
408
423
  // For owned cases, user must be permitted
@@ -83,7 +83,8 @@ export function createAuditExportSigningPayload(payload: AuditExportSigningPaylo
83
83
 
84
84
  export async function verifyAuditExportSignature(
85
85
  payload: Partial<AuditExportSigningPayload>,
86
- signature?: ForensicManifestSignature
86
+ signature?: ForensicManifestSignature,
87
+ verificationPublicKeyPem?: string
87
88
  ): Promise<ManifestSignatureVerificationResult> {
88
89
  if (!signature) {
89
90
  return {
@@ -112,6 +113,9 @@ export async function verifyAuditExportSignature(
112
113
  noVerificationKeyPrefix: 'No verification key configured for key ID',
113
114
  invalidPublicKeyError: 'Audit export signature verification failed: invalid public key',
114
115
  verificationFailedError: 'Audit export signature verification failed'
116
+ },
117
+ {
118
+ verificationPublicKeyPem
115
119
  }
116
120
  );
117
121
  }
@@ -134,6 +134,9 @@ export function createConfirmationSigningPayload(
134
134
  exportedByUid: confirmationData.metadata.exportedByUid,
135
135
  exportedByName: confirmationData.metadata.exportedByName,
136
136
  exportedByCompany: confirmationData.metadata.exportedByCompany,
137
+ ...(confirmationData.metadata.exportedByBadgeId
138
+ ? { exportedByBadgeId: confirmationData.metadata.exportedByBadgeId }
139
+ : {}),
137
140
  totalConfirmations: confirmationData.metadata.totalConfirmations,
138
141
  version: confirmationData.metadata.version,
139
142
  hash: confirmationData.metadata.hash.toUpperCase(),