@striae-org/striae 5.4.2 → 5.4.3
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.
- package/app/components/actions/case-export/download-handlers.ts +1 -1
- package/app/components/actions/case-export/metadata-helpers.ts +2 -4
- package/app/components/actions/case-import/zip-processing.ts +3 -3
- package/app/components/mobile-warning/mobile-warning.module.css +80 -0
- package/app/components/mobile-warning/mobile-warning.tsx +108 -0
- package/app/components/navbar/case-import/utils/file-validation.ts +1 -1
- package/app/config-example/config.json +2 -2
- package/app/root.tsx +2 -0
- package/app/services/audit/audit-file-type.ts +0 -1
- package/app/services/audit/audit.service.ts +1 -1
- package/app/services/audit/builders/audit-event-builders-workflow.ts +2 -6
- package/app/services/audit/index.ts +0 -1
- package/app/types/audit.ts +1 -1
- package/app/utils/data/permissions.ts +17 -15
- package/app/utils/forensics/audit-export-signature.ts +4 -4
- package/app/utils/forensics/export-verification.ts +3 -11
- package/package.json +2 -2
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/src/audit-worker.example.ts +1 -1
- package/workers/audit-worker/src/handlers/audit-routes.ts +1 -30
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +17 -17
- package/workers/data-worker/src/encryption-utils.ts +1 -1
- package/workers/data-worker/src/signing-payload-utils.ts +4 -4
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/pdf-worker/package.json +1 -1
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +1 -1
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +1 -1
- package/app/components/audit/viewer/use-audit-viewer-export.ts +0 -176
- package/app/services/audit/audit-export-csv.ts +0 -130
- package/app/services/audit/audit-export-report.ts +0 -205
- package/app/services/audit/audit-export.service.ts +0 -333
|
@@ -438,7 +438,7 @@ async function generateJSONContent(
|
|
|
438
438
|
|
|
439
439
|
// Add forensic protection warning if enabled
|
|
440
440
|
if (protectForensicData) {
|
|
441
|
-
return addForensicDataWarning(finalJsonString
|
|
441
|
+
return addForensicDataWarning(finalJsonString);
|
|
442
442
|
}
|
|
443
443
|
|
|
444
444
|
return finalJsonString;
|
|
@@ -32,10 +32,8 @@ export async function getUserExportMetadata(user: User) {
|
|
|
32
32
|
/**
|
|
33
33
|
* Add data protection warning to content
|
|
34
34
|
*/
|
|
35
|
-
export function addForensicDataWarning(content: string
|
|
36
|
-
const warning =
|
|
37
|
-
? `"CASE DATA WARNING: This file contains evidence data for forensic examination. Any modification may compromise the integrity of the evidence. Handle according to your organization's chain of custody procedures."\n\n`
|
|
38
|
-
: `/* CASE DATA WARNING
|
|
35
|
+
export function addForensicDataWarning(content: string): string {
|
|
36
|
+
const warning = `/* CASE DATA WARNING
|
|
39
37
|
* This file contains evidence data for forensic examination.
|
|
40
38
|
* Any modification may compromise the integrity of the evidence.
|
|
41
39
|
* Handle according to your organization's chain of custody procedures.
|
|
@@ -281,13 +281,13 @@ export async function parseImportZip(zipFile: File): Promise<{
|
|
|
281
281
|
const zip = await JSZip.loadAsync(zipFile);
|
|
282
282
|
const verificationPublicKeyPem = await extractVerificationPublicKeyFromZip(zip);
|
|
283
283
|
|
|
284
|
-
// Find the main data file (JSON
|
|
284
|
+
// Find the main data file (JSON)
|
|
285
285
|
const dataFiles = Object.keys(zip.files).filter(name =>
|
|
286
|
-
name.endsWith('_data.json')
|
|
286
|
+
name.endsWith('_data.json')
|
|
287
287
|
);
|
|
288
288
|
|
|
289
289
|
if (dataFiles.length === 0) {
|
|
290
|
-
throw new Error('No valid data file found in ZIP archive');
|
|
290
|
+
throw new Error('No valid JSON data file found in ZIP archive');
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
if (dataFiles.length > 1) {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
@layer layout {
|
|
2
|
+
.overlay {
|
|
3
|
+
display: none;
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0;
|
|
6
|
+
z-index: 10000;
|
|
7
|
+
background: var(--background);
|
|
8
|
+
align-items: center;
|
|
9
|
+
justify-content: center;
|
|
10
|
+
padding: var(--spaceXL);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.backdrop {
|
|
14
|
+
appearance: none;
|
|
15
|
+
position: absolute;
|
|
16
|
+
inset: 0;
|
|
17
|
+
background: transparent;
|
|
18
|
+
border: none;
|
|
19
|
+
cursor: default;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.content {
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
align-items: center;
|
|
26
|
+
text-align: center;
|
|
27
|
+
max-width: 420px;
|
|
28
|
+
gap: var(--spaceL);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.icon {
|
|
32
|
+
color: var(--primary);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.title {
|
|
36
|
+
font-size: var(--fontSizeH4);
|
|
37
|
+
font-weight: var(--fontWeightBold);
|
|
38
|
+
color: var(--textTitle);
|
|
39
|
+
line-height: var(--lineHeightTitle);
|
|
40
|
+
margin: 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.message {
|
|
44
|
+
font-size: var(--fontSizeBodyS);
|
|
45
|
+
color: var(--textBody);
|
|
46
|
+
line-height: var(--lineHeightBody);
|
|
47
|
+
margin: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.dismissButton {
|
|
51
|
+
appearance: none;
|
|
52
|
+
background-color: var(--primary);
|
|
53
|
+
border: 1px solid var(--primary);
|
|
54
|
+
border-radius: var(--radiusM);
|
|
55
|
+
color: var(--white);
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
font-size: var(--fontSizeBodyS);
|
|
58
|
+
font-weight: var(--fontWeightMedium);
|
|
59
|
+
padding: var(--spaceM) var(--spaceXL);
|
|
60
|
+
transition:
|
|
61
|
+
background-color var(--durationS) var(--bezierFastoutSlowin),
|
|
62
|
+
border-color var(--durationS) var(--bezierFastoutSlowin);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.dismissButton:hover {
|
|
66
|
+
background-color: color-mix(in lab, var(--primary) 82%, var(--black));
|
|
67
|
+
border-color: color-mix(in lab, var(--primary) 82%, var(--black));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.dismissButton:focus-visible {
|
|
71
|
+
outline: 3px solid color-mix(in lab, var(--white) 65%, var(--primary));
|
|
72
|
+
outline-offset: 3px;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@media (max-width: 1024px) {
|
|
76
|
+
.overlay {
|
|
77
|
+
display: flex;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useState, useCallback, useRef, useEffect } from 'react';
|
|
2
|
+
import styles from './mobile-warning.module.css';
|
|
3
|
+
|
|
4
|
+
const DISMISSED_KEY = 'striae-mobile-warning-dismissed';
|
|
5
|
+
|
|
6
|
+
const isBrowser =
|
|
7
|
+
typeof window !== 'undefined' && typeof window.sessionStorage !== 'undefined';
|
|
8
|
+
|
|
9
|
+
export function MobileWarning() {
|
|
10
|
+
const [dismissed, setDismissed] = useState<boolean>(() => {
|
|
11
|
+
if (!isBrowser) {
|
|
12
|
+
// Match previous server behavior: render null during SSR.
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
return window.sessionStorage.getItem(DISMISSED_KEY) === '1';
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!dismissed && buttonRef.current) {
|
|
27
|
+
buttonRef.current.focus();
|
|
28
|
+
}
|
|
29
|
+
}, [dismissed]);
|
|
30
|
+
|
|
31
|
+
const handleDismiss = useCallback(() => {
|
|
32
|
+
if (isBrowser) {
|
|
33
|
+
try {
|
|
34
|
+
window.sessionStorage.setItem(DISMISSED_KEY, '1');
|
|
35
|
+
} catch {
|
|
36
|
+
// Ignore storage errors and still dismiss for this session.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
setDismissed(true);
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (dismissed) return;
|
|
44
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
45
|
+
if (e.key === 'Escape') {
|
|
46
|
+
handleDismiss();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
document.addEventListener('keydown', onKeyDown);
|
|
50
|
+
return () => document.removeEventListener('keydown', onKeyDown);
|
|
51
|
+
}, [dismissed, handleDismiss]);
|
|
52
|
+
|
|
53
|
+
if (dismissed) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={styles.overlay}
|
|
60
|
+
role="dialog"
|
|
61
|
+
aria-modal="true"
|
|
62
|
+
aria-labelledby="mobile-warning-title"
|
|
63
|
+
aria-describedby="mobile-warning-message"
|
|
64
|
+
>
|
|
65
|
+
{/* Backdrop dismiss button covers area outside dialog content */}
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
className={styles.backdrop}
|
|
69
|
+
onClick={handleDismiss}
|
|
70
|
+
aria-label="Dismiss"
|
|
71
|
+
tabIndex={-1}
|
|
72
|
+
/>
|
|
73
|
+
<div className={styles.content}>
|
|
74
|
+
<div className={styles.icon}>
|
|
75
|
+
<svg
|
|
76
|
+
width="48"
|
|
77
|
+
height="48"
|
|
78
|
+
viewBox="0 0 24 24"
|
|
79
|
+
fill="none"
|
|
80
|
+
stroke="currentColor"
|
|
81
|
+
strokeWidth="1.5"
|
|
82
|
+
strokeLinecap="round"
|
|
83
|
+
strokeLinejoin="round"
|
|
84
|
+
>
|
|
85
|
+
<rect x="5" y="2" width="14" height="20" rx="2" ry="2" />
|
|
86
|
+
<line x1="12" y1="18" x2="12.01" y2="18" />
|
|
87
|
+
</svg>
|
|
88
|
+
</div>
|
|
89
|
+
<h2 className={styles.title} id="mobile-warning-title">
|
|
90
|
+
Desktop Experience Only
|
|
91
|
+
</h2>
|
|
92
|
+
<p className={styles.message} id="mobile-warning-message">
|
|
93
|
+
Striae is designed for desktop browsers and is not optimized for
|
|
94
|
+
mobile devices or tablets. For the best experience, please use a
|
|
95
|
+
desktop computer.
|
|
96
|
+
</p>
|
|
97
|
+
<button
|
|
98
|
+
ref={buttonRef}
|
|
99
|
+
type="button"
|
|
100
|
+
className={styles.dismissButton}
|
|
101
|
+
onClick={handleDismiss}
|
|
102
|
+
>
|
|
103
|
+
Continue Anyway
|
|
104
|
+
</button>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const CASE_EXPORT_DATA_FILE_REGEX = /_data\.
|
|
1
|
+
const CASE_EXPORT_DATA_FILE_REGEX = /_data\.json$/i;
|
|
2
2
|
const CONFIRMATION_EXPORT_FILE_REGEX = /^confirmation-data-.*\.json$/i;
|
|
3
3
|
const FORENSIC_MANIFEST_FILE_NAME = 'forensic_manifest.json';
|
|
4
4
|
const ENCRYPTION_MANIFEST_FILE_NAME = 'encryption_manifest.json';
|
package/app/root.tsx
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
themeStyles
|
|
14
14
|
} from '~/components/theme-provider/theme-provider';
|
|
15
15
|
import { AuthProvider } from '~/components/auth/auth-provider';
|
|
16
|
+
import { MobileWarning } from '~/components/mobile-warning/mobile-warning';
|
|
16
17
|
import { auth } from '~/services/firebase';
|
|
17
18
|
import styles from '~/styles/root.module.css';
|
|
18
19
|
import './global.css';
|
|
@@ -68,6 +69,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
|
|
|
68
69
|
</head>
|
|
69
70
|
<body className="flex flex-col h-screen w-full overflow-x-hidden">
|
|
70
71
|
<ThemeProvider theme={theme} className="">
|
|
72
|
+
<MobileWarning />
|
|
71
73
|
<main>
|
|
72
74
|
{children}
|
|
73
75
|
</main>
|
|
@@ -4,7 +4,6 @@ export const getAuditFileTypeFromMime = (mimeType: string): AuditFileType => {
|
|
|
4
4
|
if (mimeType.startsWith('image/')) return 'image-file';
|
|
5
5
|
if (mimeType === 'application/pdf') return 'pdf-document';
|
|
6
6
|
if (mimeType === 'application/json') return 'json-data';
|
|
7
|
-
if (mimeType === 'text/csv') return 'csv-export';
|
|
8
7
|
return 'unknown';
|
|
9
8
|
};
|
|
10
9
|
|
|
@@ -208,7 +208,7 @@ class AuditService {
|
|
|
208
208
|
result: AuditResult,
|
|
209
209
|
errors: string[] = [],
|
|
210
210
|
performanceMetrics?: PerformanceMetrics,
|
|
211
|
-
exportFormat?: 'json' | '
|
|
211
|
+
exportFormat?: 'json' | 'zip',
|
|
212
212
|
protectionEnabled?: boolean,
|
|
213
213
|
signatureDetails?: {
|
|
214
214
|
present?: boolean;
|
|
@@ -20,21 +20,18 @@ interface BuildCaseExportAuditParamsInput {
|
|
|
20
20
|
result: AuditResult;
|
|
21
21
|
errors?: string[];
|
|
22
22
|
performanceMetrics?: PerformanceMetrics;
|
|
23
|
-
exportFormat?: 'json' | '
|
|
23
|
+
exportFormat?: 'json' | 'zip';
|
|
24
24
|
signatureDetails?: SignatureDetailsInput;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const resolveCaseExportFileType = (
|
|
28
28
|
fileName: string,
|
|
29
|
-
exportFormat?: 'json' | '
|
|
29
|
+
exportFormat?: 'json' | 'zip'
|
|
30
30
|
): AuditFileType => {
|
|
31
31
|
if (exportFormat) {
|
|
32
32
|
switch (exportFormat) {
|
|
33
33
|
case 'json':
|
|
34
34
|
return 'json-data';
|
|
35
|
-
case 'csv':
|
|
36
|
-
case 'xlsx':
|
|
37
|
-
return 'csv-export';
|
|
38
35
|
case 'zip':
|
|
39
36
|
default:
|
|
40
37
|
return 'case-package';
|
|
@@ -42,7 +39,6 @@ const resolveCaseExportFileType = (
|
|
|
42
39
|
}
|
|
43
40
|
|
|
44
41
|
if (fileName.includes('.json')) return 'json-data';
|
|
45
|
-
if (fileName.includes('.csv') || fileName.includes('.xlsx')) return 'csv-export';
|
|
46
42
|
return 'case-package';
|
|
47
43
|
};
|
|
48
44
|
|
package/app/types/audit.ts
CHANGED
|
@@ -25,7 +25,7 @@ export type AuditResult = 'success' | 'failure' | 'warning' | 'blocked' | 'pendi
|
|
|
25
25
|
|
|
26
26
|
export type AuditFileType =
|
|
27
27
|
| 'case-package' | 'confirmation-data' | 'image-file' | 'pdf-document'
|
|
28
|
-
| 'json-data' | '
|
|
28
|
+
| 'json-data' | 'log-file' | 'unknown';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Core audit entry structure for all validation events
|
|
@@ -3,8 +3,8 @@ import type { UserData, ExtendedUserData, UserLimits, ReadOnlyCaseMetadata } fro
|
|
|
3
3
|
import paths from '~/config/config.json';
|
|
4
4
|
import { fetchDataApi, fetchUserApi } from '../api';
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
const
|
|
6
|
+
const MAX_CASES_DEMO = paths.max_cases_demo;
|
|
7
|
+
const MAX_FILES_PER_CASE_DEMO = paths.max_files_per_case_demo;
|
|
8
8
|
|
|
9
9
|
export interface UserUsage {
|
|
10
10
|
currentCases: number;
|
|
@@ -63,8 +63,8 @@ export const getUserLimits = (userData: UserData): UserLimits => {
|
|
|
63
63
|
};
|
|
64
64
|
} else {
|
|
65
65
|
return {
|
|
66
|
-
maxCases:
|
|
67
|
-
maxFilesPerCase:
|
|
66
|
+
maxCases: MAX_CASES_DEMO, // Use config value for demo users
|
|
67
|
+
maxFilesPerCase: MAX_FILES_PER_CASE_DEMO // Use config value for demo users
|
|
68
68
|
};
|
|
69
69
|
}
|
|
70
70
|
};
|
|
@@ -155,7 +155,7 @@ export const canCreateCase = async (user: User): Promise<{ canCreate: boolean; r
|
|
|
155
155
|
if (usage.currentCases >= limits.maxCases) {
|
|
156
156
|
return {
|
|
157
157
|
canCreate: false,
|
|
158
|
-
reason: `
|
|
158
|
+
reason: `Demo account only: Maximum of ${limits.maxCases} case${limits.maxCases === 1 ? '' : 's'} reached`
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
|
|
@@ -181,7 +181,7 @@ export const canUploadFile = async (user: User, currentFileCount: number): Promi
|
|
|
181
181
|
if (currentFileCount >= limits.maxFilesPerCase) {
|
|
182
182
|
return {
|
|
183
183
|
canUpload: false,
|
|
184
|
-
reason: `
|
|
184
|
+
reason: `Demo account only: Maximum of ${limits.maxFilesPerCase} file${limits.maxFilesPerCase === 1 ? '' : 's'} per case reached`
|
|
185
185
|
};
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -199,13 +199,13 @@ export const getLimitsDescription = async (user: User): Promise<string> => {
|
|
|
199
199
|
try {
|
|
200
200
|
const userData = await getUserData(user);
|
|
201
201
|
if (!userData) {
|
|
202
|
-
return `Account limits: ${
|
|
202
|
+
return `Account limits: ${MAX_CASES_DEMO} case${MAX_CASES_DEMO === 1 ? '' : 's'}, ${MAX_FILES_PER_CASE_DEMO} file${MAX_FILES_PER_CASE_DEMO === 1 ? '' : 's'} per case`;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
if (userData.permitted) {
|
|
206
206
|
return '';
|
|
207
207
|
} else {
|
|
208
|
-
return `
|
|
208
|
+
return `Demo account only: ${MAX_CASES_DEMO} case${MAX_CASES_DEMO === 1 ? '' : 's'}, ${MAX_FILES_PER_CASE_DEMO} file${MAX_FILES_PER_CASE_DEMO === 1 ? '' : 's'} per case`;
|
|
209
209
|
}
|
|
210
210
|
} catch (error) {
|
|
211
211
|
console.error('Error getting limits description:', error);
|
|
@@ -391,8 +391,9 @@ export const canAccessCase = async (user: User, caseNumber: string): Promise<Per
|
|
|
391
391
|
/**
|
|
392
392
|
* Check if user can modify a specific case
|
|
393
393
|
* - Regular users (permitted=true) can modify their owned cases
|
|
394
|
-
* -
|
|
395
|
-
* -
|
|
394
|
+
* - Demo users (permitted=false) can modify their owned cases
|
|
395
|
+
* - Both permitted and demo users can modify read-only cases for review
|
|
396
|
+
* - Nobody can modify cases marked as archived in the case data itself
|
|
396
397
|
*/
|
|
397
398
|
export const canModifyCase = async (user: User, caseNumber: string): Promise<PermissionResult> => {
|
|
398
399
|
try {
|
|
@@ -419,20 +420,21 @@ export const canModifyCase = async (user: User, caseNumber: string): Promise<Per
|
|
|
419
420
|
if (caseData.archived) {
|
|
420
421
|
return { allowed: false, reason: 'Archived cases are immutable and read-only' };
|
|
421
422
|
}
|
|
423
|
+
} else if (archiveCheckResponse.status !== 404) {
|
|
424
|
+
// Fail closed: if archive status can't be verified (worker error/timeout),
|
|
425
|
+
// block modification rather than risk mutating an archived case
|
|
426
|
+
return { allowed: false, reason: 'Unable to verify case archive status' };
|
|
422
427
|
}
|
|
423
428
|
|
|
424
429
|
// Check if user owns the case (regular cases)
|
|
425
430
|
if (userData.cases && userData.cases.some(c => c.caseNumber === caseNumber)) {
|
|
426
|
-
//
|
|
427
|
-
if (!userData.permitted) {
|
|
428
|
-
return { allowed: false, reason: 'Read-Only Account: Cannot modify owned cases' };
|
|
429
|
-
}
|
|
431
|
+
// Both permitted and demo users can modify their owned cases
|
|
430
432
|
return { allowed: true };
|
|
431
433
|
}
|
|
432
434
|
|
|
433
435
|
// Check if it's a read-only case that user can review
|
|
434
436
|
if (userData.readOnlyCases && userData.readOnlyCases.some(c => c.caseNumber === caseNumber)) {
|
|
435
|
-
// For read-only cases, both permitted and
|
|
437
|
+
// For read-only cases, both permitted and demo users can modify for review
|
|
436
438
|
// The actual read-only restrictions should be enforced at the case data level, not user level
|
|
437
439
|
return { allowed: true };
|
|
438
440
|
}
|
|
@@ -9,8 +9,8 @@ export const AUDIT_EXPORT_SIGNATURE_VERSION = '1.0';
|
|
|
9
9
|
|
|
10
10
|
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
|
|
11
11
|
|
|
12
|
-
export type AuditExportFormat = '
|
|
13
|
-
export type AuditExportType = '
|
|
12
|
+
export type AuditExportFormat = 'json';
|
|
13
|
+
export type AuditExportType = 'trail';
|
|
14
14
|
export type AuditExportScopeType = 'case' | 'user';
|
|
15
15
|
|
|
16
16
|
export interface AuditExportSigningPayload {
|
|
@@ -35,11 +35,11 @@ export function isValidAuditExportSigningPayload(
|
|
|
35
35
|
return false;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
if (payload.exportFormat !== '
|
|
38
|
+
if (payload.exportFormat !== 'json') {
|
|
39
39
|
return false;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
if (payload.exportType !== '
|
|
42
|
+
if (payload.exportType !== 'trail') {
|
|
43
43
|
return false;
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -23,7 +23,7 @@ interface BundledAuditExportFile {
|
|
|
23
23
|
exportVersion?: string;
|
|
24
24
|
totalEntries?: number;
|
|
25
25
|
application?: string;
|
|
26
|
-
exportType?: '
|
|
26
|
+
exportType?: 'trail';
|
|
27
27
|
scopeType?: 'case' | 'user';
|
|
28
28
|
scopeIdentifier?: string;
|
|
29
29
|
hash?: string;
|
|
@@ -164,7 +164,7 @@ async function verifyBundledAuditExport(
|
|
|
164
164
|
const embeddedSignaturePayload: Partial<AuditExportSigningPayload> = metadata.signatureMetadata ?? {
|
|
165
165
|
signatureVersion: metadata.signatureVersion,
|
|
166
166
|
exportFormat: 'json',
|
|
167
|
-
exportType:
|
|
167
|
+
exportType: 'trail',
|
|
168
168
|
scopeType: metadata.scopeType,
|
|
169
169
|
scopeIdentifier: metadata.scopeIdentifier,
|
|
170
170
|
generatedAt: metadata.exportTimestamp,
|
|
@@ -209,23 +209,15 @@ async function verifyBundledAuditExport(
|
|
|
209
209
|
|
|
210
210
|
/**
|
|
211
211
|
* Remove forensic warning from content for hash validation.
|
|
212
|
-
* Supports the warning
|
|
212
|
+
* Supports the warning format added to JSON case exports.
|
|
213
213
|
*/
|
|
214
214
|
export function removeForensicWarning(content: string): string {
|
|
215
215
|
const jsonForensicWarningRegex = /^\/\*\s*CASE\s+DATA\s+WARNING[\s\S]*?\*\/\s*\r?\n*/;
|
|
216
|
-
const csvForensicWarningRegex = /^"CASE DATA WARNING: This file contains evidence data for forensic examination\. Any modification may compromise the integrity of the evidence\. Handle according to your organization's chain of custody procedures\."(?:\r?\n){2}/;
|
|
217
216
|
|
|
218
217
|
let cleaned = content;
|
|
219
218
|
|
|
220
219
|
if (jsonForensicWarningRegex.test(content)) {
|
|
221
220
|
cleaned = content.replace(jsonForensicWarningRegex, '');
|
|
222
|
-
} else if (csvForensicWarningRegex.test(content)) {
|
|
223
|
-
cleaned = content.replace(csvForensicWarningRegex, '');
|
|
224
|
-
} else if (content.startsWith('"CASE DATA WARNING:')) {
|
|
225
|
-
const match = content.match(/^"[^"]*"(?:\r?\n)+/);
|
|
226
|
-
if (match) {
|
|
227
|
-
cleaned = content.substring(match[0].length);
|
|
228
|
-
}
|
|
229
221
|
}
|
|
230
222
|
|
|
231
223
|
return cleaned.replace(/^\s+/, '');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "5.4.
|
|
3
|
+
"version": "5.4.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Striae is a specialized, cloud-native platform designed to streamline forensic firearms identification by providing an intuitive environment for digital comparison image annotation, authenticated confirmations, and automated report generation.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -128,7 +128,7 @@
|
|
|
128
128
|
"typescript": "^5.9.3",
|
|
129
129
|
"vite": "^6.4.1",
|
|
130
130
|
"vite-tsconfig-paths": "^6.1.1",
|
|
131
|
-
"wrangler": "^4.
|
|
131
|
+
"wrangler": "^4.80.0"
|
|
132
132
|
},
|
|
133
133
|
"overrides": {
|
|
134
134
|
"@tootallnate/once": "3.0.1",
|
|
@@ -4,7 +4,7 @@ import type { CreateResponse, Env } from './types';
|
|
|
4
4
|
|
|
5
5
|
const corsHeaders: Record<string, string> = {
|
|
6
6
|
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
7
|
-
'Access-Control-Allow-Methods': 'GET, POST,
|
|
7
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
8
8
|
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key',
|
|
9
9
|
'Content-Type': 'application/json'
|
|
10
10
|
};
|
|
@@ -92,34 +92,5 @@ export async function handleAuditRequest(
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
if (!userId) {
|
|
97
|
-
return respond({ error: 'userId parameter is required' }, 400);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const prefix = `audit-trails/${userId}/`;
|
|
102
|
-
let deletedCount = 0;
|
|
103
|
-
let cursor: string | undefined;
|
|
104
|
-
|
|
105
|
-
do {
|
|
106
|
-
const listed = await bucket.list({ prefix, cursor, limit: 1000 });
|
|
107
|
-
|
|
108
|
-
const keys = listed.objects.map((obj) => obj.key);
|
|
109
|
-
if (keys.length > 0) {
|
|
110
|
-
await bucket.delete(keys);
|
|
111
|
-
deletedCount += keys.length;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
cursor = listed.truncated ? listed.cursor : undefined;
|
|
115
|
-
} while (cursor !== undefined);
|
|
116
|
-
|
|
117
|
-
return respond({ success: true, deletedCount });
|
|
118
|
-
} catch (error) {
|
|
119
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
120
|
-
return respond({ error: `Failed to delete audit entries: ${errorMessage}` }, 500);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return respond({ error: 'Method not allowed for audit endpoints. Only GET, POST, and DELETE are supported.' }, 405);
|
|
95
|
+
return respond({ error: 'Method not allowed for audit endpoints. Only GET and POST are supported.' }, 405);
|
|
125
96
|
}
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
2
|
+
"name": "data-worker",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"deploy": "wrangler deploy",
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"start": "wrangler dev"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@cloudflare/puppeteer": "^1.0.6",
|
|
12
|
+
"wrangler": "^4.80.0"
|
|
13
|
+
},
|
|
14
|
+
"overrides": {
|
|
15
|
+
"undici": "7.24.1",
|
|
16
|
+
"yauzl": "3.2.1"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -37,8 +37,8 @@ export interface ConfirmationSigningPayload {
|
|
|
37
37
|
confirmations: Record<string, ConfirmationRecord[]>;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
export type AuditExportFormat = '
|
|
41
|
-
export type AuditExportType = '
|
|
40
|
+
export type AuditExportFormat = 'json';
|
|
41
|
+
export type AuditExportType = 'trail';
|
|
42
42
|
export type AuditExportScopeType = 'case' | 'user';
|
|
43
43
|
|
|
44
44
|
export interface AuditExportSigningPayload {
|
|
@@ -187,11 +187,11 @@ export function isValidAuditExportPayload(
|
|
|
187
187
|
return false;
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
if (candidate.exportFormat !== '
|
|
190
|
+
if (candidate.exportFormat !== 'json') {
|
|
191
191
|
return false;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
if (candidate.exportType !== '
|
|
194
|
+
if (candidate.exportType !== 'trail') {
|
|
195
195
|
return false;
|
|
196
196
|
}
|
|
197
197
|
|