@striae-org/striae 5.5.2 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/components/actions/case-export/download-handlers.ts +130 -62
- package/app/components/actions/case-manage/archive-package-builder.ts +299 -0
- package/app/components/actions/case-manage/delete-helpers.ts +61 -0
- package/app/components/actions/case-manage/index.ts +2 -0
- package/app/components/actions/case-manage/operations.ts +714 -0
- package/app/components/actions/case-manage/types.ts +21 -0
- package/app/components/actions/case-manage/utils.ts +34 -0
- package/app/components/actions/case-manage.ts +1 -1079
- package/app/components/navbar/case-import/case-import.module.css +2 -2
- package/app/components/navbar/case-import/case-import.tsx +0 -8
- package/app/components/navbar/case-import/components/CasePreviewSection.tsx +1 -1
- package/app/components/navbar/case-modals/all-cases-modal.tsx +13 -1
- package/app/components/navbar/navbar.tsx +8 -5
- package/app/components/sidebar/cases/case-sidebar.tsx +3 -2
- package/app/routes/striae/striae.tsx +36 -11
- package/app/types/export.ts +1 -0
- package/app/utils/forensics/SHA256.ts +2 -2
- package/app/utils/forensics/audit-export-signature.ts +1 -1
- package/app/utils/forensics/confirmation-signature.ts +1 -1
- package/app/utils/forensics/signature-utils.ts +7 -2
- package/package.json +1 -1
- package/workers/audit-worker/package.json +1 -1
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +1 -1
- package/workers/data-worker/src/signature-utils.ts +7 -2
- 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
|
@@ -433,7 +433,7 @@
|
|
|
433
433
|
}
|
|
434
434
|
|
|
435
435
|
.archivedImportNote {
|
|
436
|
-
margin
|
|
436
|
+
margin: var(--spaceM) 0;
|
|
437
437
|
padding: var(--spaceS) var(--spaceM);
|
|
438
438
|
border-radius: var(--spaceXS);
|
|
439
439
|
background: color-mix(in lab, var(--success) 10%, transparent);
|
|
@@ -444,7 +444,7 @@
|
|
|
444
444
|
}
|
|
445
445
|
|
|
446
446
|
.archivedRegularCaseRiskNote {
|
|
447
|
-
margin
|
|
447
|
+
margin: var(--spaceM) 0;
|
|
448
448
|
padding: var(--spaceS) var(--spaceM);
|
|
449
449
|
border-radius: var(--spaceXS);
|
|
450
450
|
background: color-mix(in lab, var(--warning) 12%, transparent);
|
|
@@ -239,7 +239,6 @@ export const CaseImport = ({
|
|
|
239
239
|
if (!user || !importState.selectedFile || !importState.importType) return;
|
|
240
240
|
|
|
241
241
|
if (importState.importType === 'case' && isArchivedRegularCaseImportBlocked) {
|
|
242
|
-
setError(ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE);
|
|
243
242
|
return;
|
|
244
243
|
}
|
|
245
244
|
|
|
@@ -260,7 +259,6 @@ export const CaseImport = ({
|
|
|
260
259
|
casePreview,
|
|
261
260
|
updateImportState,
|
|
262
261
|
executeImport,
|
|
263
|
-
setError,
|
|
264
262
|
]);
|
|
265
263
|
|
|
266
264
|
const handleCancelImport = useCallback(() => {
|
|
@@ -333,7 +331,6 @@ export const CaseImport = ({
|
|
|
333
331
|
// Handle confirmation import
|
|
334
332
|
const handleConfirmImport = useCallback(() => {
|
|
335
333
|
if (isArchivedRegularCaseImportBlocked) {
|
|
336
|
-
setError(ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE);
|
|
337
334
|
return;
|
|
338
335
|
}
|
|
339
336
|
|
|
@@ -343,7 +340,6 @@ export const CaseImport = ({
|
|
|
343
340
|
isArchivedRegularCaseImportBlocked,
|
|
344
341
|
executeImport,
|
|
345
342
|
updateImportState,
|
|
346
|
-
setError,
|
|
347
343
|
]);
|
|
348
344
|
|
|
349
345
|
if (!isOpen) return null;
|
|
@@ -414,10 +410,6 @@ export const CaseImport = ({
|
|
|
414
410
|
{/* Import progress */}
|
|
415
411
|
<ProgressSection importProgress={importProgress} />
|
|
416
412
|
|
|
417
|
-
{isArchivedRegularCaseImportBlocked && (
|
|
418
|
-
<div className={styles.error}>{ARCHIVED_REGULAR_CASE_BLOCK_MESSAGE}</div>
|
|
419
|
-
)}
|
|
420
|
-
|
|
421
413
|
{/* Success message */}
|
|
422
414
|
{messages.success && (
|
|
423
415
|
<div className={styles.success}>
|
|
@@ -225,6 +225,14 @@ export const CasesModal = ({
|
|
|
225
225
|
selectedCase && selectedCase.caseNumber !== currentCase && !selectedCase.isReadOnly
|
|
226
226
|
);
|
|
227
227
|
|
|
228
|
+
const deleteSelectedCaseTitle = !selectedCase
|
|
229
|
+
? 'Select a case to delete.'
|
|
230
|
+
: selectedCase.caseNumber === currentCase
|
|
231
|
+
? 'Open a different case before deleting this one.'
|
|
232
|
+
: selectedCase.isReadOnly
|
|
233
|
+
? 'Read-only review cases cannot be deleted. Use Clear RO Case under Case Management first.'
|
|
234
|
+
: undefined;
|
|
235
|
+
|
|
228
236
|
useEffect(() => {
|
|
229
237
|
setCurrentPage(0);
|
|
230
238
|
}, [preferences.sortBy, preferences.confirmationFilter, preferences.showArchivedOnly]);
|
|
@@ -484,12 +492,15 @@ export const CasesModal = ({
|
|
|
484
492
|
const handleDeleteSelectedCase = async () => {
|
|
485
493
|
if (!selectedCase || !canDeleteSelectedCase) {
|
|
486
494
|
const isCurrentCaseSelection = selectedCase?.caseNumber === currentCase;
|
|
495
|
+
const isReadOnlyReviewSelection = selectedCase?.isReadOnly === true;
|
|
487
496
|
|
|
488
497
|
setActionNotice({
|
|
489
498
|
type: 'warning',
|
|
490
499
|
message: isCurrentCaseSelection
|
|
491
500
|
? 'Open a different case before deleting this one.'
|
|
492
|
-
:
|
|
501
|
+
: isReadOnlyReviewSelection
|
|
502
|
+
? 'Read-only review cases cannot be deleted. Use Clear RO Case under Case Management first.'
|
|
503
|
+
: 'Selected case cannot be deleted.',
|
|
493
504
|
});
|
|
494
505
|
return;
|
|
495
506
|
}
|
|
@@ -785,6 +796,7 @@ export const CasesModal = ({
|
|
|
785
796
|
className={`${styles.secondaryActionButton} ${styles.deleteActionButton}`}
|
|
786
797
|
onClick={handleDeleteSelectedCase}
|
|
787
798
|
disabled={!canDeleteSelectedCase || isRunningAction}
|
|
799
|
+
title={deleteSelectedCaseTitle}
|
|
788
800
|
>
|
|
789
801
|
Delete Selected
|
|
790
802
|
</button>
|
|
@@ -121,6 +121,11 @@ export const Navbar = ({
|
|
|
121
121
|
const isImageNotesActive = canOpenImageNotes;
|
|
122
122
|
const canDeleteCurrentFile = hasLoadedImage && !isReadOnly;
|
|
123
123
|
const isArchivedRegularReadOnly = Boolean(isReadOnly && archiveDetails?.archived && !isReviewOnlyCase);
|
|
124
|
+
const caseExportLabel = isArchivedRegularReadOnly
|
|
125
|
+
? 'Export Archive'
|
|
126
|
+
: isReadOnly
|
|
127
|
+
? 'Export Confirmations'
|
|
128
|
+
: 'Export Case Package';
|
|
124
129
|
|
|
125
130
|
return (
|
|
126
131
|
<>
|
|
@@ -179,11 +184,9 @@ export const Navbar = ({
|
|
|
179
184
|
type="button"
|
|
180
185
|
role="menuitem"
|
|
181
186
|
className={`${styles.caseMenuItem} ${styles.caseMenuItemExport}`}
|
|
182
|
-
disabled={!hasLoadedCase || disableLongRunningCaseActions
|
|
187
|
+
disabled={!hasLoadedCase || disableLongRunningCaseActions}
|
|
183
188
|
title={
|
|
184
|
-
|
|
185
|
-
? 'Export is unavailable for archived cases loaded from your regular case list'
|
|
186
|
-
: !hasLoadedCase
|
|
189
|
+
!hasLoadedCase
|
|
187
190
|
? 'Load a case to export case data'
|
|
188
191
|
: disableLongRunningCaseActions
|
|
189
192
|
? 'Export is unavailable while files are uploading'
|
|
@@ -194,7 +197,7 @@ export const Navbar = ({
|
|
|
194
197
|
setIsCaseMenuOpen(false);
|
|
195
198
|
}}
|
|
196
199
|
>
|
|
197
|
-
{
|
|
200
|
+
{caseExportLabel}
|
|
198
201
|
</button>
|
|
199
202
|
<button
|
|
200
203
|
type="button"
|
|
@@ -258,7 +258,8 @@ const handleImageSelect = (file: FileData) => {
|
|
|
258
258
|
? 'Select an image first'
|
|
259
259
|
: undefined;
|
|
260
260
|
|
|
261
|
-
const showCaseExportButton = Boolean(currentCase && isReadOnly
|
|
261
|
+
const showCaseExportButton = Boolean(currentCase && isReadOnly);
|
|
262
|
+
const caseExportButtonLabel = isArchivedCase ? 'Export Archive' : 'Export Confirmations';
|
|
262
263
|
|
|
263
264
|
const exportCaseTitle = isUploading
|
|
264
265
|
? 'Cannot export while uploading'
|
|
@@ -391,7 +392,7 @@ return (
|
|
|
391
392
|
disabled={isUploading || !currentCase}
|
|
392
393
|
title={exportCaseTitle}
|
|
393
394
|
>
|
|
394
|
-
|
|
395
|
+
{caseExportButtonLabel}
|
|
395
396
|
</button>
|
|
396
397
|
) : (
|
|
397
398
|
<button
|
|
@@ -19,7 +19,7 @@ import { FilesModal } from '~/components/sidebar/files/files-modal';
|
|
|
19
19
|
import { NotesEditorModal } from '~/components/sidebar/notes/notes-editor-modal';
|
|
20
20
|
import { UserAuditViewer } from '~/components/audit/user-audit-viewer';
|
|
21
21
|
import { fetchUserApi } from '~/utils/api';
|
|
22
|
-
import { type AnnotationData, type FileData } from '~/types';
|
|
22
|
+
import { type AnnotationData, type FileData, type ExportOptions } from '~/types';
|
|
23
23
|
import { validateCaseNumber, renameCase, deleteCase, checkExistingCase, createNewCase, archiveCase, getCaseArchiveDetails } from '~/components/actions/case-manage';
|
|
24
24
|
import { checkReadOnlyCaseExists, deleteReadOnlyCase } from '~/components/actions/case-review';
|
|
25
25
|
import { canCreateCase, getCaseConfirmationSummary } from '~/utils/data';
|
|
@@ -77,15 +77,21 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
77
77
|
|
|
78
78
|
// PDF generation states
|
|
79
79
|
const [isGeneratingPDF, setIsGeneratingPDF] = useState(false);
|
|
80
|
+
|
|
81
|
+
// Toast notification states
|
|
80
82
|
const [showToast, setShowToast] = useState(false);
|
|
81
83
|
const [toastMessage, setToastMessage] = useState('');
|
|
82
84
|
const [toastType, setToastType] = useState<ToastType>('success');
|
|
83
85
|
const [toastDuration, setToastDuration] = useState(4000);
|
|
86
|
+
|
|
87
|
+
// Modal and sidebar states
|
|
84
88
|
const [isAuditTrailOpen, setIsAuditTrailOpen] = useState(false);
|
|
85
89
|
const [isRenameCaseModalOpen, setIsRenameCaseModalOpen] = useState(false);
|
|
86
90
|
const [isOpenCaseModalOpen, setIsOpenCaseModalOpen] = useState(false);
|
|
87
91
|
const [isListCasesModalOpen, setIsListCasesModalOpen] = useState(false);
|
|
88
92
|
const [isFilesModalOpen, setIsFilesModalOpen] = useState(false);
|
|
93
|
+
|
|
94
|
+
// Case management action states
|
|
89
95
|
const [isRenamingCase, setIsRenamingCase] = useState(false);
|
|
90
96
|
const [isDeletingCase, setIsDeletingCase] = useState(false);
|
|
91
97
|
const [isArchivingCase, setIsArchivingCase] = useState(false);
|
|
@@ -93,6 +99,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
93
99
|
const [isOpeningCase, setIsOpeningCase] = useState(false);
|
|
94
100
|
const [openCaseHelperText, setOpenCaseHelperText] = useState('');
|
|
95
101
|
const [isArchiveCaseModalOpen, setIsArchiveCaseModalOpen] = useState(false);
|
|
102
|
+
|
|
103
|
+
// Export states
|
|
96
104
|
const [isExportCaseModalOpen, setIsExportCaseModalOpen] = useState(false);
|
|
97
105
|
const [isExportingCase, setIsExportingCase] = useState(false);
|
|
98
106
|
const [isExportConfirmationsModalOpen, setIsExportConfirmationsModalOpen] = useState(false);
|
|
@@ -287,7 +295,8 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
287
295
|
const handleExport = async (
|
|
288
296
|
exportCaseNumber: string,
|
|
289
297
|
designatedReviewerEmail?: string,
|
|
290
|
-
onProgress?: (progress: number, label: string) => void
|
|
298
|
+
onProgress?: (progress: number, label: string) => void,
|
|
299
|
+
exportOptions?: ExportOptions
|
|
291
300
|
) => {
|
|
292
301
|
if (!exportCaseNumber) {
|
|
293
302
|
showNotification('Select a case before exporting.', 'error');
|
|
@@ -311,7 +320,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
311
320
|
setShowToast(true);
|
|
312
321
|
onProgress?.(roundedProgress, label);
|
|
313
322
|
},
|
|
314
|
-
{ designatedReviewerEmail }
|
|
323
|
+
{ ...exportOptions, designatedReviewerEmail }
|
|
315
324
|
);
|
|
316
325
|
|
|
317
326
|
showNotification(`Case ${exportCaseNumber} exported successfully.`, 'success');
|
|
@@ -364,6 +373,28 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
364
373
|
}
|
|
365
374
|
};
|
|
366
375
|
|
|
376
|
+
const handleOpenCaseExport = () => {
|
|
377
|
+
if (!currentCase) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (isReadOnlyCase) {
|
|
382
|
+
if (isReviewOnlyCase) {
|
|
383
|
+
void handleOpenExportConfirmationsModal();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (archiveDetails.archived) {
|
|
388
|
+
void handleExport(currentCase, undefined, undefined, {
|
|
389
|
+
archivePackageMode: true,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
setIsExportCaseModalOpen(true);
|
|
396
|
+
};
|
|
397
|
+
|
|
367
398
|
const handleRenameCaseSubmit = async (newCaseName: string) => {
|
|
368
399
|
if (!currentCase) {
|
|
369
400
|
showNotification('Select a case before renaming.', 'error');
|
|
@@ -809,13 +840,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
809
840
|
void handleOpenCaseModal();
|
|
810
841
|
}}
|
|
811
842
|
onOpenListAllCases={() => setIsListCasesModalOpen(true)}
|
|
812
|
-
onOpenCaseExport={
|
|
813
|
-
if (isReadOnlyCase) {
|
|
814
|
-
void handleOpenExportConfirmationsModal();
|
|
815
|
-
} else {
|
|
816
|
-
setIsExportCaseModalOpen(true);
|
|
817
|
-
}
|
|
818
|
-
}}
|
|
843
|
+
onOpenCaseExport={handleOpenCaseExport}
|
|
819
844
|
onOpenAuditTrail={() => setIsAuditTrailOpen(true)}
|
|
820
845
|
onOpenRenameCase={() => setIsRenameCaseModalOpen(true)}
|
|
821
846
|
onDeleteCase={() => {
|
|
@@ -838,7 +863,7 @@ export const Striae = ({ user }: StriaePage) => {
|
|
|
838
863
|
onOpenCase={() => {
|
|
839
864
|
void handleOpenCaseModal();
|
|
840
865
|
}}
|
|
841
|
-
onOpenCaseExport={
|
|
866
|
+
onOpenCaseExport={handleOpenCaseExport}
|
|
842
867
|
imageId={imageId}
|
|
843
868
|
currentCase={currentCase}
|
|
844
869
|
imageLoaded={imageLoaded}
|
package/app/types/export.ts
CHANGED
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { verifySignaturePayload } from './signature-utils';
|
|
8
8
|
|
|
9
|
-
export const FORENSIC_MANIFEST_VERSION = '
|
|
10
|
-
export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-
|
|
9
|
+
export const FORENSIC_MANIFEST_VERSION = '3.0';
|
|
10
|
+
export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PSS-SHA-256';
|
|
11
11
|
|
|
12
12
|
export interface ForensicManifestData {
|
|
13
13
|
dataHash: string;
|
|
@@ -29,6 +29,8 @@ export interface PublicSigningKeyDetails {
|
|
|
29
29
|
publicKeyPem: string | null;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
const RSA_PSS_SALT_LENGTH = 32;
|
|
33
|
+
|
|
32
34
|
type ManifestSigningConfig = {
|
|
33
35
|
manifest_signing_public_keys?: Record<string, string>;
|
|
34
36
|
manifest_signing_public_key?: string;
|
|
@@ -197,7 +199,7 @@ export async function verifySignaturePayload(
|
|
|
197
199
|
'spki',
|
|
198
200
|
publicKeyPemToArrayBuffer(publicKeyPem, invalidPublicKeyError),
|
|
199
201
|
{
|
|
200
|
-
name: '
|
|
202
|
+
name: 'RSA-PSS',
|
|
201
203
|
hash: 'SHA-256'
|
|
202
204
|
},
|
|
203
205
|
false,
|
|
@@ -209,7 +211,10 @@ export async function verifySignaturePayload(
|
|
|
209
211
|
signatureBuffer.set(signatureBytes);
|
|
210
212
|
|
|
211
213
|
const verified = await crypto.subtle.verify(
|
|
212
|
-
{
|
|
214
|
+
{
|
|
215
|
+
name: 'RSA-PSS',
|
|
216
|
+
saltLength: RSA_PSS_SALT_LENGTH
|
|
217
|
+
},
|
|
213
218
|
key,
|
|
214
219
|
signatureBuffer,
|
|
215
220
|
new TextEncoder().encode(payload)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@striae-org/striae",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.0",
|
|
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",
|
|
@@ -5,6 +5,8 @@ export interface WorkerSignatureEnvelope {
|
|
|
5
5
|
value: string;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
const RSA_PSS_SALT_LENGTH = 32;
|
|
9
|
+
|
|
8
10
|
function base64UrlEncode(value: Uint8Array): string {
|
|
9
11
|
let binary = '';
|
|
10
12
|
for (const byte of value) {
|
|
@@ -57,7 +59,7 @@ export async function signPayload(
|
|
|
57
59
|
'pkcs8',
|
|
58
60
|
parsePkcs8PrivateKey(privateKey),
|
|
59
61
|
{
|
|
60
|
-
name: '
|
|
62
|
+
name: 'RSA-PSS',
|
|
61
63
|
hash: 'SHA-256'
|
|
62
64
|
},
|
|
63
65
|
false,
|
|
@@ -65,7 +67,10 @@ export async function signPayload(
|
|
|
65
67
|
);
|
|
66
68
|
|
|
67
69
|
const signature = await crypto.subtle.sign(
|
|
68
|
-
{
|
|
70
|
+
{
|
|
71
|
+
name: 'RSA-PSS',
|
|
72
|
+
saltLength: RSA_PSS_SALT_LENGTH
|
|
73
|
+
},
|
|
69
74
|
signingKey,
|
|
70
75
|
new TextEncoder().encode(payload)
|
|
71
76
|
);
|
|
@@ -52,10 +52,10 @@ export interface AuditExportSigningPayload {
|
|
|
52
52
|
hash: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export const FORENSIC_MANIFEST_VERSION = '
|
|
56
|
-
export const CONFIRMATION_SIGNATURE_VERSION = '
|
|
57
|
-
export const AUDIT_EXPORT_SIGNATURE_VERSION = '
|
|
58
|
-
export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-
|
|
55
|
+
export const FORENSIC_MANIFEST_VERSION = '3.0';
|
|
56
|
+
export const CONFIRMATION_SIGNATURE_VERSION = '3.0';
|
|
57
|
+
export const AUDIT_EXPORT_SIGNATURE_VERSION = '2.0';
|
|
58
|
+
export const FORENSIC_MANIFEST_SIGNATURE_ALGORITHM = 'RSASSA-PSS-SHA-256';
|
|
59
59
|
|
|
60
60
|
const SHA256_HEX_REGEX = /^[a-f0-9]{64}$/i;
|
|
61
61
|
|
package/wrangler.toml.example
CHANGED