@striae-org/striae 5.4.2 → 5.4.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 (52) hide show
  1. package/app/components/actions/case-export/download-handlers.ts +1 -1
  2. package/app/components/actions/case-export/metadata-helpers.ts +2 -4
  3. package/app/components/actions/case-import/zip-processing.ts +3 -3
  4. package/app/components/mobile-warning/mobile-warning.module.css +80 -0
  5. package/app/components/mobile-warning/mobile-warning.tsx +108 -0
  6. package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
  7. package/app/config-example/config.json +2 -2
  8. package/app/root.tsx +2 -0
  9. package/app/services/audit/audit-file-type.ts +0 -1
  10. package/app/services/audit/audit.service.ts +1 -1
  11. package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
  12. package/app/services/audit/index.ts +0 -1
  13. package/app/types/audit.ts +1 -1
  14. package/app/utils/auth/auth-action-settings.ts +1 -1
  15. package/app/utils/data/permissions.ts +17 -15
  16. package/app/utils/forensics/audit-export-signature.ts +4 -4
  17. package/app/utils/forensics/export-verification.ts +3 -11
  18. package/package.json +2 -2
  19. package/workers/audit-worker/package.json +1 -1
  20. package/workers/audit-worker/src/audit-worker.example.ts +1 -1
  21. package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
  22. package/workers/audit-worker/wrangler.jsonc.example +1 -1
  23. package/workers/data-worker/package.json +17 -17
  24. package/workers/data-worker/src/encryption-utils.ts +1 -1
  25. package/workers/data-worker/src/signing-payload-utils.ts +4 -4
  26. package/workers/data-worker/wrangler.jsonc.example +1 -1
  27. package/workers/image-worker/package.json +1 -1
  28. package/workers/image-worker/src/auth.ts +7 -0
  29. package/workers/image-worker/src/handlers/delete-image.ts +26 -0
  30. package/workers/image-worker/src/handlers/mint-signed-url.ts +83 -0
  31. package/workers/image-worker/src/handlers/serve-image.ts +65 -0
  32. package/workers/image-worker/src/handlers/upload-image.ts +62 -0
  33. package/workers/image-worker/src/image-worker.example.ts +3 -707
  34. package/workers/image-worker/src/router.ts +53 -0
  35. package/workers/image-worker/src/security/key-registry.ts +193 -0
  36. package/workers/image-worker/src/security/signed-url.ts +163 -0
  37. package/workers/image-worker/src/types.ts +68 -0
  38. package/workers/image-worker/src/utils/content-disposition.ts +33 -0
  39. package/workers/image-worker/src/utils/path-utils.ts +50 -0
  40. package/workers/image-worker/src/utils/storage-metadata.ts +27 -0
  41. package/workers/image-worker/wrangler.jsonc.example +1 -1
  42. package/workers/pdf-worker/package.json +1 -1
  43. package/workers/pdf-worker/wrangler.jsonc.example +1 -1
  44. package/workers/user-worker/package.json +1 -1
  45. package/workers/user-worker/src/handlers/user-routes.ts +23 -34
  46. package/workers/user-worker/src/user-worker.example.ts +17 -23
  47. package/workers/user-worker/wrangler.jsonc.example +1 -1
  48. package/wrangler.toml.example +1 -1
  49. package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
  50. package/app/services/audit/audit-export-csv.ts +0 -130
  51. package/app/services/audit/audit-export-report.ts +0 -205
  52. package/app/services/audit/audit-export.service.ts +0 -333
