@striae-org/striae 4.3.3 → 5.0.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 (53) hide show
  1. package/.env.example +4 -0
  2. package/app/components/actions/case-export/download-handlers.ts +60 -4
  3. package/app/components/actions/case-import/confirmation-import.ts +50 -7
  4. package/app/components/actions/case-import/confirmation-package.ts +99 -22
  5. package/app/components/actions/case-import/orchestrator.ts +116 -13
  6. package/app/components/actions/case-import/validation.ts +171 -7
  7. package/app/components/actions/case-import/zip-processing.ts +224 -127
  8. package/app/components/actions/case-manage.ts +110 -10
  9. package/app/components/actions/confirm-export.ts +32 -3
  10. package/app/components/audit/user-audit.module.css +49 -0
  11. package/app/components/audit/viewer/audit-entries-list.tsx +130 -48
  12. package/app/components/navbar/navbar.module.css +0 -10
  13. package/app/components/navbar/navbar.tsx +0 -22
  14. package/app/components/sidebar/case-import/case-import.module.css +7 -131
  15. package/app/components/sidebar/case-import/case-import.tsx +7 -14
  16. package/app/components/sidebar/case-import/components/CasePreviewSection.tsx +17 -60
  17. package/app/components/sidebar/case-import/components/ConfirmationDialog.tsx +23 -39
  18. package/app/components/sidebar/case-import/components/ConfirmationPreviewSection.tsx +5 -45
  19. package/app/components/sidebar/case-import/components/FileSelector.tsx +5 -6
  20. package/app/components/sidebar/case-import/hooks/useFilePreview.ts +2 -48
  21. package/app/components/sidebar/case-import/utils/file-validation.ts +9 -21
  22. package/app/config-example/config.json +5 -0
  23. package/app/routes/auth/login.tsx +1 -1
  24. package/app/services/audit/audit-console-logger.ts +1 -1
  25. package/app/services/audit/audit-export-csv.ts +1 -1
  26. package/app/services/audit/audit-export-signing.ts +2 -2
  27. package/app/services/audit/audit-export.service.ts +1 -1
  28. package/app/services/audit/audit-worker-client.ts +1 -1
  29. package/app/services/audit/audit.service.ts +5 -75
  30. package/app/services/audit/builders/audit-event-builders-case-file.ts +3 -0
  31. package/app/services/audit/index.ts +2 -2
  32. package/app/types/audit.ts +8 -7
  33. package/app/utils/data/operations/signing-operations.ts +93 -0
  34. package/app/utils/data/operations/types.ts +6 -0
  35. package/app/utils/forensics/export-encryption.ts +316 -0
  36. package/app/utils/forensics/export-verification.ts +1 -409
  37. package/app/utils/forensics/index.ts +1 -0
  38. package/app/utils/ui/case-messages.ts +5 -2
  39. package/package.json +1 -1
  40. package/scripts/deploy-config.sh +97 -3
  41. package/scripts/deploy-worker-secrets.sh +1 -1
  42. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  43. package/workers/data-worker/src/data-worker.example.ts +130 -0
  44. package/workers/data-worker/src/encryption-utils.ts +125 -0
  45. package/workers/data-worker/worker-configuration.d.ts +1 -1
  46. package/workers/data-worker/wrangler.jsonc.example +2 -2
  47. package/workers/image-worker/wrangler.jsonc.example +1 -1
  48. package/workers/keys-worker/wrangler.jsonc.example +1 -1
  49. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  50. package/workers/user-worker/wrangler.jsonc.example +1 -1
  51. package/wrangler.toml.example +1 -1
  52. package/app/components/public-signing-key-modal/public-signing-key-modal.module.css +0 -287
  53. package/app/components/public-signing-key-modal/public-signing-key-modal.tsx +0 -470
@@ -1,6 +1,6 @@
1
1
  import { type ValidationAuditEntry } from '~/types';
2
2
 
