@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,755 @@
1
+ import { ValidationAuditEntry, AuditTrail } from '~/types';
2
+ import { User } from 'firebase/auth';
3
+ import { calculateSHA256Secure } from '~/utils/SHA256';
4
+ import { signAuditExportData } from '~/utils/data-operations';
5
+ import {
6
+ AUDIT_EXPORT_SIGNATURE_VERSION,
7
+ AuditExportFormat,
8
+ AuditExportSigningPayload,
9
+ AuditExportType
10
+ } from '~/utils/audit-export-signature';
11
+
12
+ interface AuditExportContext {
13
+ user: User;
14
+ scopeType: 'case' | 'user';
15
+ scopeIdentifier: string;
16
+ caseNumber?: string;
17
+ }
18
+
19
+ /**
20
+ * Audit Export Service
21
+ * Handles exporting audit trails to various formats for compliance and forensic analysis
22
+ */
23
+ export class AuditExportService {
24
+ private static instance: AuditExportService;
25
+
26
+ private constructor() {}
27
+
28
+ public static getInstance(): AuditExportService {
29
+ if (!AuditExportService.instance) {
30
+ AuditExportService.instance = new AuditExportService();
31
+ }
32
+ return AuditExportService.instance;
33
+ }
34
+
35
+ /**
36
+ * Export audit entries to CSV format
37
+ */
38
+ public async exportToCSV(
39
+ entries: ValidationAuditEntry[],
40
+ filename: string,
41
+ context: AuditExportContext
42
+ ): Promise<void> {
43
+ const headers = [
44
+ 'Timestamp',
45
+ 'User Email',
46
+ 'Action',
47
+ 'Result',
48
+ 'File Name',
49
+ 'File Type',
50
+ 'Case Number',
51
+ 'Confirmation ID',
52
+ 'Original Examiner UID',
53
+ 'Reviewing Examiner UID',
54
+ 'File ID',
55
+ 'Original Filename',
56
+ 'File Size (MB)',
57
+ 'MIME Type',
58
+ 'Upload Method',
59
+ 'Delete Reason',
60
+ 'Annotation ID',
61
+ 'Annotation Type',
62
+ 'Annotation Tool',
63
+ 'Session ID',
64
+ 'User Agent',
65
+ 'Processing Time (ms)',
66
+ 'Hash Valid',
67
+ 'Validation Errors',
68
+ 'Security Issues',
69
+ 'Workflow Phase',
70
+ 'Profile Field',
71
+ 'Old Value',
72
+ 'New Value',
73
+ 'Total Confirmations In File',
74
+ 'Confirmations Successfully Imported',
75
+ 'Validation Steps Failed',
76
+ 'Case Name',
77
+ 'Total Files',
78
+ 'MFA Method',
79
+ 'Security Incident Type',
80
+ 'Security Severity'
81
+ ];
82
+
83
+ const csvData = [
84
+ headers.join(','),
85
+ ...entries.map(entry => this.entryToCSVRow(entry))
86
+ ].join('\n');
87
+
88
+ const generatedAt = new Date().toISOString();
89
+ const hash = await calculateSHA256Secure(csvData);
90
+ const signaturePayload = await this.signAuditExport(
91
+ {
92
+ exportFormat: 'csv',
93
+ exportType: 'entries',
94
+ generatedAt,
95
+ totalEntries: entries.length,
96
+ hash: hash.toUpperCase()
97
+ },
98
+ context
99
+ );
100
+
101
+ // Add hash metadata header
102
+ const csvContent = [
103
+ `# Striae Audit Export - Generated: ${generatedAt}`,
104
+ `# Total Entries: ${entries.length}`,
105
+ `# SHA-256 Hash: ${hash.toUpperCase()}`,
106
+ `# Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}`,
107
+ `# Audit Signature: ${JSON.stringify(signaturePayload.signature)}`,
108
+ `# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)`,
109
+ '',
110
+ csvData
111
+ ].join('\n');
112
+
113
+ this.downloadFile(csvContent, filename, 'text/csv');
114
+ }
115
+
116
+ /**
117
+ * Export audit trail to detailed CSV with summary
118
+ */
119
+ public async exportAuditTrailToCSV(
120
+ auditTrail: AuditTrail,
121
+ filename: string,
122
+ context: AuditExportContext
123
+ ): Promise<void> {
124
+ const summaryHeaders = [
125
+ 'Case Number',
126
+ 'Workflow ID',
127
+ 'Total Events',
128
+ 'Successful Events',
129
+ 'Failed Events',
130
+ 'Warning Events',
131
+ 'Compliance Status',
132
+ 'Security Incidents',
133
+ 'Start Time',
134
+ 'End Time',
135
+ 'Participating Users'
136
+ ];
137
+
138
+ const summaryRow = [
139
+ auditTrail.caseNumber,
140
+ auditTrail.workflowId,
141
+ auditTrail.summary.totalEvents,
142
+ auditTrail.summary.successfulEvents,
143
+ auditTrail.summary.failedEvents,
144
+ auditTrail.summary.warningEvents,
145
+ auditTrail.summary.complianceStatus.toUpperCase(),
146
+ auditTrail.summary.securityIncidents,
147
+ auditTrail.summary.startTimestamp,
148
+ auditTrail.summary.endTimestamp,
149
+ auditTrail.summary.participatingUsers.join('; ')
150
+ ].join(',');
151
+
152
+ const entryHeaders = [
153
+ 'Timestamp',
154
+ 'User Email',
155
+ 'Action',
156
+ 'Result',
157
+ 'File Name',
158
+ 'File Type',
159
+ 'Case Number',
160
+ 'Confirmation ID',
161
+ 'Original Examiner UID',
162
+ 'Reviewing Examiner UID',
163
+ 'File ID',
164
+ 'Original Filename',
165
+ 'File Size (MB)',
166
+ 'MIME Type',
167
+ 'Upload Method',
168
+ 'Delete Reason',
169
+ 'Annotation ID',
170
+ 'Annotation Type',
171
+ 'Annotation Tool',
172
+ 'Session ID',
173
+ 'User Agent',
174
+ 'Processing Time (ms)',
175
+ 'Hash Valid',
176
+ 'Validation Errors',
177
+ 'Security Issues',
178
+ 'Workflow Phase',
179
+ 'Profile Field',
180
+ 'Old Value',
181
+ 'New Value',
182
+ 'Total Confirmations In File',
183
+ 'Confirmations Successfully Imported',
184
+ 'Validation Steps Failed',
185
+ 'Case Name',
186
+ 'Total Files',
187
+ 'MFA Method',
188
+ 'Security Incident Type',
189
+ 'Security Severity'
190
+ ];
191
+
192
+ const csvData = [
193
+ summaryHeaders.join(','),
194
+ summaryRow,
195
+ '',
196
+ entryHeaders.join(','),
197
+ ...auditTrail.entries.map(entry => this.entryToCSVRow(entry))
198
+ ].join('\n');
199
+
200
+ const generatedAt = new Date().toISOString();
201
+ const hash = await calculateSHA256Secure(csvData);
202
+ const signaturePayload = await this.signAuditExport(
203
+ {
204
+ exportFormat: 'csv',
205
+ exportType: 'trail',
206
+ generatedAt,
207
+ totalEntries: auditTrail.summary.totalEvents,
208
+ hash: hash.toUpperCase()
209
+ },
210
+ context
211
+ );
212
+
213
+ const csvContent = [
214
+ '# Striae Audit Trail Export - Generated: ' + generatedAt,
215
+ `# Case: ${auditTrail.caseNumber} | Workflow: ${auditTrail.workflowId}`,
216
+ `# Total Events: ${auditTrail.summary.totalEvents}`,
217
+ `# SHA-256 Hash: ${hash.toUpperCase()}`,
218
+ `# Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}`,
219
+ `# Audit Signature: ${JSON.stringify(signaturePayload.signature)}`,
220
+ '# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)',
221
+ '',
222
+ '# AUDIT TRAIL SUMMARY',
223
+ csvData
224
+ ].join('\n');
225
+
226
+ this.downloadFile(csvContent, filename, 'text/csv');
227
+ }
228
+
229
+ /**
230
+ * Convert audit entry to CSV row
231
+ */
232
+ private entryToCSVRow(entry: ValidationAuditEntry): string {
233
+ const fileDetails = entry.details.fileDetails;
234
+ const annotationDetails = entry.details.annotationDetails;
235
+ const sessionDetails = entry.details.sessionDetails;
236
+ const securityChecks = entry.details.securityChecks;
237
+ const userProfileDetails = entry.details.userProfileDetails;
238
+ const caseDetails = entry.details.caseDetails;
239
+ const performanceMetrics = entry.details.performanceMetrics;
240
+ const securityDetails = entry.details.securityDetails;
241
+
242
+ // Calculate security check status - different checks have different semantics
243
+ const securityIssues = securityChecks ? (() => {
244
+ const issues = [];
245
+
246
+ // selfConfirmationPrevented: true means issue (prevention was triggered)
247
+ if (securityChecks.selfConfirmationPrevented === true) {
248
+ issues.push('selfConfirmationPrevented');
249
+ }
250
+
251
+ // fileIntegrityValid: false means issue (file integrity failed)
252
+ if (securityChecks.fileIntegrityValid === false) {
253
+ issues.push('fileIntegrityValid');
254
+ }
255
+
256
+ // exporterUidValidated: false means issue (validation failed)
257
+ if (securityChecks.exporterUidValidated === false) {
258
+ issues.push('exporterUidValidated');
259
+ }
260
+
261
+ return issues.join('; ');
262
+ })() : '';
263
+
264
+ const values = [
265
+ this.formatForCSV(entry.timestamp),
266
+ this.formatForCSV(entry.userEmail),
267
+ this.formatForCSV(entry.action),
268
+ this.formatForCSV(entry.result),
269
+ this.formatForCSV(entry.details.fileName),
270
+ this.formatForCSV(entry.details.fileType),
271
+ this.formatForCSV(entry.details.caseNumber),
272
+ this.formatForCSV(entry.details.confirmationId),
273
+ this.formatForCSV(entry.details.originalExaminerUid),
274
+ this.formatForCSV(entry.details.reviewingExaminerUid),
275
+ this.formatForCSV(fileDetails?.fileId),
276
+ this.formatForCSV(fileDetails?.originalFileName),
277
+ fileDetails?.fileSize ? (fileDetails.fileSize / 1024 / 1024).toFixed(2) : '',
278
+ this.formatForCSV(fileDetails?.mimeType),
279
+ this.formatForCSV(fileDetails?.uploadMethod),
280
+ this.formatForCSV(fileDetails?.deleteReason),
281
+ this.formatForCSV(annotationDetails?.annotationId),
282
+ this.formatForCSV(annotationDetails?.annotationType),
283
+ this.formatForCSV(annotationDetails?.tool),
284
+ this.formatForCSV(sessionDetails?.sessionId),
285
+ this.formatForCSV(sessionDetails?.userAgent),
286
+ performanceMetrics?.processingTimeMs || '',
287
+ entry.details.hashValid !== undefined ?
288
+ (entry.details.hashValid ? 'Yes' : 'No') : '',
289
+ this.formatForCSV(entry.details.validationErrors?.join('; ')),
290
+ this.formatForCSV(securityIssues),
291
+ this.formatForCSV(entry.details.workflowPhase),
292
+ this.formatForCSV(userProfileDetails?.profileField),
293
+ this.formatForCSV(userProfileDetails?.oldValue),
294
+ this.formatForCSV(userProfileDetails?.newValue),
295
+ // New confirmation tracking fields
296
+ caseDetails?.totalAnnotations?.toString() || '', // Total confirmations in file
297
+ performanceMetrics?.validationStepsCompleted?.toString() || '', // Successfully imported
298
+ performanceMetrics?.validationStepsFailed?.toString() || '', // Validation steps failed
299
+ // Additional case and audit details
300
+ this.formatForCSV(caseDetails?.newCaseName || caseDetails?.oldCaseName), // Case name
301
+ caseDetails?.totalFiles?.toString() || '', // Total files in case
302
+ this.formatForCSV(securityDetails?.mfaMethod), // MFA method
303
+ this.formatForCSV(securityDetails?.incidentType), // Security incident type
304
+ this.formatForCSV(securityDetails?.severity) // Security severity
305
+ ];
306
+
307
+ return values.join(',');
308
+ }
309
+
310
+ /**
311
+ * Format value for CSV (handle quotes and commas)
312
+ */
313
+ private formatForCSV(value?: string | number | null): string {
314
+ if (value === undefined || value === null) return '';
315
+ const str = String(value);
316
+ // Escape quotes and wrap in quotes if contains comma, quote, or newline
317
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
318
+ return `"${str.replace(/"/g, '""')}"`;
319
+ }
320
+ return str;
321
+ }
322
+
323
+ /**
324
+ * Generate filename with timestamp
325
+ */
326
+ public generateFilename(type: 'case' | 'user', identifier: string, format: 'csv' | 'json'): string {
327
+ const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
328
+ const sanitizedId = identifier.replace(/[^a-zA-Z0-9-_]/g, '_');
329
+ return `striae-audit-${type}-${sanitizedId}-${timestamp}.${format}`;
330
+ }
331
+
332
+ /**
333
+ * Download file helper
334
+ */
335
+ private downloadFile(content: string, filename: string, mimeType: string): void {
336
+ const blob = new Blob([content], { type: mimeType });
337
+ const url = URL.createObjectURL(blob);
338
+
339
+ const link = document.createElement('a');
340
+ link.href = url;
341
+ link.download = filename;
342
+ link.style.display = 'none';
343
+
344
+ document.body.appendChild(link);
345
+ link.click();
346
+ document.body.removeChild(link);
347
+
348
+ // Clean up the URL object
349
+ setTimeout(() => URL.revokeObjectURL(url), 100);
350
+ }
351
+
352
+ /**
353
+ * Export audit entries to JSON format (for technical analysis)
354
+ */
355
+ public async exportToJSON(
356
+ entries: ValidationAuditEntry[],
357
+ filename: string,
358
+ context: AuditExportContext
359
+ ): Promise<void> {
360
+ const generatedAt = new Date().toISOString();
361
+
362
+ const exportData = {
363
+ metadata: {
364
+ exportTimestamp: generatedAt,
365
+ exportVersion: '1.0',
366
+ totalEntries: entries.length,
367
+ application: 'Striae',
368
+ exportType: 'entries' as AuditExportType,
369
+ scopeType: context.scopeType,
370
+ scopeIdentifier: context.scopeIdentifier
371
+ },
372
+ auditEntries: entries
373
+ };
374
+
375
+ const jsonContent = JSON.stringify(exportData, null, 2);
376
+
377
+ // Calculate hash for integrity verification
378
+ const hash = await calculateSHA256Secure(jsonContent);
379
+ const signaturePayload = await this.signAuditExport(
380
+ {
381
+ exportFormat: 'json',
382
+ exportType: exportData.metadata.exportType,
383
+ generatedAt,
384
+ totalEntries: entries.length,
385
+ hash: hash.toUpperCase()
386
+ },
387
+ context
388
+ );
389
+
390
+ // Create final export with hash included
391
+ const finalExportData = {
392
+ metadata: {
393
+ ...exportData.metadata,
394
+ hash: hash.toUpperCase(),
395
+ integrityNote: 'Verify hash and signature before trusting this export',
396
+ signatureVersion: signaturePayload.signatureMetadata.signatureVersion,
397
+ signature: signaturePayload.signature
398
+ },
399
+ auditEntries: entries
400
+ };
401
+
402
+ const finalJsonContent = JSON.stringify(finalExportData, null, 2);
403
+ this.downloadFile(finalJsonContent, filename.replace('.csv', '.json'), 'application/json');
404
+ }
405
+
406
+ /**
407
+ * Export full audit trail to JSON
408
+ */
409
+ public async exportAuditTrailToJSON(
410
+ auditTrail: AuditTrail,
411
+ filename: string,
412
+ context: AuditExportContext
413
+ ): Promise<void> {
414
+ const generatedAt = new Date().toISOString();
415
+
416
+ const exportData = {
417
+ metadata: {
418
+ exportTimestamp: generatedAt,
419
+ exportVersion: '1.0',
420
+ totalEntries: auditTrail.summary.totalEvents,
421
+ application: 'Striae',
422
+ exportType: 'trail' as AuditExportType,
423
+ scopeType: context.scopeType,
424
+ scopeIdentifier: context.scopeIdentifier
425
+ },
426
+ auditTrail
427
+ };
428
+
429
+ const jsonContent = JSON.stringify(exportData, null, 2);
430
+
431
+ // Calculate hash for integrity verification
432
+ const hash = await calculateSHA256Secure(jsonContent);
433
+ const signaturePayload = await this.signAuditExport(
434
+ {
435
+ exportFormat: 'json',
436
+ exportType: exportData.metadata.exportType,
437
+ generatedAt,
438
+ totalEntries: auditTrail.summary.totalEvents,
439
+ hash: hash.toUpperCase()
440
+ },
441
+ context
442
+ );
443
+
444
+ // Create final export with hash included
445
+ const finalExportData = {
446
+ metadata: {
447
+ ...exportData.metadata,
448
+ hash: hash.toUpperCase(),
449
+ integrityNote: 'Verify hash and signature before trusting this export',
450
+ signatureVersion: signaturePayload.signatureMetadata.signatureVersion,
451
+ signature: signaturePayload.signature
452
+ },
453
+ auditTrail
454
+ };
455
+
456
+ const finalJsonContent = JSON.stringify(finalExportData, null, 2);
457
+ this.downloadFile(finalJsonContent, filename.replace('.csv', '.json'), 'application/json');
458
+ }
459
+
460
+ /**
461
+ * Generate audit report summary text
462
+ */
463
+ public async generateReportSummary(auditTrail: AuditTrail, context: AuditExportContext): Promise<string> {
464
+ const summary = auditTrail.summary;
465
+ const successRate = ((summary.successfulEvents / summary.totalEvents) * 100).toFixed(1);
466
+ const generatedAt = new Date().toISOString();
467
+
468
+ const reportContent = `
469
+ STRIAE AUDIT TRAIL REPORT
470
+ ============================
471
+
472
+ Case Number: ${auditTrail.caseNumber}
473
+ Workflow ID: ${auditTrail.workflowId}
474
+ Report Generated: ${new Date(generatedAt).toLocaleString()}
475
+
476
+ SUMMARY STATISTICS
477
+ ------------------
478
+ Total Events: ${summary.totalEvents}
479
+ Successful Events: ${summary.successfulEvents} (${successRate}%)
480
+ Failed Events: ${summary.failedEvents}
481
+ Warning Events: ${summary.warningEvents}
482
+ Security Incidents: ${summary.securityIncidents}
483
+
484
+ COMPLIANCE STATUS
485
+ -----------------
486
+ Status: ${summary.complianceStatus.toUpperCase()}
487
+ ${summary.complianceStatus === 'compliant' ?
488
+ '✅ All audit events completed successfully' :
489
+ '⚠️ Some audit events failed - requires investigation'}
490
+
491
+ TIMELINE
492
+ --------
493
+ Start Time: ${new Date(summary.startTimestamp).toLocaleString()}
494
+ End Time: ${new Date(summary.endTimestamp).toLocaleString()}
495
+ Duration: ${this.calculateDuration(summary.startTimestamp, summary.endTimestamp)}
496
+
497
+ PARTICIPANTS
498
+ ------------
499
+ Users Involved: ${summary.participatingUsers.length}
500
+ ${summary.participatingUsers.map(uid => `- User ID: ${uid}`).join('\n')}
501
+
502
+ WORKFLOW PHASES
503
+ ---------------
504
+ ${summary.workflowPhases.map(phase => `- ${phase}`).join('\n')}
505
+
506
+ SECURITY ANALYSIS
507
+ -----------------
508
+ ${this.generateSecurityAnalysis(auditTrail.entries)}
509
+
510
+ CONFIRMATION WORKFLOW DETAILS
511
+ ------------------------------
512
+ ${this.generateConfirmationSummary(auditTrail.entries)}
513
+
514
+ ---
515
+ This report contains ${summary.totalEvents} audit entries providing complete forensic accountability.
516
+ Generated by Striae
517
+ `.trim();
518
+
519
+ return this.appendSignedReportIntegrity(
520
+ reportContent,
521
+ context,
522
+ summary.totalEvents,
523
+ generatedAt
524
+ );
525
+ }
526
+
527
+ /**
528
+ * Append signed integrity metadata to a plain-text audit report.
529
+ */
530
+ public async appendSignedReportIntegrity(
531
+ reportContent: string,
532
+ context: AuditExportContext,
533
+ totalEntries: number,
534
+ generatedAt: string = new Date().toISOString()
535
+ ): Promise<string> {
536
+ const hash = await calculateSHA256Secure(reportContent);
537
+ const signaturePayload = await this.signAuditExport(
538
+ {
539
+ exportFormat: 'txt',
540
+ exportType: 'report',
541
+ generatedAt,
542
+ totalEntries,
543
+ hash: hash.toUpperCase()
544
+ },
545
+ context
546
+ );
547
+
548
+ return reportContent + `
549
+
550
+ ============================
551
+ INTEGRITY VERIFICATION
552
+ ============================
553
+ Report Content SHA-256 Hash: ${hash.toUpperCase()}
554
+ Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}
555
+ Audit Signature: ${JSON.stringify(signaturePayload.signature)}
556
+
557
+ Verification Instructions:
558
+ 1. Copy the entire report content above the "INTEGRITY VERIFICATION" section
559
+ 2. Calculate SHA256 hash of that content (excluding this verification section)
560
+ 3. Validate audit signature metadata and signature with the Striae Hash Utility
561
+ 4. Confirm both hash and signature validation pass before relying on this report
562
+
563
+ This report requires both hash and signature validation for tamper detection.
564
+ Generated by Striae`;
565
+ }
566
+
567
+ private buildAuditSignaturePayload(
568
+ exportFormat: AuditExportFormat,
569
+ exportType: AuditExportType,
570
+ generatedAt: string,
571
+ totalEntries: number,
572
+ hash: string,
573
+ context: AuditExportContext
574
+ ): AuditExportSigningPayload {
575
+ return {
576
+ signatureVersion: AUDIT_EXPORT_SIGNATURE_VERSION,
577
+ exportFormat,
578
+ exportType,
579
+ scopeType: context.scopeType,
580
+ scopeIdentifier: context.scopeIdentifier,
581
+ generatedAt,
582
+ totalEntries,
583
+ hash: hash.toUpperCase()
584
+ };
585
+ }
586
+
587
+ private async signAuditExport(
588
+ payload: {
589
+ exportFormat: AuditExportFormat;
590
+ exportType: AuditExportType;
591
+ generatedAt: string;
592
+ totalEntries: number;
593
+ hash: string;
594
+ },
595
+ context: AuditExportContext
596
+ ): Promise<{
597
+ signatureMetadata: AuditExportSigningPayload;
598
+ signature: {
599
+ algorithm: string;
600
+ keyId: string;
601
+ signedAt: string;
602
+ value: string;
603
+ };
604
+ }> {
605
+ const signatureMetadata = this.buildAuditSignaturePayload(
606
+ payload.exportFormat,
607
+ payload.exportType,
608
+ payload.generatedAt,
609
+ payload.totalEntries,
610
+ payload.hash,
611
+ context
612
+ );
613
+
614
+ const caseNumber =
615
+ context.scopeType === 'case'
616
+ ? (context.caseNumber || context.scopeIdentifier)
617
+ : undefined;
618
+
619
+ const signatureResponse = await signAuditExportData(
620
+ context.user,
621
+ signatureMetadata,
622
+ { caseNumber }
623
+ );
624
+
625
+ return {
626
+ signatureMetadata,
627
+ signature: signatureResponse.signature
628
+ };
629
+ }
630
+
631
+ /**
632
+ * Calculate duration between timestamps
633
+ */
634
+ private calculateDuration(start: string, end: string): string {
635
+ const startTime = new Date(start).getTime();
636
+ const endTime = new Date(end).getTime();
637
+ const durationMs = endTime - startTime;
638
+
639
+ const hours = Math.floor(durationMs / (1000 * 60 * 60));
640
+ const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
641
+ const seconds = Math.floor((durationMs % (1000 * 60)) / 1000);
642
+
643
+ if (hours > 0) {
644
+ return `${hours}h ${minutes}m ${seconds}s`;
645
+ } else if (minutes > 0) {
646
+ return `${minutes}m ${seconds}s`;
647
+ } else {
648
+ return `${seconds}s`;
649
+ }
650
+ }
651
+
652
+ /**
653
+ * Generate security analysis section for reports
654
+ */
655
+ private generateSecurityAnalysis(entries: ValidationAuditEntry[]): string {
656
+ const securityEntries = entries.filter(e => e.details.securityChecks);
657
+
658
+ if (securityEntries.length === 0) {
659
+ return 'No security-sensitive operations detected.';
660
+ }
661
+
662
+ let selfConfirmationAttempts = 0;
663
+ let fileIntegrityFailures = 0;
664
+ let exporterValidationFailures = 0;
665
+ let legitimateImports = 0;
666
+
667
+ securityEntries.forEach(entry => {
668
+ const checks = entry.details.securityChecks!;
669
+
670
+ // Count actual security issues
671
+ if (checks.selfConfirmationPrevented === true) {
672
+ selfConfirmationAttempts++;
673
+ }
674
+ if (checks.fileIntegrityValid === false) {
675
+ fileIntegrityFailures++;
676
+ }
677
+ if (checks.exporterUidValidated === false) {
678
+ exporterValidationFailures++;
679
+ }
680
+
681
+ // Count legitimate confirmation imports
682
+ if (entry.action === 'import' && entry.details.workflowPhase === 'confirmation' &&
683
+ entry.result === 'success' && checks.selfConfirmationPrevented === false) {
684
+ legitimateImports++;
685
+ }
686
+ });
687
+
688
+ return [
689
+ `Total Security-Sensitive Operations: ${securityEntries.length}`,
690
+ `Legitimate Confirmation Imports: ${legitimateImports}`,
691
+ `Self-Confirmation Attempts Blocked: ${selfConfirmationAttempts}`,
692
+ `File Integrity Failures: ${fileIntegrityFailures}`,
693
+ `Exporter Validation Failures: ${exporterValidationFailures}`,
694
+ '',
695
+ selfConfirmationAttempts === 0 && fileIntegrityFailures === 0 && exporterValidationFailures === 0
696
+ ? '✅ No security violations detected'
697
+ : '⚠️ Security violations detected - review required'
698
+ ].join('\n');
699
+ }
700
+
701
+ /**
702
+ * Generate confirmation workflow summary for reports
703
+ */
704
+ private generateConfirmationSummary(entries: ValidationAuditEntry[]): string {
705
+ const confirmationEntries = entries.filter(e =>
706
+ e.details.workflowPhase === 'confirmation' ||
707
+ (e.action === 'import' && e.details.fileType === 'confirmation-data')
708
+ );
709
+
710
+ if (confirmationEntries.length === 0) {
711
+ return 'No confirmation workflow operations detected.';
712
+ }
713
+
714
+ const imports = confirmationEntries.filter(e => e.action === 'import');
715
+ const exports = confirmationEntries.filter(e => e.action === 'export');
716
+ const creations = confirmationEntries.filter(e => e.action === 'confirm');
717
+
718
+ let totalConfirmationsImported = 0;
719
+ let totalConfirmationsInFiles = 0;
720
+ const reviewingExaminers = new Set<string>();
721
+
722
+ imports.forEach(entry => {
723
+ const metrics = entry.details.performanceMetrics;
724
+ const caseDetails = entry.details.caseDetails;
725
+
726
+ if (metrics?.validationStepsCompleted) {
727
+ totalConfirmationsImported += metrics.validationStepsCompleted;
728
+ }
729
+ if (caseDetails?.totalAnnotations) {
730
+ totalConfirmationsInFiles += caseDetails.totalAnnotations;
731
+ }
732
+ if (entry.details.reviewingExaminerUid) {
733
+ reviewingExaminers.add(entry.details.reviewingExaminerUid);
734
+ }
735
+ });
736
+
737
+ return [
738
+ `Confirmation Operations: ${confirmationEntries.length}`,
739
+ `- Imports: ${imports.length}`,
740
+ `- Exports: ${exports.length}`,
741
+ `- Creations: ${creations.length}`,
742
+ '',
743
+ `Total Confirmations Imported: ${totalConfirmationsImported}`,
744
+ `Total Confirmations in Import Files: ${totalConfirmationsInFiles}`,
745
+ `Reviewing Examiners Involved: ${reviewingExaminers.size}`,
746
+ '',
747
+ reviewingExaminers.size > 0
748
+ ? `External Reviewers: ${Array.from(reviewingExaminers).join(', ')}`
749
+ : 'No external reviewers detected'
750
+ ].join('\n');
751
+ }
752
+ }
753
+
754
+ // Export singleton instance
755
+ export const auditExportService = AuditExportService.getInstance();