@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,1454 @@
1
+ import { User } from 'firebase/auth';
2
+ import {
3
+ ValidationAuditEntry,
4
+ CreateAuditEntryParams,
5
+ AuditTrail,
6
+ AuditQueryParams,
7
+ AuditSummary,
8
+ WorkflowPhase,
9
+ AuditAction,
10
+ AuditResult,
11
+ AuditFileType,
12
+ SecurityCheckResults,
13
+ PerformanceMetrics
14
+ } from '~/types';
15
+ import paths from '~/config/config.json';
16
+ import { getDataApiKey } from '~/utils/auth';
17
+ import { generateWorkflowId } from '../utils/id-generator';
18
+
19
+ const AUDIT_WORKER_URL = paths.audit_worker_url;
20
+
21
+ /**
22
+ * Audit Service for ValidationAuditEntry system
23
+ * Provides comprehensive audit logging throughout the confirmation workflow
24
+ */
25
+ export class AuditService {
26
+ private static instance: AuditService;
27
+ private auditBuffer: ValidationAuditEntry[] = [];
28
+ private workflowId: string | null = null;
29
+
30
+ private constructor() {}
31
+
32
+ public static getInstance(): AuditService {
33
+ if (!AuditService.instance) {
34
+ AuditService.instance = new AuditService();
35
+ }
36
+ return AuditService.instance;
37
+ }
38
+
39
+ /**
40
+ * Initialize a new workflow session with unique ID
41
+ */
42
+ public startWorkflow(caseNumber: string): string {
43
+ const workflowId = generateWorkflowId(caseNumber);
44
+ this.workflowId = workflowId;
45
+ console.log(`🔍 Audit: Started workflow ${this.workflowId}`);
46
+ return workflowId;
47
+ }
48
+
49
+ /**
50
+ * End current workflow session
51
+ */
52
+ public endWorkflow(): void {
53
+ if (this.workflowId) {
54
+ console.log(`🔍 Audit: Ended workflow ${this.workflowId}`);
55
+ this.workflowId = null;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Create and log an audit entry
61
+ */
62
+ public async logEvent(params: CreateAuditEntryParams): Promise<void> {
63
+ const startTime = Date.now();
64
+
65
+ try {
66
+ const auditEntry: ValidationAuditEntry = {
67
+ timestamp: new Date().toISOString(),
68
+ userId: params.userId,
69
+ userEmail: params.userEmail,
70
+ action: params.action,
71
+ result: params.result,
72
+ details: {
73
+ fileName: params.fileName,
74
+ fileType: params.fileType,
75
+ hashValid: params.hashValid,
76
+ validationErrors: params.validationErrors || [],
77
+ caseNumber: params.caseNumber,
78
+ confirmationId: params.confirmationId,
79
+ originalExaminerUid: params.originalExaminerUid,
80
+ reviewingExaminerUid: params.reviewingExaminerUid,
81
+ workflowPhase: params.workflowPhase,
82
+ securityChecks: params.securityChecks,
83
+ performanceMetrics: params.performanceMetrics,
84
+ // Extended detail fields
85
+ caseDetails: params.caseDetails,
86
+ fileDetails: params.fileDetails,
87
+ annotationDetails: params.annotationDetails,
88
+ sessionDetails: params.sessionDetails,
89
+ securityDetails: params.securityDetails
90
+ }
91
+ };
92
+
93
+ // Add to buffer for batch processing
94
+ this.auditBuffer.push(auditEntry);
95
+
96
+ // Log to console for immediate feedback
97
+ this.logToConsole(auditEntry);
98
+
99
+ // Persist to storage asynchronously
100
+ await this.persistAuditEntry(auditEntry);
101
+
102
+ const endTime = Date.now();
103
+ console.log(`🔍 Audit: Event logged in ${endTime - startTime}ms`);
104
+
105
+ } catch (error) {
106
+ console.error('🚨 Audit: Failed to log event:', error);
107
+ // Don't throw - audit failures shouldn't break the main workflow
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Log case export event
113
+ */
114
+ public async logCaseExport(
115
+ user: User,
116
+ caseNumber: string,
117
+ fileName: string,
118
+ result: AuditResult,
119
+ errors: string[] = [],
120
+ performanceMetrics?: PerformanceMetrics,
121
+ exportFormat?: 'json' | 'csv' | 'xlsx' | 'zip',
122
+ protectionEnabled?: boolean,
123
+ signatureDetails?: {
124
+ present?: boolean;
125
+ valid?: boolean;
126
+ keyId?: string;
127
+ }
128
+ ): Promise<void> {
129
+ const securityChecks: SecurityCheckResults = {
130
+ selfConfirmationPrevented: false, // Not applicable for exports
131
+ fileIntegrityValid: result === 'success',
132
+ manifestSignaturePresent: signatureDetails?.present,
133
+ manifestSignatureValid: signatureDetails?.valid,
134
+ manifestSignatureKeyId: signatureDetails?.keyId
135
+ };
136
+
137
+ // Determine file type based on format or fallback to filename
138
+ let fileType: AuditFileType = 'case-package';
139
+ if (exportFormat) {
140
+ switch (exportFormat) {
141
+ case 'json':
142
+ fileType = 'json-data';
143
+ break;
144
+ case 'csv':
145
+ case 'xlsx':
146
+ fileType = 'csv-export';
147
+ break;
148
+ case 'zip':
149
+ fileType = 'case-package';
150
+ break;
151
+ default:
152
+ fileType = 'case-package';
153
+ }
154
+ } else {
155
+ // Fallback: extract from filename
156
+ if (fileName.includes('.json')) fileType = 'json-data';
157
+ else if (fileName.includes('.csv') || fileName.includes('.xlsx')) fileType = 'csv-export';
158
+ else fileType = 'case-package';
159
+ }
160
+
161
+ await this.logEvent({
162
+ userId: user.uid,
163
+ userEmail: user.email || '',
164
+ action: 'export',
165
+ result,
166
+ fileName,
167
+ fileType,
168
+ validationErrors: errors,
169
+ caseNumber,
170
+ workflowPhase: 'case-export',
171
+ securityChecks,
172
+ performanceMetrics,
173
+ originalExaminerUid: user.uid
174
+ });
175
+ }
176
+
177
+ /**
178
+ * Log case import event
179
+ */
180
+ public async logCaseImport(
181
+ user: User,
182
+ caseNumber: string,
183
+ fileName: string,
184
+ result: AuditResult,
185
+ hashValid: boolean,
186
+ errors: string[] = [],
187
+ originalExaminerUid?: string,
188
+ performanceMetrics?: PerformanceMetrics,
189
+ exporterUidValidated?: boolean, // Separate flag for validation status
190
+ signatureDetails?: {
191
+ present?: boolean;
192
+ valid?: boolean;
193
+ keyId?: string;
194
+ }
195
+ ): Promise<void> {
196
+ const securityChecks: SecurityCheckResults = {
197
+ selfConfirmationPrevented: originalExaminerUid ? originalExaminerUid !== user.uid : false,
198
+ fileIntegrityValid: hashValid,
199
+ exporterUidValidated: exporterUidValidated !== undefined ? exporterUidValidated : !!originalExaminerUid,
200
+ manifestSignaturePresent: signatureDetails?.present,
201
+ manifestSignatureValid: signatureDetails?.valid,
202
+ manifestSignatureKeyId: signatureDetails?.keyId
203
+ };
204
+
205
+ await this.logEvent({
206
+ userId: user.uid,
207
+ userEmail: user.email || '',
208
+ action: 'import',
209
+ result,
210
+ fileName,
211
+ fileType: 'case-package',
212
+ hashValid,
213
+ validationErrors: errors,
214
+ caseNumber,
215
+ workflowPhase: 'case-import',
216
+ securityChecks,
217
+ performanceMetrics,
218
+ originalExaminerUid,
219
+ reviewingExaminerUid: user.uid
220
+ });
221
+ }
222
+
223
+ /**
224
+ * Log confirmation creation event
225
+ */
226
+ public async logConfirmationCreation(
227
+ user: User,
228
+ caseNumber: string,
229
+ confirmationId: string,
230
+ result: AuditResult,
231
+ errors: string[] = [],
232
+ originalExaminerUid?: string,
233
+ performanceMetrics?: PerformanceMetrics,
234
+ imageFileId?: string,
235
+ originalImageFileName?: string
236
+ ): Promise<void> {
237
+ const securityChecks: SecurityCheckResults = {
238
+ selfConfirmationPrevented: false, // Not applicable for confirmation creation
239
+ fileIntegrityValid: true // Confirmation creation doesn't involve file integrity validation
240
+ };
241
+
242
+ await this.logEvent({
243
+ userId: user.uid,
244
+ userEmail: user.email || '',
245
+ action: 'confirm',
246
+ result,
247
+ fileName: `confirmation-${confirmationId}`,
248
+ fileType: 'confirmation-data',
249
+ validationErrors: errors,
250
+ caseNumber,
251
+ confirmationId,
252
+ workflowPhase: 'confirmation',
253
+ securityChecks,
254
+ performanceMetrics,
255
+ originalExaminerUid,
256
+ reviewingExaminerUid: user.uid,
257
+ fileDetails: imageFileId && originalImageFileName ? {
258
+ fileId: imageFileId,
259
+ originalFileName: originalImageFileName,
260
+ fileSize: 0 // Not applicable for confirmation creation
261
+ } : undefined
262
+ });
263
+ }
264
+
265
+ /**
266
+ * Log confirmation export event
267
+ */
268
+ public async logConfirmationExport(
269
+ user: User,
270
+ caseNumber: string,
271
+ fileName: string,
272
+ confirmationCount: number,
273
+ result: AuditResult,
274
+ errors: string[] = [],
275
+ originalExaminerUid?: string,
276
+ performanceMetrics?: PerformanceMetrics,
277
+ signatureDetails?: {
278
+ present: boolean;
279
+ valid: boolean;
280
+ keyId?: string;
281
+ }
282
+ ): Promise<void> {
283
+ const securityChecks: SecurityCheckResults = {
284
+ selfConfirmationPrevented: false, // Not applicable for exports
285
+ fileIntegrityValid: result === 'success',
286
+ manifestSignaturePresent: signatureDetails?.present,
287
+ manifestSignatureValid: signatureDetails?.valid,
288
+ manifestSignatureKeyId: signatureDetails?.keyId
289
+ };
290
+
291
+ await this.logEvent({
292
+ userId: user.uid,
293
+ userEmail: user.email || '',
294
+ action: 'export',
295
+ result,
296
+ fileName,
297
+ fileType: 'confirmation-data',
298
+ validationErrors: errors,
299
+ caseNumber,
300
+ workflowPhase: 'confirmation',
301
+ securityChecks,
302
+ performanceMetrics,
303
+ originalExaminerUid,
304
+ reviewingExaminerUid: user.uid
305
+ });
306
+ }
307
+
308
+ /**
309
+ * Log confirmation import event
310
+ */
311
+ public async logConfirmationImport(
312
+ user: User,
313
+ caseNumber: string,
314
+ fileName: string,
315
+ result: AuditResult,
316
+ hashValid: boolean,
317
+ confirmationsImported: number,
318
+ errors: string[] = [],
319
+ reviewingExaminerUid?: string,
320
+ performanceMetrics?: PerformanceMetrics,
321
+ exporterUidValidated?: boolean, // Separate flag for validation status
322
+ totalConfirmationsInFile?: number, // Total confirmations in the import file
323
+ signatureDetails?: {
324
+ present: boolean;
325
+ valid: boolean;
326
+ keyId?: string;
327
+ }
328
+ ): Promise<void> {
329
+ const securityChecks: SecurityCheckResults = {
330
+ selfConfirmationPrevented: reviewingExaminerUid ? reviewingExaminerUid === user.uid : false,
331
+ fileIntegrityValid: hashValid,
332
+ exporterUidValidated: exporterUidValidated !== undefined ? exporterUidValidated : !!reviewingExaminerUid,
333
+ manifestSignaturePresent: signatureDetails?.present,
334
+ manifestSignatureValid: signatureDetails?.valid,
335
+ manifestSignatureKeyId: signatureDetails?.keyId
336
+ };
337
+
338
+ await this.logEvent({
339
+ userId: user.uid,
340
+ userEmail: user.email || '',
341
+ action: 'import',
342
+ result,
343
+ fileName,
344
+ fileType: 'confirmation-data',
345
+ hashValid,
346
+ validationErrors: errors,
347
+ caseNumber,
348
+ workflowPhase: 'confirmation',
349
+ securityChecks,
350
+ performanceMetrics: performanceMetrics ? {
351
+ ...performanceMetrics,
352
+ validationStepsCompleted: confirmationsImported, // Successfully imported
353
+ validationStepsFailed: errors.length
354
+ } : {
355
+ processingTimeMs: 0,
356
+ fileSizeBytes: 0,
357
+ validationStepsCompleted: confirmationsImported, // Successfully imported
358
+ validationStepsFailed: errors.length
359
+ },
360
+ originalExaminerUid: user.uid,
361
+ reviewingExaminerUid: reviewingExaminerUid, // Pass through the reviewing examiner UID
362
+ // Store total confirmations in file using caseDetails
363
+ caseDetails: totalConfirmationsInFile !== undefined ? {
364
+ totalAnnotations: totalConfirmationsInFile // Total confirmations in the import file
365
+ } : undefined
366
+ });
367
+ }
368
+
369
+ // =============================================================================
370
+ // COMPREHENSIVE AUDIT LOGGING METHODS
371
+ // =============================================================================
372
+
373
+ /**
374
+ * Log case creation event
375
+ */
376
+ public async logCaseCreation(
377
+ user: User,
378
+ caseNumber: string,
379
+ caseName: string
380
+ ): Promise<void> {
381
+ await this.logEvent({
382
+ userId: user.uid,
383
+ userEmail: user.email || '',
384
+ action: 'case-create',
385
+ result: 'success',
386
+ fileName: `${caseNumber}.case`,
387
+ fileType: 'case-package',
388
+ validationErrors: [],
389
+ caseNumber,
390
+ workflowPhase: 'casework',
391
+ caseDetails: {
392
+ newCaseName: caseName,
393
+ createdDate: new Date().toISOString(),
394
+ totalFiles: 0,
395
+ totalAnnotations: 0
396
+ }
397
+ });
398
+ }
399
+
400
+ /**
401
+ * Log case rename event
402
+ */
403
+ public async logCaseRename(
404
+ user: User,
405
+ caseNumber: string,
406
+ oldName: string,
407
+ newName: string
408
+ ): Promise<void> {
409
+ await this.logEvent({
410
+ userId: user.uid,
411
+ userEmail: user.email || '',
412
+ action: 'case-rename',
413
+ result: 'success',
414
+ fileName: `${caseNumber}.case`,
415
+ fileType: 'case-package',
416
+ validationErrors: [],
417
+ caseNumber,
418
+ workflowPhase: 'casework',
419
+ caseDetails: {
420
+ oldCaseName: oldName,
421
+ newCaseName: newName,
422
+ lastModified: new Date().toISOString()
423
+ }
424
+ });
425
+ }
426
+
427
+ /**
428
+ * Log case deletion event
429
+ */
430
+ public async logCaseDeletion(
431
+ user: User,
432
+ caseNumber: string,
433
+ caseName: string,
434
+ deleteReason: string,
435
+ backupCreated: boolean = false
436
+ ): Promise<void> {
437
+ await this.logEvent({
438
+ userId: user.uid,
439
+ userEmail: user.email || '',
440
+ action: 'case-delete',
441
+ result: 'success',
442
+ fileName: `${caseNumber}.case`,
443
+ fileType: 'case-package',
444
+ validationErrors: [],
445
+ caseNumber,
446
+ workflowPhase: 'casework',
447
+ caseDetails: {
448
+ newCaseName: caseName,
449
+ deleteReason,
450
+ backupCreated,
451
+ lastModified: new Date().toISOString()
452
+ }
453
+ });
454
+ }
455
+
456
+ /**
457
+ * Log file upload event
458
+ */
459
+ public async logFileUpload(
460
+ user: User,
461
+ fileName: string,
462
+ fileSize: number,
463
+ mimeType: string,
464
+ uploadMethod: 'drag-drop' | 'file-picker' | 'api' | 'import',
465
+ caseNumber: string,
466
+ result: AuditResult = 'success',
467
+ processingTime?: number,
468
+ fileId?: string
469
+ ): Promise<void> {
470
+ await this.logEvent({
471
+ userId: user.uid,
472
+ userEmail: user.email || '',
473
+ action: 'file-upload',
474
+ result,
475
+ fileName,
476
+ fileType: this.getFileTypeFromMime(mimeType),
477
+ validationErrors: [],
478
+ caseNumber,
479
+ workflowPhase: 'casework',
480
+ fileDetails: {
481
+ fileId: fileId || undefined,
482
+ originalFileName: fileName,
483
+ fileSize,
484
+ mimeType,
485
+ uploadMethod,
486
+ processingTime,
487
+ thumbnailGenerated: result === 'success' && this.isImageFile(mimeType)
488
+ },
489
+ performanceMetrics: processingTime ? {
490
+ processingTimeMs: processingTime,
491
+ fileSizeBytes: fileSize
492
+ } : undefined
493
+ });
494
+ }
495
+
496
+ /**
497
+ * Log file deletion event
498
+ */
499
+ public async logFileDeletion(
500
+ user: User,
501
+ fileName: string,
502
+ fileSize: number,
503
+ deleteReason: string,
504
+ caseNumber: string,
505
+ fileId?: string,
506
+ originalFileName?: string
507
+ ): Promise<void> {
508
+ await this.logEvent({
509
+ userId: user.uid,
510
+ userEmail: user.email || '',
511
+ action: 'file-delete',
512
+ result: 'success',
513
+ fileName,
514
+ fileType: 'unknown',
515
+ validationErrors: [],
516
+ caseNumber,
517
+ workflowPhase: 'casework',
518
+ fileDetails: {
519
+ fileId: fileId || undefined,
520
+ originalFileName,
521
+ fileSize,
522
+ deleteReason
523
+ }
524
+ });
525
+ }
526
+
527
+ /**
528
+ * Log file access event (e.g., viewing an image)
529
+ */
530
+ public async logFileAccess(
531
+ user: User,
532
+ fileName: string,
533
+ fileId: string,
534
+ accessMethod: 'direct-url' | 'signed-url' | 'download',
535
+ caseNumber: string,
536
+ result: AuditResult = 'success',
537
+ processingTime?: number,
538
+ accessReason?: string,
539
+ originalFileName?: string
540
+ ): Promise<void> {
541
+ await this.logEvent({
542
+ userId: user.uid,
543
+ userEmail: user.email || '',
544
+ action: 'file-access',
545
+ result,
546
+ fileName,
547
+ fileType: 'image-file', // Most file access in Striae is for images
548
+ validationErrors: result === 'failure' ? ['File access failed'] : [],
549
+ caseNumber,
550
+ workflowPhase: 'casework',
551
+ fileDetails: {
552
+ fileId,
553
+ originalFileName,
554
+ fileSize: 0, // File size not available for access events
555
+ uploadMethod: accessMethod as any, // Reuse for access method
556
+ processingTime,
557
+ sourceLocation: accessReason || 'Image viewer'
558
+ },
559
+ performanceMetrics: processingTime ? {
560
+ processingTimeMs: processingTime,
561
+ fileSizeBytes: 0
562
+ } : undefined
563
+ });
564
+ }
565
+
566
+ /**
567
+ * Log annotation creation event
568
+ */
569
+ public async logAnnotationCreate(
570
+ user: User,
571
+ annotationId: string,
572
+ annotationType: 'measurement' | 'identification' | 'comparison' | 'note' | 'region',
573
+ annotationData: any,
574
+ caseNumber: string,
575
+ tool?: string,
576
+ imageFileId?: string,
577
+ originalImageFileName?: string
578
+ ): Promise<void> {
579
+ await this.logEvent({
580
+ userId: user.uid,
581
+ userEmail: user.email || '',
582
+ action: 'annotation-create',
583
+ result: 'success',
584
+ fileName: `annotation-${annotationId}.json`,
585
+ fileType: 'json-data',
586
+ validationErrors: [],
587
+ caseNumber,
588
+ workflowPhase: 'casework',
589
+ annotationDetails: {
590
+ annotationId,
591
+ annotationType,
592
+ annotationData,
593
+ tool,
594
+ canvasPosition: annotationData?.position,
595
+ annotationSize: annotationData?.size
596
+ },
597
+ fileDetails: imageFileId || originalImageFileName ? {
598
+ fileId: imageFileId,
599
+ originalFileName: originalImageFileName,
600
+ fileSize: 0, // Not available for image annotations
601
+ mimeType: 'image/*', // Generic image type
602
+ uploadMethod: 'api'
603
+ } : undefined
604
+ });
605
+ }
606
+
607
+ /**
608
+ * Log annotation edit event
609
+ */
610
+ public async logAnnotationEdit(
611
+ user: User,
612
+ annotationId: string,
613
+ previousValue: any,
614
+ newValue: any,
615
+ caseNumber: string,
616
+ tool?: string,
617
+ imageFileId?: string,
618
+ originalImageFileName?: string
619
+ ): Promise<void> {
620
+ await this.logEvent({
621
+ userId: user.uid,
622
+ userEmail: user.email || '',
623
+ action: 'annotation-edit',
624
+ result: 'success',
625
+ fileName: `annotation-${annotationId}.json`,
626
+ fileType: 'json-data',
627
+ validationErrors: [],
628
+ caseNumber,
629
+ workflowPhase: 'casework',
630
+ annotationDetails: {
631
+ annotationId,
632
+ annotationType: newValue?.type,
633
+ annotationData: newValue,
634
+ previousValue,
635
+ tool
636
+ },
637
+ fileDetails: imageFileId || originalImageFileName ? {
638
+ fileId: imageFileId,
639
+ originalFileName: originalImageFileName,
640
+ fileSize: 0, // Not available for image annotations
641
+ mimeType: 'image/*', // Generic image type
642
+ uploadMethod: 'api'
643
+ } : undefined
644
+ });
645
+ }
646
+
647
+ /**
648
+ * Log annotation deletion event
649
+ */
650
+ public async logAnnotationDelete(
651
+ user: User,
652
+ annotationId: string,
653
+ annotationData: any,
654
+ caseNumber: string,
655
+ deleteReason?: string,
656
+ imageFileId?: string,
657
+ originalImageFileName?: string
658
+ ): Promise<void> {
659
+ await this.logEvent({
660
+ userId: user.uid,
661
+ userEmail: user.email || '',
662
+ action: 'annotation-delete',
663
+ result: 'success',
664
+ fileName: `annotation-${annotationId}.json`,
665
+ fileType: 'json-data',
666
+ validationErrors: [],
667
+ caseNumber,
668
+ workflowPhase: 'casework',
669
+ annotationDetails: {
670
+ annotationId,
671
+ annotationType: annotationData?.type,
672
+ annotationData,
673
+ tool: deleteReason
674
+ },
675
+ fileDetails: imageFileId || originalImageFileName ? {
676
+ fileId: imageFileId,
677
+ originalFileName: originalImageFileName,
678
+ fileSize: 0, // Not available for image annotations
679
+ mimeType: 'image/*', // Generic image type
680
+ uploadMethod: 'api'
681
+ } : undefined
682
+ });
683
+ }
684
+
685
+ /**
686
+ * Log user login event
687
+ */
688
+ public async logUserLogin(
689
+ user: User,
690
+ sessionId: string,
691
+ loginMethod: 'firebase' | 'sso' | 'api-key' | 'manual',
692
+ userAgent?: string
693
+ ): Promise<void> {
694
+ await this.logEvent({
695
+ userId: user.uid,
696
+ userEmail: user.email || '',
697
+ action: 'user-login',
698
+ result: 'success',
699
+ fileName: `session-${sessionId}.log`,
700
+ fileType: 'log-file',
701
+ validationErrors: [],
702
+ workflowPhase: 'user-management',
703
+ sessionDetails: {
704
+ sessionId,
705
+ userAgent,
706
+ loginMethod
707
+ }
708
+ });
709
+ }
710
+
711
+ /**
712
+ * Log user logout event
713
+ */
714
+ public async logUserLogout(
715
+ user: User,
716
+ sessionId: string,
717
+ sessionDuration: number,
718
+ logoutReason: 'user-initiated' | 'timeout' | 'security' | 'error'
719
+ ): Promise<void> {
720
+ await this.logEvent({
721
+ userId: user.uid,
722
+ userEmail: user.email || '',
723
+ action: 'user-logout',
724
+ result: 'success',
725
+ fileName: `session-${sessionId}.log`,
726
+ fileType: 'log-file',
727
+ validationErrors: [],
728
+ workflowPhase: 'user-management',
729
+ sessionDetails: {
730
+ sessionId,
731
+ sessionDuration,
732
+ logoutReason
733
+ }
734
+ });
735
+ }
736
+
737
+ /**
738
+ * Log user profile update event
739
+ */
740
+ public async logUserProfileUpdate(
741
+ user: User,
742
+ profileField: 'displayName' | 'email' | 'organization' | 'role' | 'preferences' | 'avatar',
743
+ oldValue: string,
744
+ newValue: string,
745
+ result: AuditResult,
746
+ sessionId?: string,
747
+ errors: string[] = []
748
+ ): Promise<void> {
749
+ await this.logEvent({
750
+ userId: user.uid,
751
+ userEmail: user.email || '',
752
+ action: 'user-profile-update',
753
+ result,
754
+ fileName: `profile-update-${profileField}.log`,
755
+ fileType: 'log-file',
756
+ validationErrors: errors,
757
+ workflowPhase: 'user-management',
758
+ sessionDetails: sessionId ? {
759
+ sessionId
760
+ } : undefined,
761
+ userProfileDetails: {
762
+ profileField,
763
+ oldValue,
764
+ newValue
765
+ }
766
+ });
767
+ }
768
+
769
+ /**
770
+ * Log password reset event
771
+ */
772
+ public async logPasswordReset(
773
+ userEmail: string,
774
+ resetMethod: 'email' | 'sms' | 'security-questions' | 'admin-reset',
775
+ result: AuditResult,
776
+ resetToken?: string,
777
+ verificationMethod?: 'email-link' | 'sms-code' | 'totp' | 'backup-codes',
778
+ verificationAttempts?: number,
779
+ passwordComplexityMet?: boolean,
780
+ previousPasswordReused?: boolean,
781
+ sessionId?: string,
782
+ errors: string[] = []
783
+ ): Promise<void> {
784
+ // For password resets, we might not have the full user object yet
785
+ const userId = ''; // No user ID available during password reset
786
+
787
+ await this.logEvent({
788
+ userId,
789
+ userEmail,
790
+ action: 'user-password-reset',
791
+ result,
792
+ fileName: `password-reset-${resetMethod}.log`,
793
+ fileType: 'log-file',
794
+ validationErrors: errors,
795
+ workflowPhase: 'user-management',
796
+ sessionDetails: sessionId ? {
797
+ sessionId
798
+ } : undefined,
799
+ userProfileDetails: {
800
+ resetMethod,
801
+ resetToken: resetToken ? `***${resetToken.slice(-4)}` : undefined, // Only store last 4 chars
802
+ verificationMethod,
803
+ verificationAttempts,
804
+ passwordComplexityMet,
805
+ previousPasswordReused
806
+ }
807
+ });
808
+ }
809
+
810
+ /**
811
+ * Log user account deletion event
812
+ */
813
+ public async logAccountDeletion(
814
+ user: User,
815
+ result: AuditResult,
816
+ deletionReason: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account' = 'user-requested',
817
+ confirmationMethod: 'uid-email' | 'password' | 'admin-override' = 'uid-email',
818
+ casesCount?: number,
819
+ filesCount?: number,
820
+ dataRetentionPeriod?: number,
821
+ emailNotificationSent?: boolean,
822
+ sessionId?: string,
823
+ errors: string[] = []
824
+ ): Promise<void> {
825
+ // Wrapper that extracts user data and calls the simplified version
826
+ return this.logAccountDeletionSimple(
827
+ user.uid,
828
+ user.email || '',
829
+ result,
830
+ deletionReason,
831
+ confirmationMethod,
832
+ casesCount,
833
+ filesCount,
834
+ dataRetentionPeriod,
835
+ emailNotificationSent,
836
+ sessionId,
837
+ errors
838
+ );
839
+ }
840
+
841
+ /**
842
+ * Log user account deletion event with simplified user data
843
+ */
844
+ public async logAccountDeletionSimple(
845
+ userId: string,
846
+ userEmail: string,
847
+ result: AuditResult,
848
+ deletionReason: 'user-requested' | 'admin-initiated' | 'policy-violation' | 'inactive-account' = 'user-requested',
849
+ confirmationMethod: 'uid-email' | 'password' | 'admin-override' = 'uid-email',
850
+ casesCount?: number,
851
+ filesCount?: number,
852
+ dataRetentionPeriod?: number,
853
+ emailNotificationSent?: boolean,
854
+ sessionId?: string,
855
+ errors: string[] = []
856
+ ): Promise<void> {
857
+ await this.logEvent({
858
+ userId,
859
+ userEmail: userEmail || '',
860
+ action: 'user-account-delete',
861
+ result,
862
+ fileName: `account-deletion-${userId}.log`,
863
+ fileType: 'log-file',
864
+ validationErrors: errors,
865
+ workflowPhase: 'user-management',
866
+ sessionDetails: sessionId ? {
867
+ sessionId,
868
+ } : undefined,
869
+ userProfileDetails: {
870
+ deletionReason,
871
+ confirmationMethod,
872
+ casesCount,
873
+ filesCount,
874
+ dataRetentionPeriod,
875
+ emailNotificationSent
876
+ }
877
+ });
878
+ }
879
+
880
+ /**
881
+ * Log user registration/creation event
882
+ */
883
+ public async logUserRegistration(
884
+ user: User,
885
+ firstName: string,
886
+ lastName: string,
887
+ company: string,
888
+ registrationMethod: 'email-password' | 'sso' | 'admin-created' | 'api',
889
+ userAgent?: string,
890
+ sessionId?: string
891
+ ): Promise<void> {
892
+ await this.logEvent({
893
+ userId: user.uid,
894
+ userEmail: user.email || '',
895
+ action: 'user-registration',
896
+ result: 'success',
897
+ fileName: `registration-${user.uid}.log`,
898
+ fileType: 'log-file',
899
+ validationErrors: [],
900
+ workflowPhase: 'user-management',
901
+ sessionDetails: sessionId ? {
902
+ sessionId,
903
+ userAgent
904
+ } : { userAgent },
905
+ userProfileDetails: {
906
+ registrationMethod,
907
+ firstName,
908
+ lastName,
909
+ company,
910
+ emailVerificationRequired: true,
911
+ mfaEnrollmentRequired: true
912
+ }
913
+ });
914
+ }
915
+
916
+ /**
917
+ * Log successful MFA enrollment event
918
+ */
919
+ public async logMfaEnrollment(
920
+ user: User,
921
+ phoneNumber: string,
922
+ mfaMethod: 'sms' | 'totp' | 'hardware-key',
923
+ result: AuditResult,
924
+ enrollmentAttempts?: number,
925
+ sessionId?: string,
926
+ userAgent?: string,
927
+ errors: string[] = []
928
+ ): Promise<void> {
929
+ // Mask phone number for privacy (show only last 4 digits)
930
+ const maskedPhone = phoneNumber.length > 4
931
+ ? `***-***-${phoneNumber.slice(-4)}`
932
+ : '***-***-****';
933
+
934
+ await this.logEvent({
935
+ userId: user.uid,
936
+ userEmail: user.email || '',
937
+ action: 'mfa-enrollment',
938
+ result,
939
+ fileName: `mfa-enrollment-${user.uid}.log`,
940
+ fileType: 'log-file',
941
+ validationErrors: errors,
942
+ workflowPhase: 'user-management',
943
+ sessionDetails: sessionId ? {
944
+ sessionId,
945
+ userAgent
946
+ } : { userAgent },
947
+ securityDetails: {
948
+ mfaMethod,
949
+ phoneNumber: maskedPhone,
950
+ enrollmentAttempts,
951
+ enrollmentDate: new Date().toISOString(),
952
+ mandatoryEnrollment: true,
953
+ backupCodesGenerated: false // SMS doesn't generate backup codes
954
+ }
955
+ });
956
+ }
957
+
958
+ /**
959
+ * Log MFA authentication/verification event
960
+ */
961
+ public async logMfaAuthentication(
962
+ user: User,
963
+ mfaMethod: 'sms' | 'totp' | 'hardware-key',
964
+ result: AuditResult,
965
+ verificationAttempts?: number,
966
+ sessionId?: string,
967
+ userAgent?: string,
968
+ errors: string[] = []
969
+ ): Promise<void> {
970
+ await this.logEvent({
971
+ userId: user.uid,
972
+ userEmail: user.email || '',
973
+ action: 'mfa-authentication',
974
+ result,
975
+ fileName: `mfa-auth-${sessionId || Date.now()}.log`,
976
+ fileType: 'log-file',
977
+ validationErrors: errors,
978
+ workflowPhase: 'user-management',
979
+ sessionDetails: sessionId ? {
980
+ sessionId,
981
+ userAgent
982
+ } : { userAgent },
983
+ securityDetails: {
984
+ mfaMethod,
985
+ verificationAttempts,
986
+ authenticationDate: new Date().toISOString(),
987
+ loginFlowStep: 'second-factor'
988
+ }
989
+ });
990
+ }
991
+
992
+ /**
993
+ * Log email verification event
994
+ */
995
+ public async logEmailVerification(
996
+ user: User,
997
+ result: AuditResult,
998
+ verificationMethod: 'email-link' | 'admin-verification',
999
+ verificationAttempts?: number,
1000
+ sessionId?: string,
1001
+ userAgent?: string,
1002
+ errors: string[] = []
1003
+ ): Promise<void> {
1004
+ await this.logEvent({
1005
+ userId: user.uid,
1006
+ userEmail: user.email || '',
1007
+ action: 'email-verification',
1008
+ result,
1009
+ fileName: `email-verification-${user.uid}.log`,
1010
+ fileType: 'log-file',
1011
+ validationErrors: errors,
1012
+ workflowPhase: 'user-management',
1013
+ sessionDetails: sessionId ? {
1014
+ sessionId,
1015
+ userAgent
1016
+ } : { userAgent },
1017
+ userProfileDetails: {
1018
+ verificationMethod,
1019
+ verificationAttempts,
1020
+ verificationDate: new Date().toISOString(),
1021
+ emailVerified: result === 'success'
1022
+ }
1023
+ });
1024
+ }
1025
+
1026
+ /**
1027
+ * Log email verification event when no authenticated User object is available.
1028
+ */
1029
+ public async logEmailVerificationByEmail(
1030
+ userEmail: string,
1031
+ result: AuditResult,
1032
+ verificationMethod: 'email-link' | 'admin-verification',
1033
+ verificationAttempts?: number,
1034
+ sessionId?: string,
1035
+ userAgent?: string,
1036
+ errors: string[] = [],
1037
+ userId: string = ''
1038
+ ): Promise<void> {
1039
+ await this.logEvent({
1040
+ userId,
1041
+ userEmail,
1042
+ action: 'email-verification',
1043
+ result,
1044
+ fileName: `email-verification-${userId || Date.now()}.log`,
1045
+ fileType: 'log-file',
1046
+ validationErrors: errors,
1047
+ workflowPhase: 'user-management',
1048
+ sessionDetails: sessionId ? {
1049
+ sessionId,
1050
+ userAgent
1051
+ } : { userAgent },
1052
+ userProfileDetails: {
1053
+ verificationMethod,
1054
+ verificationAttempts,
1055
+ verificationDate: new Date().toISOString(),
1056
+ emailVerified: result === 'success'
1057
+ }
1058
+ });
1059
+ }
1060
+
1061
+ /**
1062
+ * Mark pending email verification as successful (retroactive)
1063
+ * Called when user completes MFA enrollment, which implies email verification was successful
1064
+ */
1065
+ public async markEmailVerificationSuccessful(
1066
+ user: User,
1067
+ reason: string = 'MFA enrollment completed',
1068
+ sessionId?: string,
1069
+ userAgent?: string
1070
+ ): Promise<void> {
1071
+ await this.logEvent({
1072
+ userId: user.uid,
1073
+ userEmail: user.email || '',
1074
+ action: 'email-verification',
1075
+ result: 'success',
1076
+ fileName: `email-verification-${user.uid}.log`,
1077
+ fileType: 'log-file',
1078
+ validationErrors: [],
1079
+ workflowPhase: 'user-management',
1080
+ sessionDetails: sessionId ? {
1081
+ sessionId,
1082
+ userAgent
1083
+ } : { userAgent },
1084
+ userProfileDetails: {
1085
+ verificationMethod: 'email-link',
1086
+ verificationAttempts: 1,
1087
+ verificationDate: new Date().toISOString(),
1088
+ emailVerified: true,
1089
+ retroactiveVerification: true,
1090
+ retroactiveReason: reason
1091
+ }
1092
+ });
1093
+ }
1094
+
1095
+ /**
1096
+ * Log PDF generation event
1097
+ */
1098
+ public async logPDFGeneration(
1099
+ user: User,
1100
+ fileName: string,
1101
+ caseNumber: string,
1102
+ result: AuditResult,
1103
+ processingTime: number,
1104
+ fileSize?: number,
1105
+ errors: string[] = [],
1106
+ sourceFileId?: string,
1107
+ sourceFileName?: string
1108
+ ): Promise<void> {
1109
+ await this.logEvent({
1110
+ userId: user.uid,
1111
+ userEmail: user.email || '',
1112
+ action: 'pdf-generate',
1113
+ result,
1114
+ fileName,
1115
+ fileType: 'pdf-document',
1116
+ validationErrors: errors,
1117
+ caseNumber,
1118
+ workflowPhase: 'casework',
1119
+ performanceMetrics: {
1120
+ processingTimeMs: processingTime,
1121
+ fileSizeBytes: fileSize || 0
1122
+ },
1123
+ fileDetails: sourceFileId && sourceFileName ? {
1124
+ fileId: sourceFileId,
1125
+ originalFileName: sourceFileName,
1126
+ fileSize: 0 // PDF generation doesn't modify source file size
1127
+ } : undefined
1128
+ });
1129
+ }
1130
+
1131
+ /**
1132
+ * Log security violation event
1133
+ */
1134
+ public async logSecurityViolation(
1135
+ user: User | null,
1136
+ incidentType: 'unauthorized-access' | 'data-breach' | 'malware' | 'injection' | 'brute-force' | 'privilege-escalation',
1137
+ severity: 'low' | 'medium' | 'high' | 'critical',
1138
+ description: string,
1139
+ targetResource?: string,
1140
+ blockedBySystem: boolean = true
1141
+ ): Promise<void> {
1142
+ await this.logEvent({
1143
+ userId: user?.uid || 'unknown',
1144
+ userEmail: user?.email || 'unknown@system.com',
1145
+ action: 'security-violation',
1146
+ result: blockedBySystem ? 'blocked' : 'failure',
1147
+ fileName: `security-incident-${Date.now()}.log`,
1148
+ fileType: 'log-file',
1149
+ validationErrors: [description],
1150
+ securityDetails: {
1151
+ incidentType,
1152
+ severity,
1153
+ targetResource,
1154
+ blockedBySystem,
1155
+ investigationId: `INV-${Date.now()}`,
1156
+ reportedToAuthorities: severity === 'critical',
1157
+ mitigationSteps: [
1158
+ blockedBySystem ? 'Automatically blocked by system' : 'Manual intervention required'
1159
+ ]
1160
+ }
1161
+ });
1162
+ }
1163
+
1164
+ // =============================================================================
1165
+ // HELPER METHODS
1166
+ // =============================================================================
1167
+
1168
+ /**
1169
+ * Determine file type from MIME type
1170
+ */
1171
+ private getFileTypeFromMime(mimeType: string): AuditFileType {
1172
+ if (mimeType.startsWith('image/')) return 'image-file';
1173
+ if (mimeType === 'application/pdf') return 'pdf-document';
1174
+ if (mimeType === 'application/json') return 'json-data';
1175
+ if (mimeType === 'text/csv') return 'csv-export';
1176
+ return 'unknown';
1177
+ }
1178
+
1179
+ /**
1180
+ * Check if file is an image
1181
+ */
1182
+ private isImageFile(mimeType: string): boolean {
1183
+ return mimeType.startsWith('image/');
1184
+ }
1185
+
1186
+ /**
1187
+ * Get audit entries for display (public method for components)
1188
+ */
1189
+ public async getAuditEntriesForUser(userId: string, params?: {
1190
+ startDate?: string;
1191
+ endDate?: string;
1192
+ caseNumber?: string;
1193
+ action?: AuditAction;
1194
+ result?: AuditResult;
1195
+ workflowPhase?: WorkflowPhase;
1196
+ offset?: number;
1197
+ limit?: number;
1198
+ }): Promise<ValidationAuditEntry[]> {
1199
+ const queryParams: AuditQueryParams = {
1200
+ userId,
1201
+ ...params
1202
+ };
1203
+ return await this.getAuditEntries(queryParams);
1204
+ }
1205
+
1206
+ /**
1207
+ * Get audit trail for a case
1208
+ */
1209
+ public async getAuditTrail(caseNumber: string): Promise<AuditTrail | null> {
1210
+ try {
1211
+ // Implement retrieval from storage
1212
+ const entries = await this.getAuditEntries({ caseNumber });
1213
+ if (!entries || entries.length === 0) {
1214
+ return null;
1215
+ }
1216
+
1217
+ const summary = this.generateAuditSummary(entries);
1218
+ const workflowId = this.workflowId || `${caseNumber}-archived`;
1219
+
1220
+ return {
1221
+ caseNumber,
1222
+ workflowId,
1223
+ entries,
1224
+ summary
1225
+ };
1226
+ } catch (error) {
1227
+ console.error('🚨 Audit: Failed to get audit trail:', error);
1228
+ return null;
1229
+ }
1230
+ }
1231
+
1232
+ /**
1233
+ * Generate audit summary from entries
1234
+ */
1235
+ private generateAuditSummary(entries: ValidationAuditEntry[]): AuditSummary {
1236
+ const successCount = entries.filter(e => e.result === 'success').length;
1237
+ const failureCount = entries.filter(e => e.result === 'failure').length;
1238
+ const warningCount = entries.filter(e => e.result === 'warning').length;
1239
+
1240
+ const phases = [...new Set(entries
1241
+ .map(e => e.details.workflowPhase)
1242
+ .filter(Boolean))] as WorkflowPhase[];
1243
+
1244
+ const users = [...new Set(entries.map(e => e.userId))];
1245
+
1246
+ const timestamps = entries.map(e => e.timestamp).sort();
1247
+ const securityIncidents = entries.filter(e =>
1248
+ e.result === 'failure' &&
1249
+ (e.details.securityChecks?.selfConfirmationPrevented === true ||
1250
+ !e.details.securityChecks?.fileIntegrityValid)
1251
+ ).length;
1252
+
1253
+ return {
1254
+ totalEvents: entries.length,
1255
+ successfulEvents: successCount,
1256
+ failedEvents: failureCount,
1257
+ warningEvents: warningCount,
1258
+ workflowPhases: phases,
1259
+ participatingUsers: users,
1260
+ startTimestamp: timestamps[0] || new Date().toISOString(),
1261
+ endTimestamp: timestamps[timestamps.length - 1] || new Date().toISOString(),
1262
+ complianceStatus: failureCount === 0 ? 'compliant' : 'non-compliant',
1263
+ securityIncidents
1264
+ };
1265
+ }
1266
+
1267
+ /**
1268
+ * Get audit entries based on query parameters
1269
+ */
1270
+ private async getAuditEntries(params: AuditQueryParams): Promise<ValidationAuditEntry[]> {
1271
+ try {
1272
+ // If userId is provided, fetch from server
1273
+ if (params.userId) {
1274
+ const apiKey = await getDataApiKey();
1275
+ const url = new URL(`${AUDIT_WORKER_URL}/audit/`);
1276
+ url.searchParams.set('userId', params.userId);
1277
+
1278
+ if (params.startDate) {
1279
+ url.searchParams.set('startDate', params.startDate);
1280
+ }
1281
+
1282
+ if (params.endDate) {
1283
+ url.searchParams.set('endDate', params.endDate);
1284
+ }
1285
+
1286
+ const response = await fetch(url.toString(), {
1287
+ method: 'GET',
1288
+ headers: {
1289
+ 'X-Custom-Auth-Key': apiKey
1290
+ }
1291
+ });
1292
+
1293
+ if (response.ok) {
1294
+ const result = await response.json() as { entries: ValidationAuditEntry[]; total: number };
1295
+ let entries = result.entries;
1296
+
1297
+ // Apply client-side filters
1298
+ if (params.caseNumber) {
1299
+ entries = entries.filter(e => e.details.caseNumber === params.caseNumber);
1300
+ }
1301
+
1302
+ if (params.action) {
1303
+ entries = entries.filter(e => e.action === params.action);
1304
+ }
1305
+
1306
+ if (params.result) {
1307
+ entries = entries.filter(e => e.result === params.result);
1308
+ }
1309
+
1310
+ if (params.workflowPhase) {
1311
+ entries = entries.filter(e => e.details.workflowPhase === params.workflowPhase);
1312
+ }
1313
+
1314
+ // Apply pagination
1315
+ if (params.offset || params.limit) {
1316
+ const offset = params.offset || 0;
1317
+ const limit = params.limit || 100;
1318
+ entries = entries.slice(offset, offset + limit);
1319
+ }
1320
+
1321
+ return entries;
1322
+ } else {
1323
+ console.error('🚨 Audit: Failed to fetch entries from server');
1324
+ }
1325
+ }
1326
+
1327
+ // Fallback to buffer for backward compatibility
1328
+ let entries = [...this.auditBuffer];
1329
+
1330
+ if (params.caseNumber) {
1331
+ entries = entries.filter(e => e.details.caseNumber === params.caseNumber);
1332
+ }
1333
+
1334
+ if (params.userId) {
1335
+ entries = entries.filter(e => e.userId === params.userId);
1336
+ }
1337
+
1338
+ if (params.action) {
1339
+ entries = entries.filter(e => e.action === params.action);
1340
+ }
1341
+
1342
+ if (params.result) {
1343
+ entries = entries.filter(e => e.result === params.result);
1344
+ }
1345
+
1346
+ if (params.workflowPhase) {
1347
+ entries = entries.filter(e => e.details.workflowPhase === params.workflowPhase);
1348
+ }
1349
+
1350
+ // Sort by timestamp (newest first)
1351
+ entries.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
1352
+
1353
+ // Apply pagination
1354
+ if (params.offset || params.limit) {
1355
+ const offset = params.offset || 0;
1356
+ const limit = params.limit || 100;
1357
+ entries = entries.slice(offset, offset + limit);
1358
+ }
1359
+
1360
+ return entries;
1361
+ } catch (error) {
1362
+ console.error('🚨 Audit: Failed to get audit entries:', error);
1363
+ return [];
1364
+ }
1365
+ }
1366
+
1367
+ /**
1368
+ * Persist audit entry to storage
1369
+ */
1370
+ private async persistAuditEntry(entry: ValidationAuditEntry): Promise<void> {
1371
+ try {
1372
+ // Store to audit worker
1373
+ const apiKey = await getDataApiKey();
1374
+ const url = new URL(`${AUDIT_WORKER_URL}/audit/`);
1375
+ url.searchParams.set('userId', entry.userId);
1376
+
1377
+ const response = await fetch(url.toString(), {
1378
+ method: 'POST',
1379
+ headers: {
1380
+ 'Content-Type': 'application/json',
1381
+ 'X-Custom-Auth-Key': apiKey
1382
+ },
1383
+ body: JSON.stringify(entry)
1384
+ });
1385
+
1386
+ if (!response.ok) {
1387
+ const errorData = await response.json().catch(() => ({}));
1388
+ console.error('🚨 Audit: Failed to persist entry:', response.status, errorData);
1389
+ } else {
1390
+ const result = await response.json() as { success: boolean; entryCount: number; filename: string };
1391
+ console.log(`🔍 Audit: Entry persisted (${result.entryCount} total entries)`);
1392
+ }
1393
+ } catch (error) {
1394
+ console.error('🚨 Audit: Storage error:', error);
1395
+ }
1396
+ }
1397
+
1398
+ /**
1399
+ * Log audit entry to console for development
1400
+ */
1401
+ private logToConsole(entry: ValidationAuditEntry): void {
1402
+ const icon = entry.result === 'success' ? '✅' :
1403
+ entry.result === 'failure' ? '❌' : '⚠️';
1404
+
1405
+ console.log(
1406
+ `${icon} Audit [${entry.action.toUpperCase()}]: ${entry.details.fileName} ` +
1407
+ `(Case: ${entry.details.caseNumber || 'N/A'}) - ${entry.result.toUpperCase()}`
1408
+ );
1409
+
1410
+ if (entry.details.validationErrors.length > 0) {
1411
+ console.log(' Errors:', entry.details.validationErrors);
1412
+ }
1413
+
1414
+ if (entry.details.securityChecks) {
1415
+ const securityIssues = [];
1416
+
1417
+ // selfConfirmationPrevented: Only check for import actions when self-confirmation was actually prevented
1418
+ if (entry.action === 'import' && entry.details.securityChecks.selfConfirmationPrevented === true) {
1419
+ securityIssues.push('selfConfirmationPrevented');
1420
+ }
1421
+
1422
+ // fileIntegrityValid: false means issue (integrity failed)
1423
+ if (entry.details.securityChecks.fileIntegrityValid === false) {
1424
+ securityIssues.push('fileIntegrityValid');
1425
+ }
1426
+
1427
+ // exporterUidValidated: false means issue (validation failed)
1428
+ if (entry.details.securityChecks.exporterUidValidated === false) {
1429
+ securityIssues.push('exporterUidValidated');
1430
+ }
1431
+
1432
+ if (securityIssues.length > 0) {
1433
+ console.warn(' Security Issues:', securityIssues);
1434
+ }
1435
+ }
1436
+ }
1437
+
1438
+ /**
1439
+ * Clear audit buffer (for testing)
1440
+ */
1441
+ public clearBuffer(): void {
1442
+ this.auditBuffer = [];
1443
+ }
1444
+
1445
+ /**
1446
+ * Get current buffer size (for monitoring)
1447
+ */
1448
+ public getBufferSize(): number {
1449
+ return this.auditBuffer.length;
1450
+ }
1451
+ }
1452
+
1453
+ // Export singleton instance
1454
+ export const auditService = AuditService.getInstance();