3
- export const getAuditSecurityIssuesForConsole = (
3
+ const getAuditSecurityIssuesForConsole = (
4
4
  entry: ValidationAuditEntry
5
5
  ): string[] => {
6
6
  const checks = entry.details.securityChecks;
@@ -42,7 +42,7 @@ export const AUDIT_CSV_ENTRY_HEADERS = [
42
42
  'Confirmed Files'
43
43
  ];
44
44
 
45
- export const formatForCSV = (value?: string | number | null): string => {
45
+ const formatForCSV = (value?: string | number | null): string => {
46
46
  if (value === undefined || value === null) return '';
47
47
  const str = String(value);
48
48
  if (str.includes(',') || str.includes('"') || str.includes('\n')) {
@@ -22,14 +22,14 @@ interface SignAuditExportInput {
22
22
  hash: string;
23
23
  }
24
24
 
25
- export interface AuditExportSignature {
25
+ interface AuditExportSignature {
26
26
  algorithm: string;
27
27
  keyId: string;
28
28
  signedAt: string;
29
29
  value: string;
30
30
  }
31
31
 
32
- export interface SignedAuditExportPayload {
32
+ interface SignedAuditExportPayload {
33
33
  signatureMetadata: AuditExportSigningPayload;
34
34
  signature: AuditExportSignature;
35
35
  }
@@ -8,7 +8,7 @@ import { type AuditExportContext, signAuditExport } from './audit-export-signing
8
8
  * Audit Export Service
9
9
  * Handles exporting audit trails to various formats for compliance and forensic analysis
10
10
  */
11
- export class AuditExportService {
11
+ class AuditExportService {
12
12
  private static instance: AuditExportService;
13
13
 
14
14
  private constructor() {}
@@ -18,7 +18,7 @@ interface PersistAuditEntryResponse {
18
18
  filename: string;
19
19
  }
20
20
 
21
- export type PersistAuditEntryResult =
21
+ type PersistAuditEntryResult =
22
22
  | {
23
23
  ok: true;
24
24
  entryCount: number;
@@ -2,7 +2,6 @@ import type { User } from 'firebase/auth';
2
2
  import type {
3
3
  ValidationAuditEntry,
4
4
  CreateAuditEntryParams,
5
- AuditTrail,
6
5
  AuditQueryParams,
7
6
  WorkflowPhase,
8
7
  AuditAction,
@@ -18,7 +17,6 @@ import {
18
17
  import {
19
18
  applyAuditEntryFilters,
20
19
  applyAuditPagination,
21
- generateAuditSummary,
22
20
  sortAuditEntriesNewestFirst
23
21
  } from './audit-query-helpers';
24
22
  import { logAuditEntryToConsole } from './audit-console-logger';
@@ -58,7 +56,7 @@ import {
58
56
  * Audit Service for ValidationAuditEntry system
59
57
  * Provides comprehensive audit logging throughout the confirmation workflow
60
58
  */
61
- export class AuditService {
59
+ class AuditService {
62
60
  private static instance: AuditService;
63
61
  private auditBuffer: ValidationAuditEntry[] = [];
64
62
  private workflowId: string | null = null;
@@ -383,13 +381,15 @@ export class AuditService {
383
381
  public async logCaseCreation(
384
382
  user: User,
385
383
  caseNumber: string,
386
- caseName: string
384
+ caseName: string,
385
+ renamedFromCaseNumber?: string
387
386
  ): Promise<void> {
388
387
  await this.logEventForUser(user,
389
388
  buildCaseCreationAuditParams({
390
389
  user,
391
390
  caseNumber,
392
- caseName
391
+ caseName,
392
+ renamedFromCaseNumber
393
393
  })
394
394
  );
395
395
  }
@@ -721,37 +721,6 @@ export class AuditService {
721
721
  );
722
722
  }
723
723
 
724
- /**
725
- * Log user account deletion event
726
- */
727
- public async logAccountDeletion(
728
- user: User,
729
- result: AuditResult,
730
- deletionReason: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account' = 'user-requested',
731
- confirmationMethod: 'uid-email' | 'password' | 'admin-override' = 'uid-email',
732
- casesCount?: number,
733
- filesCount?: number,
734
- dataRetentionPeriod?: number,
735
- emailNotificationSent?: boolean,
736
- sessionId?: string,
737
- errors: string[] = []
738
- ): Promise<void> {
739
- // Wrapper that extracts user data and calls the simplified version
740
- return this.logAccountDeletionSimple(
741
- user.uid,
742
- user.email || '',
743
- result,
744
- deletionReason,
745
- confirmationMethod,
746
- casesCount,
747
- filesCount,
748
- dataRetentionPeriod,
749
- emailNotificationSent,
750
- sessionId,
751
- errors
752
- );
753
- }
754
-
755
724
  /**
756
725
  * Log user account deletion event with simplified user data
757
726
  */
@@ -1011,32 +980,6 @@ export class AuditService {
1011
980
  return await this.getAuditEntries(queryParams, params?.requestingUser);
1012
981
  }
1013
982
 
1014
- /**
1015
- * Get audit trail for a case
1016
- */
1017
- public async getAuditTrail(caseNumber: string): Promise<AuditTrail | null> {
1018
- try {
1019
- // Implement retrieval from storage
1020
- const entries = await this.getAuditEntries({ caseNumber });
1021
- if (!entries || entries.length === 0) {
1022
- return null;
1023
- }
1024
-
1025
- const summary = generateAuditSummary(entries);
1026
- const workflowId = this.workflowId || `${caseNumber}-archived`;
1027
-
1028
- return {
1029
- caseNumber,
1030
- workflowId,
1031
- entries,
1032
- summary
1033
- };
1034
- } catch (error) {
1035
- console.error('🚨 Audit: Failed to get audit trail:', error);
1036
- return null;
1037
- }
1038
- }
1039
-
1040
983
  /**
1041
984
  * Get audit entries based on query parameters
1042
985
  */
@@ -1143,19 +1086,6 @@ export class AuditService {
1143
1086
  }
1144
1087
  }
1145
1088
 
1146
- /**
1147
- * Clear audit buffer (for testing)
1148
- */
1149
- public clearBuffer(): void {
1150
- this.auditBuffer = [];
1151
- }
1152
-
1153
- /**
1154
- * Get current buffer size (for monitoring)
1155
- */
1156
- public getBufferSize(): number {
1157
- return this.auditBuffer.length;
1158
- }
1159
1089
  }
1160
1090
 
1161
1091
  // Export singleton instance
@@ -6,6 +6,7 @@ interface BuildCaseCreationAuditParamsInput {
6
6
  user: User;
7
7
  caseNumber: string;
8
8
  caseName: string;
9
+ renamedFromCaseNumber?: string;
9
10
  }
10
11
 
11
12
  export const buildCaseCreationAuditParams = (
@@ -22,7 +23,9 @@ export const buildCaseCreationAuditParams = (
22
23
  caseNumber: input.caseNumber,
23
24
  workflowPhase: 'casework',
24
25
  caseDetails: {
26
+ oldCaseName: input.renamedFromCaseNumber,
25
27
  newCaseName: input.caseName,
28
+ createdByRename: Boolean(input.renamedFromCaseNumber),
26
29
  createdDate: new Date().toISOString(),
27
30
  totalFiles: 0,
28
31
  totalAnnotations: 0
@@ -1,2 +1,2 @@
1
- export { AuditService, auditService } from './audit.service';
2
- export { AuditExportService, auditExportService } from './audit-export.service';
1
+ export { auditService } from './audit.service';
2
+ export { auditExportService } from './audit-export.service';
@@ -44,7 +44,7 @@ export interface ValidationAuditEntry {
44
44
  * Detailed information for each audit entry
45
45
  * Contains action-specific data and metadata
46
46
  */
47
- export interface AuditDetails {
47
+ interface AuditDetails {
48
48
  // Core identification
49
49
  fileName?: string;
50
50
  fileType?: AuditFileType;
@@ -194,9 +194,10 @@ export interface AuditQueryParams {
194
194
  /**
195
195
  * Case management specific audit details
196
196
  */
197
- export interface CaseAuditDetails {
197
+ interface CaseAuditDetails {
198
198
  oldCaseName?: string;
199
199
  newCaseName?: string;
200
+ createdByRename?: boolean;
200
201
  totalFiles?: number;
201
202
  totalAnnotations?: number;
202
203
  confirmedFileNames?: string[];
@@ -210,7 +211,7 @@ export interface CaseAuditDetails {
210
211
  /**
211
212
  * File operation specific audit details
212
213
  */
213
- export interface FileAuditDetails {
214
+ interface FileAuditDetails {
214
215
  fileId?: string;
215
216
  originalFileName?: string;
216
217
  fileSize: number;
@@ -225,7 +226,7 @@ export interface FileAuditDetails {
225
226
  /**
226
227
  * Annotation operation specific audit details
227
228
  */
228
- export interface AnnotationAuditDetails {
229
+ interface AnnotationAuditDetails {
229
230
  annotationId?: string;
230
231
  annotationType?: 'measurement' | 'identification' | 'comparison' | 'note' | 'region';
231
232
  annotationData?: unknown; // The actual annotation data structure
@@ -238,7 +239,7 @@ export interface AnnotationAuditDetails {
238
239
  /**
239
240
  * User session specific audit details
240
241
  */
241
- export interface SessionAuditDetails {
242
+ interface SessionAuditDetails {
242
243
  sessionId?: string;
243
244
  userAgent?: string;
244
245
  sessionDuration?: number;
@@ -249,7 +250,7 @@ export interface SessionAuditDetails {
249
250
  /**
250
251
  * Security incident specific audit details
251
252
  */
252
- export interface SecurityAuditDetails {
253
+ interface SecurityAuditDetails {
253
254
  incidentType?: 'unauthorized-access' | 'data-breach' | 'malware' | 'injection' | 'brute-force' | 'privilege-escalation';
254
255
  severity?: 'low' | 'medium' | 'high' | 'critical';
255
256
  targetResource?: string;
@@ -272,7 +273,7 @@ export interface SecurityAuditDetails {
272
273
  /**
273
274
  * User profile and authentication specific audit details
274
275
  */
275
- export interface UserProfileAuditDetails {
276
+ interface UserProfileAuditDetails {
276
277
  profileField?: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar' | 'badgeId';
277
278
  oldValue?: string;
278
279
  newValue?: string;
@@ -13,6 +13,7 @@ import {
13
13
  type ForensicManifestSignature,
14
14
  FORENSIC_MANIFEST_VERSION
15
15
  } from '../../forensics/SHA256';
16
+ import type { EncryptionManifest } from '../../forensics/export-encryption';
16
17
  import { canAccessCase, validateUserSession } from '../permissions';
17
18
  import type {
18
19
  AuditExportSigningResponse,
@@ -223,3 +224,95 @@ export const signAuditExportData = async (
223
224
  throw error;
224
225
  }
225
226
  };
227
+
228
+ /**
229
+ * Request batch decryption of export data file and images from the data worker
230
+ */
231
+ export const decryptExportBatch = async (
232
+ user: User,
233
+ encryptionManifest: EncryptionManifest,
234
+ encryptedDataBase64: string,
235
+ encryptedImageMap: Record<string, string>
236
+ ): Promise<{ plaintext: string; decryptedImages: Record<string, Blob> }> => {
237
+ try {
238
+ const sessionValidation = await validateUserSession(user);
239
+ if (!sessionValidation.valid) {
240
+ throw new Error(`Session validation failed: ${sessionValidation.reason}`);
241
+ }
242
+
243
+ // Convert encryptedImageMap to array format expected by worker, including per-image IV from manifest
244
+ const encryptedImages = Object.entries(encryptedImageMap).map(([filename, encryptedData]) => {
245
+ const manifestEntry = encryptionManifest.encryptedImages.find(e => e.filename === filename);
246
+ return {
247
+ filename,
248
+ encryptedData,
249
+ iv: manifestEntry?.iv
250
+ };
251
+ });
252
+
253
+ const response = await fetchDataApi(user, '/api/forensic/decrypt-export', {
254
+ method: 'POST',
255
+ headers: {
256
+ 'Content-Type': 'application/json'
257
+ },
258
+ body: JSON.stringify({
259
+ userId: user.uid,
260
+ wrappedKey: encryptionManifest.wrappedKey,
261
+ dataIv: encryptionManifest.dataIv,
262
+ encryptedData: encryptedDataBase64,
263
+ encryptedImages,
264
+ keyId: encryptionManifest.keyId
265
+ })
266
+ });
267
+
268
+ const responseData = await response.json().catch(() => null) as {
269
+ success?: boolean;
270
+ error?: string;
271
+ plaintext?: string;
272
+ decryptedImages?: Array<{ filename: string; data: string }>;
273
+ } | null;
274
+
275
+ if (!response.ok) {
276
+ const errorMessage = responseData?.error || `Failed to decrypt export: ${response.status} ${response.statusText}`;
277
+
278
+ // Special handling for encrypted exports without configured key
279
+ if (response.status === 400 && errorMessage.includes('not configured')) {
280
+ throw new Error(
281
+ 'This export is encrypted. To import it, your Striae instance must have EXPORT_ENCRYPTION_PRIVATE_KEY configured.'
282
+ );
283
+ }
284
+
285
+ throw new Error(errorMessage);
286
+ }
287
+
288
+ if (!responseData?.success || !responseData.plaintext) {
289
+ throw new Error('Invalid decrypt response from data worker');
290
+ }
291
+
292
+ // Convert decrypted image base64 data back to Blobs
293
+ const decryptedImages: Record<string, Blob> = {};
294
+ if (Array.isArray(responseData.decryptedImages)) {
295
+ for (const imageEntry of responseData.decryptedImages) {
296
+ try {
297
+ const binaryString = atob(imageEntry.data);
298
+ const bytes = new Uint8Array(binaryString.length);
299
+ for (let i = 0; i < binaryString.length; i++) {
300
+ bytes[i] = binaryString.charCodeAt(i);
301
+ }
302
+ decryptedImages[imageEntry.filename] = new Blob([bytes]);
303
+ } catch (error) {
304
+ console.error(`Failed to convert decrypted image ${imageEntry.filename}:`, error);
305
+ throw new Error(`Failed to convert decrypted image: ${imageEntry.filename}`);
306
+ }
307
+ }
308
+ }
309
+
310
+ return {
311
+ plaintext: responseData.plaintext,
312
+ decryptedImages
313
+ };
314
+ } catch (error) {
315
+ console.error('Error decrypting export batch:', error);
316
+ throw error;
317
+ }
318
+ };
@@ -39,4 +39,10 @@ export interface AuditExportSigningResponse {
39
39
  signature: ForensicManifestSignature;
40
40
  }
41
41
 
42
+ export interface DecryptExportBatchResponse {
43
+ success: boolean;
44
+ plaintext: string;
45
+ decryptedImages: Array<{ filename: string; data: string }>;
46
+ }
47
+
42
48
  export type DataOperation<T> = (user: User, ...args: unknown[]) => Promise<T>;