@@ -1,205 +0,0 @@
1
- import { type AuditTrail, type ValidationAuditEntry } from '~/types';
2
-
3
- const calculateDuration = (start: string, end: string): string => {
4
- const startTime = new Date(start).getTime();
5
- const endTime = new Date(end).getTime();
6
- const durationMs = endTime - startTime;
7
-
8
- const hours = Math.floor(durationMs / (1000 * 60 * 60));
9
- const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
10
- const seconds = Math.floor((durationMs % (1000 * 60)) / 1000);
11
-
12
- if (hours > 0) {
13
- return `${hours}h ${minutes}m ${seconds}s`;
14
- }
15
-
16
- if (minutes > 0) {
17
- return `${minutes}m ${seconds}s`;
18
- }
19
-
20
- return `${seconds}s`;
21
- };
22
-
23
- const generateSecurityAnalysis = (entries: ValidationAuditEntry[]): string => {
24
- const securityEntries = entries.filter(entry => entry.details.securityChecks);
25
-
26
- if (securityEntries.length === 0) {
27
- return 'No security-sensitive operations detected.';
28
- }
29
-
30
- let selfConfirmationAttempts = 0;
31
- let fileIntegrityFailures = 0;
32
- let exporterValidationFailures = 0;
33
- let legitimateImports = 0;
34
-
35
- securityEntries.forEach(entry => {
36
- const checks = entry.details.securityChecks!;
37
-
38
- if (checks.selfConfirmationPrevented === true) {
39
- selfConfirmationAttempts++;
40
- }
41
- if (checks.fileIntegrityValid === false) {
42
- fileIntegrityFailures++;
43
- }
44
- if (checks.exporterUidValidated === false) {
45
- exporterValidationFailures++;
46
- }
47
-
48
- if (
49
- entry.action === 'import' &&
50
- entry.details.workflowPhase === 'confirmation' &&
51
- entry.result === 'success' &&
52
- checks.selfConfirmationPrevented === false
53
- ) {
54
- legitimateImports++;
55
- }
56
- });
57
-
58
- return [
59
- `Total Security-Sensitive Operations: ${securityEntries.length}`,
60
- `Legitimate Confirmation Imports: ${legitimateImports}`,
61
- `Self-Confirmation Attempts Blocked: ${selfConfirmationAttempts}`,
62
- `File Integrity Failures: ${fileIntegrityFailures}`,
63
- `Exporter Validation Failures: ${exporterValidationFailures}`,
64
- '',
65
- selfConfirmationAttempts === 0 && fileIntegrityFailures === 0 && exporterValidationFailures === 0
66
- ? '✅ No security violations detected'
67
- : '⚠️ Security violations detected - review required'
68
- ].join('\n');
69
- };
70
-
71
- const generateConfirmationSummary = (entries: ValidationAuditEntry[]): string => {
72
- const confirmationEntries = entries.filter(entry =>
73
- entry.details.workflowPhase === 'confirmation' ||
74
- (entry.action === 'import' && entry.details.fileType === 'confirmation-data')
75
- );
76
-
77
- if (confirmationEntries.length === 0) {
78
- return 'No confirmation workflow operations detected.';
79
- }
80
-
81
- const imports = confirmationEntries.filter(entry => entry.action === 'import');
82
- const exports = confirmationEntries.filter(entry => entry.action === 'export');
83
- const creations = confirmationEntries.filter(entry => entry.action === 'confirm');
84
-
85
- let totalConfirmationsImported = 0;
86
- let totalConfirmationsInFiles = 0;
87
- const reviewingExaminers = new Map<string, { uid: string; badgeId?: string; confirmedFiles: Set<string> }>();
88
- const allConfirmedFiles = new Set<string>();
89
-
90
- imports.forEach(entry => {
91
- const metrics = entry.details.performanceMetrics;
92
- const caseDetails = entry.details.caseDetails;
93
-
94
- if (metrics?.validationStepsCompleted) {
95
- totalConfirmationsImported += metrics.validationStepsCompleted;
96
- }
97
- if (caseDetails?.totalAnnotations) {
98
- totalConfirmationsInFiles += caseDetails.totalAnnotations;
99
- }
100
- if (entry.details.reviewingExaminerUid) {
101
- const uid = entry.details.reviewingExaminerUid;
102
- const badgeId = entry.details.reviewerBadgeId;
103
- const confirmedFileNames = caseDetails?.confirmedFileNames || [];
104
-
105
- if (!reviewingExaminers.has(uid)) {
106
- reviewingExaminers.set(uid, {
107
- uid,
108
- badgeId,
109
- confirmedFiles: new Set()
110
- });
111
- }
112
-
113
- const examiner = reviewingExaminers.get(uid)!;
114
- confirmedFileNames.forEach(file => {
115
- examiner.confirmedFiles.add(file);
116
- allConfirmedFiles.add(file);
117
- });
118
- }
119
- });
120
-
121
- const examinersDetail = Array.from(reviewingExaminers.values())
122
- .map(examiner => {
123
- const badgeInfo = examiner.badgeId ? ` (Badge: ${examiner.badgeId})` : '';
124
- const filesInfo = examiner.confirmedFiles.size > 0
125
- ? `\n Confirmed Files: ${Array.from(examiner.confirmedFiles).sort().join(', ')}`
126
- : '';
127
- return `- UID: ${examiner.uid}${badgeInfo}${filesInfo}`;
128
- })
129
- .join('\n');
130
-
131
- return [
132
- `Confirmation Operations: ${confirmationEntries.length}`,
133
- `- Imports: ${imports.length}`,
134
- `- Exports: ${exports.length}`,
135
- `- Creations: ${creations.length}`,
136
- '',
137
- `Total Confirmations Imported: ${totalConfirmationsImported}`,
138
- `Total Confirmations in Import Files: ${totalConfirmationsInFiles}`,
139
- `Reviewing Examiners Involved: ${reviewingExaminers.size}`,
140
- '',
141
- reviewingExaminers.size > 0
142
- ? `External Reviewers:\n${examinersDetail}`
143
- : 'No external reviewers detected',
144
- '',
145
- allConfirmedFiles.size > 0
146
- ? `Successfully Confirmed Files (Total: ${allConfirmedFiles.size}):\n${Array.from(allConfirmedFiles).sort().map(file => ` - ${file}`).join('\n')}`
147
- : 'No files confirmed'
148
- ].join('\n');
149
- };
150
-
151
- export const buildAuditReportContent = (auditTrail: AuditTrail, generatedAt: string): string => {
152
- const summary = auditTrail.summary;
153
- const successRate = ((summary.successfulEvents / summary.totalEvents) * 100).toFixed(1);
154
-
155
- return `
156
- STRIAE AUDIT TRAIL REPORT
157
- ============================
158
-
159
- Case Number: ${auditTrail.caseNumber}
160
- Workflow ID: ${auditTrail.workflowId}
161
- Report Generated: ${new Date(generatedAt).toLocaleString()}
162
-
163
- SUMMARY STATISTICS
164
- ------------------
165
- Total Events: ${summary.totalEvents}
166
- Successful Events: ${summary.successfulEvents} (${successRate}%)
167
- Failed Events: ${summary.failedEvents}
168
- Warning Events: ${summary.warningEvents}
169
- Security Incidents: ${summary.securityIncidents}
170
-
171
- COMPLIANCE STATUS
172
- -----------------
173
- Status: ${summary.complianceStatus.toUpperCase()}
174
- ${summary.complianceStatus === 'compliant'
175
- ? '✅ All audit events completed successfully'
176
- : '⚠️ Some audit events failed - requires investigation'}
177
-
178
- TIMELINE
179
- --------
180
- Start Time: ${new Date(summary.startTimestamp).toLocaleString()}
181
- End Time: ${new Date(summary.endTimestamp).toLocaleString()}
182
- Duration: ${calculateDuration(summary.startTimestamp, summary.endTimestamp)}
183
-
184
- PARTICIPANTS
185
- ------------
186
- Users Involved: ${summary.participatingUsers.length}
187
- ${summary.participatingUsers.map(uid => `- User ID: ${uid}`).join('\n')}
188
-
189
- WORKFLOW PHASES
190
- ---------------
191
- ${summary.workflowPhases.map(phase => `- ${phase}`).join('\n')}
192
-
193
- SECURITY ANALYSIS
194
- -----------------
195
- ${generateSecurityAnalysis(auditTrail.entries)}
196
-
197
- CONFIRMATION WORKFLOW DETAILS
198
- ------------------------------
199
- ${generateConfirmationSummary(auditTrail.entries)}
200
-
201
- ---
202
- This report contains ${summary.totalEvents} audit entries providing complete forensic accountability.
203
- Generated by Striae
204
- `.trim();
205
- };
@@ -1,333 +0,0 @@
1
- import { type ValidationAuditEntry, type AuditTrail } from '~/types';
2
- import { calculateSHA256Secure, type AuditExportType } from '~/utils/forensics';
3
- import { AUDIT_CSV_ENTRY_HEADERS, entryToCSVRow } from './audit-export-csv';
4
- import { buildAuditReportContent } from './audit-export-report';
5
- import { type AuditExportContext, signAuditExport } from './audit-export-signing';
6
-
7
- /**
8
- * Audit Export Service
9
- * Handles exporting audit trails to various formats for compliance and forensic analysis
10
- */
11
- class AuditExportService {
12
- private static instance: AuditExportService;
13
-
14
- private constructor() {}
15
-
16
- public static getInstance(): AuditExportService {
17
- if (!AuditExportService.instance) {
18
- AuditExportService.instance = new AuditExportService();
19
- }
20
- return AuditExportService.instance;
21
- }
22
-
23
- /**
24
- * Export audit entries to CSV format
25
- */
26
- public async exportToCSV(
27
- entries: ValidationAuditEntry[],
28
- filename: string,
29
- context: AuditExportContext
30
- ): Promise<void> {
31
- const csvData = [
32
- AUDIT_CSV_ENTRY_HEADERS.join(','),
33
- ...entries.map(entry => entryToCSVRow(entry))
34
- ].join('\n');
35
-
36
- const generatedAt = new Date().toISOString();
37
- const hash = await calculateSHA256Secure(csvData);
38
- const signaturePayload = await signAuditExport(
39
- {
40
- exportFormat: 'csv',
41
- exportType: 'entries',
42
- generatedAt,
43
- totalEntries: entries.length,
44
- hash: hash.toUpperCase()
45
- },
46
- context
47
- );
48
-
49
- // Add hash metadata header
50
- const csvContent = [
51
- `# Striae Audit Export - Generated: ${generatedAt}`,
52
- `# Total Entries: ${entries.length}`,
53
- `# SHA-256 Hash: ${hash.toUpperCase()}`,
54
- `# Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}`,
55
- `# Audit Signature: ${JSON.stringify(signaturePayload.signature)}`,
56
- `# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)`,
57
- '',
58
- csvData
59
- ].join('\n');
60
-
61
- this.downloadFile(csvContent, filename, 'text/csv');
62
- }
63
-
64
- /**
65
- * Export audit trail to detailed CSV with summary
66
- */
67
- public async exportAuditTrailToCSV(
68
- auditTrail: AuditTrail,
69
- filename: string,
70
- context: AuditExportContext
71
- ): Promise<void> {
72
- const summaryHeaders = [
73
- 'Case Number',
74
- 'Workflow ID',
75
- 'Total Events',
76
- 'Successful Events',
77
- 'Failed Events',
78
- 'Warning Events',
79
- 'Compliance Status',
80
- 'Security Incidents',
81
- 'Start Time',
82
- 'End Time',
83
- 'Participating Users'
84
- ];
85
-
86
- const summaryRow = [
87
- auditTrail.caseNumber,
88
- auditTrail.workflowId,
89
- auditTrail.summary.totalEvents,
90
- auditTrail.summary.successfulEvents,
91
- auditTrail.summary.failedEvents,
92
- auditTrail.summary.warningEvents,
93
- auditTrail.summary.complianceStatus.toUpperCase(),
94
- auditTrail.summary.securityIncidents,
95
- auditTrail.summary.startTimestamp,
96
- auditTrail.summary.endTimestamp,
97
- auditTrail.summary.participatingUsers.join('; ')
98
- ].join(',');
99
-
100
- const csvData = [
101
- summaryHeaders.join(','),
102
- summaryRow,
103
- '',
104
- AUDIT_CSV_ENTRY_HEADERS.join(','),
105
- ...auditTrail.entries.map(entry => entryToCSVRow(entry))
106
- ].join('\n');
107
-
108
- const generatedAt = new Date().toISOString();
109
- const hash = await calculateSHA256Secure(csvData);
110
- const signaturePayload = await signAuditExport(
111
- {
112
- exportFormat: 'csv',
113
- exportType: 'trail',
114
- generatedAt,
115
- totalEntries: auditTrail.summary.totalEvents,
116
- hash: hash.toUpperCase()
117
- },
118
- context
119
- );
120
-
121
- const csvContent = [
122
- '# Striae Audit Trail Export - Generated: ' + generatedAt,
123
- `# Case: ${auditTrail.caseNumber} | Workflow: ${auditTrail.workflowId}`,
124
- `# Total Events: ${auditTrail.summary.totalEvents}`,
125
- `# SHA-256 Hash: ${hash.toUpperCase()}`,
126
- `# Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}`,
127
- `# Audit Signature: ${JSON.stringify(signaturePayload.signature)}`,
128
- '# Verification: Recalculate SHA-256 of data rows only (excluding these comment lines)',
129
- '',
130
- '# AUDIT TRAIL SUMMARY',
131
- csvData
132
- ].join('\n');
133
-
134
- this.downloadFile(csvContent, filename, 'text/csv');
135
- }
136
-
137
- /**
138
- * Generate filename with timestamp
139
- */
140
- public generateFilename(type: 'case' | 'user', identifier: string, format: 'csv' | 'json'): string {
141
- const timestamp = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
142
- const sanitizedId = identifier.replace(/[^a-zA-Z0-9-_]/g, '_');
143
- return `striae-audit-${type}-${sanitizedId}-${timestamp}.${format}`;
144
- }
145
-
146
- /**
147
- * Download file helper
148
- */
149
- private downloadFile(content: string, filename: string, mimeType: string): void {
150
- const blob = new Blob([content], { type: mimeType });
151
- const url = URL.createObjectURL(blob);
152
-
153
- const link = document.createElement('a');
154
- link.href = url;
155
- link.download = filename;
156
- link.style.display = 'none';
157
-
158
- document.body.appendChild(link);
159
- link.click();
160
- document.body.removeChild(link);
161
-
162
- // Clean up the URL object
163
- setTimeout(() => URL.revokeObjectURL(url), 100);
164
- }
165
-
166
- /**
167
- * Export audit entries to JSON format (for technical analysis)
168
- */
169
- public async exportToJSON(
170
- entries: ValidationAuditEntry[],
171
- filename: string,
172
- context: AuditExportContext
173
- ): Promise<void> {
174
- const generatedAt = new Date().toISOString();
175
-
176
- const exportData = {
177
- metadata: {
178
- exportTimestamp: generatedAt,
179
- exportVersion: '1.0',
180
- totalEntries: entries.length,
181
- application: 'Striae',
182
- exportType: 'entries' as AuditExportType,
183
- scopeType: context.scopeType,
184
- scopeIdentifier: context.scopeIdentifier
185
- },
186
- auditEntries: entries
187
- };
188
-
189
- const jsonContent = JSON.stringify(exportData, null, 2);
190
-
191
- // Calculate hash for integrity verification
192
- const hash = await calculateSHA256Secure(jsonContent);
193
- const signaturePayload = await signAuditExport(
194
- {
195
- exportFormat: 'json',
196
- exportType: exportData.metadata.exportType,
197
- generatedAt,
198
- totalEntries: entries.length,
199
- hash: hash.toUpperCase()
200
- },
201
- context
202
- );
203
-
204
- // Create final export with hash included
205
- const finalExportData = {
206
- metadata: {
207
- ...exportData.metadata,
208
- hash: hash.toUpperCase(),
209
- integrityNote: 'Verify hash and signature before trusting this export',
210
- signatureVersion: signaturePayload.signatureMetadata.signatureVersion,
211
- signature: signaturePayload.signature
212
- },
213
- auditEntries: entries
214
- };
215
-
216
- const finalJsonContent = JSON.stringify(finalExportData, null, 2);
217
- this.downloadFile(finalJsonContent, filename.replace('.csv', '.json'), 'application/json');
218
- }
219
-
220
- /**
221
- * Export full audit trail to JSON
222
- */
223
- public async exportAuditTrailToJSON(
224
- auditTrail: AuditTrail,
225
- filename: string,
226
- context: AuditExportContext
227
- ): Promise<void> {
228
- const generatedAt = new Date().toISOString();
229
-
230
- const exportData = {
231
- metadata: {
232
- exportTimestamp: generatedAt,
233
- exportVersion: '1.0',
234
- totalEntries: auditTrail.summary.totalEvents,
235
- application: 'Striae',
236
- exportType: 'trail' as AuditExportType,
237
- scopeType: context.scopeType,
238
- scopeIdentifier: context.scopeIdentifier
239
- },
240
- auditTrail
241
- };
242
-
243
- const jsonContent = JSON.stringify(exportData, null, 2);
244
-
245
- // Calculate hash for integrity verification
246
- const hash = await calculateSHA256Secure(jsonContent);
247
- const signaturePayload = await signAuditExport(
248
- {
249
- exportFormat: 'json',
250
- exportType: exportData.metadata.exportType,
251
- generatedAt,
252
- totalEntries: auditTrail.summary.totalEvents,
253
- hash: hash.toUpperCase()
254
- },
255
- context
256
- );
257
-
258
- // Create final export with hash included
259
- const finalExportData = {
260
- metadata: {
261
- ...exportData.metadata,
262
- hash: hash.toUpperCase(),
263
- integrityNote: 'Verify hash and signature before trusting this export',
264
- signatureVersion: signaturePayload.signatureMetadata.signatureVersion,
265
- signature: signaturePayload.signature
266
- },
267
- auditTrail
268
- };
269
-
270
- const finalJsonContent = JSON.stringify(finalExportData, null, 2);
271
- this.downloadFile(finalJsonContent, filename.replace('.csv', '.json'), 'application/json');
272
- }
273
-
274
- /**
275
- * Generate audit report summary text
276
- */
277
- public async generateReportSummary(auditTrail: AuditTrail, context: AuditExportContext): Promise<string> {
278
- const summary = auditTrail.summary;
279
- const generatedAt = new Date().toISOString();
280
-
281
- const reportContent = buildAuditReportContent(auditTrail, generatedAt);
282
-
283
- return this.appendSignedReportIntegrity(
284
- reportContent,
285
- context,
286
- summary.totalEvents,
287
- generatedAt
288
- );
289
- }
290
-
291
- /**
292
- * Append signed integrity metadata to a plain-text audit report.
293
- */
294
- public async appendSignedReportIntegrity(
295
- reportContent: string,
296
- context: AuditExportContext,
297
- totalEntries: number,
298
- generatedAt: string = new Date().toISOString()
299
- ): Promise<string> {
300
- const hash = await calculateSHA256Secure(reportContent);
301
- const signaturePayload = await signAuditExport(
302
- {
303
- exportFormat: 'txt',
304
- exportType: 'report',
305
- generatedAt,
306
- totalEntries,
307
- hash: hash.toUpperCase()
308
- },
309
- context
310
- );
311
-
312
- return reportContent + `
313
-
314
- ============================
315
- INTEGRITY VERIFICATION
316
- ============================
317
- Report Content SHA-256 Hash: ${hash.toUpperCase()}
318
- Audit Signature Metadata: ${JSON.stringify(signaturePayload.signatureMetadata)}
319
- Audit Signature: ${JSON.stringify(signaturePayload.signature)}
320
-
321
- Verification Instructions:
322
- 1. Copy the entire report content above the "INTEGRITY VERIFICATION" section
323
- 2. Calculate SHA256 hash of that content (excluding this verification section)
324
- 3. Validate audit signature metadata and signature with your signature verification workflow (for example OpenSSL or an internal verifier)
325
- 4. Confirm both hash and signature validation pass before relying on this report
326
-
327
- This report requires both hash and signature validation for tamper detection.
328
- Generated by Striae`;
329
- }
330
- }
331
-
332
- // Export singleton instance
333
- export const auditExportService = AuditExportService.getInstance();